Policy examples
This page provides example policies written for the Terraform policy framework. Refer to these examples to learn how to implement your own policies.
Encrypt EBS volumes
This example demonstrates a basic resource policy that ensures all AWS EBS volumes are encrypted.
Policy
The policy checks that each aws_ebs_volume resource has the encrypted attribute set to true.
policies/encrypted_ebs.policy.hcl
resource_policy "aws_ebs_volume" "ebs_encrypted" {
enforce {
condition = attrs.encrypted == true
error_message = "EBS volume is not encrypted"
}
}
Tests
This test includes two mock resources: one that passes the policy and one that fails.
tests/encrypted_ebs.policytest.hcl
policytest {
targets = ["../policies/encrypted_ebs.policy.hcl"]
}
resource "aws_ebs_volume" "pass" {
attrs = {
availability_zone = "us-east-1a"
size = 10
encrypted = true
}
}
resource "aws_ebs_volume" "fail" {
expect_failure = true
attrs = {
availability_zone = "us-east-1a"
size = 10
encrypted = false
}
}
Validate and test
Validate the policy syntax:
$ tfpolicy validate --policies=policies/encrypted_ebs.policy.hcl
Success! Policy is valid.
Run the tests:
$ tfpolicy test --policies=policies/encrypted_ebs.policy.hcl --tests=tests/encrypted_ebs.policytest.hcl
# encrypted_ebs.policytest.hcl... running
# resource.aws_ebs_volume.pass... pass
# resource.aws_ebs_volume.fail... pass
# encrypted_ebs.policytest.hcl... pass
Sample Terraform configuration
The following Terraform configuration defines an EBS volume that would pass this policy, and one that would fail:
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "us-west-2"
}
resource "aws_ebs_volume" "pass" {
availability_zone = "us-west-2a"
size = 10
encrypted = true
}
resource "aws_ebs_volume" "fail" {
availability_zone = "us-west-2b"
size = 10
encrypted = false
}
Ensure Azure managed disks have encryption settings
This example demonstrates a resource policy that validates nested attributes in Azure managed disks.
Policy
The policy ensures that every managed disk has encryption settings explicitly enabled.
policies/azure_disk_encryption.policy.hcl
resource_policy "azurerm_managed_disk" "require_encryption" {
enforce {
condition = attrs.encryption_settings_collection[0].enabled == true
error_message = "The managed disk must have encryption settings enabled."
}
}
Tests
This test includes both passing and failing test cases.
tests/azure_disk_encryption.policytest.hcl
policytest {
targets = ["../policies/azure_disk_encryption.policy.hcl"]
}
resource "azurerm_managed_disk" "pass" {
attrs = {
name = "data-disk-01"
storage_account_type = "Standard_LRS"
create_option = "Empty"
encryption_settings_collection = [
{
enabled = true
}
]
}
}
resource "azurerm_managed_disk" "fail" {
expect_failure = true
attrs = {
name = "shadow-disk-02"
storage_account_type = "Standard_LRS"
create_option = "Empty"
encryption_settings_collection = [
{
enabled = false
}
]
}
}
Validate and test
Validate the policy syntax:
$ tfpolicy validate --policies=policies/azure_disk_encryption.policy.hcl
Success! Policy is valid.
Run the tests:
$ tfpolicy test --policies=policies/azure_disk_encryption.policy.hcl --tests=tests/azure_disk_encryption.policytest.hcl
# azure_disk_encryption.policytest.hcl... running
# resource.azurerm_managed_disk.pass... pass
# resource.azurerm_managed_disk.fail... pass
# azure_disk_encryption.policytest.hcl... pass
Sample Terraform configuration
The following Terraform configuration defines Azure managed disks that would pass and fail this policy:
main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
required_version = ">= 1.2.0"
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}
# This disk would pass the policy (encryption enabled)
resource "azurerm_managed_disk" "pass" {
name = "data-disk-01"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
storage_account_type = "Standard_LRS"
create_option = "Empty"
disk_size_gb = 10
encryption_settings {
enabled = true
}
}
# This disk would fail the policy (encryption disabled)
resource "azurerm_managed_disk" "fail" {
name = "shadow-disk-02"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
storage_account_type = "Standard_LRS"
create_option = "Empty"
disk_size_gb = 10
encryption_settings {
enabled = false
}
}
Verify module sources and versions
This example demonstrates a module policy that validates module sources and versions using meta-attributes and the semverconstraint function.
Policy
The policy ensures that VPC modules are sourced from an internal private registry and use version 2.0.0 or higher.
policies/module_validation.policy.hcl
module_policy "aws_vpc" "registry_check" {
enforce {
condition = core::startswith(meta.source, "app.terraform.io/my-org/")
error_message = "The VPC module (${meta.address}) must be sourced from the internal private registry (app.terraform.io/my-org/)."
}
enforce {
condition = core::semverconstraint(meta.version, ">= 2.0.0")
error_message = "The VPC module (${meta.address}) must be version 2.0.0 or higher."
}
}
Test
This test includes multiple failure scenarios to ensure the policy works correctly.
tests/module_validation.policytest.hcl
policytest {
targets = ["../policies/module_validation.policy.hcl"]
}
module "aws_vpc" "pass" {
meta = {
source = "app.terraform.io/my-org/vpc/aws"
version = "2.1.0"
}
attrs = {
enable_flow_log = true
}
}
module "aws_vpc" "fail_version" {
expect_failure = true
meta = {
source = "app.terraform.io/my-org/vpc/aws"
version = "1.9.0"
}
attrs = {
enable_flow_log = true
}
}
module "aws_vpc" "fail_source" {
expect_failure = true
meta = {
source = "terraform-aws-modules/vpc/aws"
version = "2.1.0"
}
attrs = {
enable_flow_log = true
}
}
Validate and test
Validate the policy syntax:
$ tfpolicy validate --policies=policies/module_validation.policy.hcl
Success! Policy is valid.
Run the tests:
$ tfpolicy test --policies=policies/module_validation.policy.hcl --tests=tests/module_validation.policytest.hcl
# module_validation.policytest.hcl... running
# module.aws_vpc.pass... pass
# module.aws_vpc.fail_version... pass
# module.aws_vpc.fail_source... pass
# module_validation.policytest.hcl... pass
Sample Terraform configuration
The following Terraform configuration uses modules that would pass and fail this policy:
main.tf
terraform {
required_version = ">= 1.2.0"
}
module "pass" {
source = "app.terraform.io/my-org/vpc/aws"
version = "2.1.0"
vpc_cidr = "10.10.0.0/16"
enable_flow_log = true
}
module "fail_version" {
source = "app.terraform.io/my-org/vpc/aws"
version = "1.9.0"
vpc_cidr = "10.20.0.0/16"
enable_flow_log = true
}
module "fail_source" {
source = "terraform-aws-modules/vpc/aws"
version = "2.1.0"
vpc_cidr = "10.30.0.0/16"
enable_flow_log = true
}
Ensure only the official AWS provider is used
This example demonstrates a provider policy that validates the provider source.
Policy
The policy ensures that only the official HashiCorp AWS provider is used.
policies/aws_provider.policy.hcl
provider_policy "aws" "official_source" {
enforce {
condition = meta.source == "hashicorp/aws"
error_message = "Only the official HashiCorp AWS provider is permitted."
}
}
Test
This test includes multiple failure scenarios including untrusted sources.
tests/provider_validation.policytest.hcl
policytest {
targets = ["../policies/aws_provider.policy.hcl"]
}
provider "aws" "pass" {
meta = {
source = "hashicorp/aws"
}
attrs = {
region = "us-east-1"
}
}
provider "aws" "fail" {
expect_failure = true
meta = {
source = "untrusted-registry.com/fake/aws"
}
attrs = {
region = "us-east-1"
}
}
Validate and test
Validate the policy syntax:
$ tfpolicy validate --policies=policies/aws_provider.policy.hcl
Success! Policy is valid.
Run the tests:
$ tfpolicy test --policies=policies/aws_provider.policy.hcl --tests=tests/provider_validation.policytest.hcl
# provider_validation.policytest.hcl... running
# provider.aws.pass... pass
# provider.aws.fail... pass
# provider_validation.policytest.hcl... pass
Sample Terraform configuration
The following Terraform configuration uses providers that would pass and fail this policy:
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
alias = "pass"
region = "us-east-1"
}
provider "aws" {
alias = "fail"
region = "us-east-1"
source = "untrusted-registry.com/fake/aws"
}
resource "aws_s3_bucket" "example" {
provider = aws.pass
bucket = "my-example-bucket"
}
Validate CloudTrail S3 bucket ACL
This example demonstrates using core::getresources() to validate relationships between resources.
Policy
The policy ensures that CloudTrail resources are associated with S3 buckets that have private ACLs.
policies/cloudtrail_s3_acl.policy.hcl
resource_policy "aws_cloudtrail" "must_be_private" {
locals {
s3_bucket_acl = core::getresources("aws_s3_bucket_acl", {
bucket = attrs.s3_bucket_name
})
}
enforce {
condition = local.s3_bucket_acl[0].acl == "private"
error_message = "S3 bucket associated with CloudTrail is not private"
}
}
Test
This test demonstrates using the skip attribute to mock dependencies without evaluating policies against them.
tests/cloudtrail_s3_acl.policytest.hcl
policytest {
targets = ["../policies/cloudtrail_s3_acl.policy.hcl"]
}
resource "aws_s3_bucket_acl" "pass" {
skip = true
attrs = {
bucket = "cloudtrail-logs-private"
acl = "private"
}
}
resource "aws_cloudtrail" "pass" {
attrs = {
name = "secure-trail"
s3_bucket_name = aws_s3_bucket_acl.pass.bucket
enable_logging = true
}
}
resource "aws_s3_bucket_acl" "fail" {
skip = true
attrs = {
bucket = "cloudtrail-logs-public"
acl = "public-read"
}
}
resource "aws_cloudtrail" "fail" {
expect_failure = true
attrs = {
name = "insecure-trail"
s3_bucket_name = aws_s3_bucket_acl.fail_bucket.bucket
enable_logging = true
}
}
Validate and test
Validate the policy syntax:
$ tfpolicy validate --policies=policies/cloudtrail_s3_acl.policy.hcl
Success! Policy is valid.
Run the tests:
$ tfpolicy test --policies=policies/cloudtrail_s3_acl.policy.hcl --tests=tests/cloudtrail_s3_acl.policytest.hcl
# cloudtrail_s3_acl.policytest.hcl... running
# resource.aws_cloudtrail.pass... pass
# resource.aws_cloudtrail.fail... pass
# cloudtrail_s3_acl.policytest.hcl... pass
Sample Terraform configuration
The following Terraform configuration defines CloudTrail and S3 bucket resources that would be evaluated against this policy:
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = "us-west-2"
}
resource "aws_s3_bucket" "cloudtrail_bucket" {
bucket = "my-cloudtrail-logs-bucket"
}
resource "aws_s3_bucket_acl" "my_bucket_acl" {
bucket = aws_s3_bucket.cloudtrail_bucket.id
acl = "private"
}
resource "aws_cloudtrail" "my_cloudtrail" {
name = "my-cloudtrail"
s3_bucket_name = aws_s3_bucket.cloudtrail_bucket.id
enable_logging = true
}
Multiple policies with core functions and plugins
This example demonstrates working with multiple policies in a single project, including both core functions and custom plugins. The policies validate Azure network interface security group associations and Azure subnet CIDR ranges.
Policies
Create two policy files in the policies/ directory.
The first policy ensures that all network interfaces are associated with a network security group using core::getresources():
policies/azure_nic_nsg.policy.hcl
resource_policy "azurerm_network_interface" "require_nsg_association" {
locals {
associations = core::getresources("azurerm_network_interface_security_group_association", {network_interface_id = attrs.id})
}
enforce {
condition = core::length(local.associations) > 0
error_message = "The Network Interface ${attrs.name} must be associated with a Network Security Group."
}
}
The second policy uses a custom plugin function to validate that subnet CIDR ranges don't overlap with reserved ranges:
policies/azure_subnet_cidr.policy.hcl
policy {
plugins {
network = {
source = "../plugins/bin/cidr_utils"
}
}
}
resource_policy "azurerm_subnet" "no_cidr_overlap" {
locals {
reserved_cidrs = ["10.0.0.0/24", "10.0.1.0/24"]
}
enforce {
condition = !plugin::network::cidr_overlaps(attrs.address_prefixes[0], local.reserved_cidrs)
error_message = "Subnet CIDR ${attrs.address_prefixes[0]} overlaps with reserved ranges."
}
}
Plugin
The above policy uses the following plugin, authored in Go. It implements the logic to compare a CIDR against a restricted list and serves the function via the plugin server.
plugins/src/cidr_utils/main.go
package main
import (
"fmt"
"net/netip"
"strings"
"github.com/hashicorp/terraform-policy-plugin-framework/policy-plugin/plugins"
)
func main() {
plugins.RegisterFunction("cidr_overlaps", cidr_overlaps)
plugins.Serve()
}
func trim(value, prefix string) (string, error) {
return strings.TrimPrefix(value, prefix), nil
}
func cidr_overlaps(checkStr string, restrictedStrs []string) (bool, error) {
checkPrefix, err := netip.ParsePrefix(checkStr)
if err != nil {
return false, fmt.Errorf("Invalid CIDR format")
}
for _, s := range restrictedStrs {
restrictedPrefix, err := netip.ParsePrefix(s)
if err != nil {
continue
}
if checkPrefix.Overlaps(restrictedPrefix) {
return true, nil
}
}
return false, nil
}
Before you can use your plugin in your policies, you must compile it.
Change into the plugin source directory.
$ cd plugins/src/cidr_utils
Initialize go modules.
$ go mod init cidr_utils
Download and verify go modules.
$ go mod tidy
Build the plugin.
$ go build -o ../../bin/cidr_utils main.go
Tests
Create two test files in the tests/ directory to test both policies.
The first test validates the Azure network interface policy:
tests/azure_nic_nsg.policytest.hcl
policytest {
targets = ["../policies/azure_nic_nsg.policy.hcl"]
}
resource "azurerm_network_interface" "pass" {
attrs = {
name = "web-nic-01"
id = "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkInterfaces/web-nic-01"
}
}
resource "azurerm_network_interface_security_group_association" "nic_link" {
attrs = {
network_interface_id = azurerm_network_interface.pass.id
network_security_group_id = "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkSecurityGroups/web-nsg"
}
}
resource "azurerm_network_interface" "fail" {
expect_failure = true
attrs = {
name = "rogue-nic-02"
id = "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/networkInterfaces/rogue-nic-02"
}
}
The second test validates the CIDR overlap policy:
tests/azure_subnet_cidr.policytest.hcl
policytest {
targets = ["../policies/azure_subnet_cidr.policy.hcl"]
}
resource "azurerm_subnet" "pass" {
attrs = {
name = "example-subnet"
address_prefixes = ["10.0.2.0/24"]
virtual_network_name = "example-vnet"
resource_group_name = "example-rg"
}
}
resource "azurerm_subnet" "fail" {
expect_failure = true
attrs = {
name = "reserved-subnet"
address_prefixes = ["10.0.0.0/24"]
virtual_network_name = "example-vnet"
resource_group_name = "example-rg"
}
}
Validate and test
Validate both policies:
$ tfpolicy validate --policies=policies/
Success! All policies are valid.
Run all tests:
$ tfpolicy test --policies=policies/ --tests=tests/
# azure_nic_nsg.policytest.hcl... running
# resource.azurerm_network_interface.pass... pass
# resource.azurerm_network_interface.fail... pass
# azure_nic_nsg.policytest.hcl... pass
# azure_subnet_cidr.policytest.hcl... running
# resource.azurerm_subnet.pass... pass
# resource.azurerm_subnet.fail... pass
# azure_subnet_cidr.policytest.hcl... pass
Sample Terraform configuration
The following Terraform configuration demonstrates Azure resources that would be evaluated against both policies:
example-main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
required_version = ">= 1.2.0"
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}
resource "azurerm_virtual_network" "example" {
name = "example-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
}
resource "azurerm_network_security_group" "example" {
name = "web-nsg"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
}
# This subnet would pass the CIDR policy (no overlap with reserved ranges)
resource "azurerm_subnet" "pass" {
name = "allowed-subnet"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.2.0/24"]
}
# This subnet would fail the CIDR policy (overlaps with reserved range)
resource "azurerm_subnet" "fail" {
name = "reserved-subnet"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.0.0/24"]
}
# This network interface would pass the NSG policy (has NSG association)
resource "azurerm_network_interface" "pass" {
name = "web-nic-01"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.pass.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_network_interface_security_group_association" "pass" {
network_interface_id = azurerm_network_interface.pass.id
network_security_group_id = azurerm_network_security_group.example.id
}
# This network interface would fail the NSG policy (no NSG association)
resource "azurerm_network_interface" "fail" {
name = "rogue-nic-02"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.pass.id
private_ip_address_allocation = "Dynamic"
}
}