2026-01-27 17:35:45 -05:00
---
name : policy-as-code
description : Implement policy as code with OPA, Sentinel, and Kyverno. Automate policy enforcement in CI/CD and infrastructure. Use when enforcing compliance through automation.
license : MIT
metadata :
author : devops-skills
version : "1.0"
---
# Policy as Code
2026-03-24 18:02:50 -04:00
Automate policy enforcement through code using OPA/Rego, Kyverno, Checkov, and CI/CD integration to prevent compliance violations before they reach production.
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
## When to Use
- Enforcing security and compliance policies on infrastructure-as-code changes
- Preventing misconfigured Kubernetes workloads from deploying
- Automating guardrails in CI/CD pipelines for Terraform, CloudFormation, or Helm
- Implementing organizational standards that must be consistently applied
- Replacing manual approval gates with automated policy checks
## Open Policy Agent (OPA) Rego Policies
2026-01-27 17:35:45 -05:00
```rego
2026-03-24 18:02:50 -04:00
# deny_public_s3.rego - Deny S3 buckets with public access
package terraform . aws . s3
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
import rego . v1
deny contains msg if {
resource := input . resource_changes [ _ ]
resource . type == "aws_s3_bucket"
resource . change . after . acl == "public-read"
msg := sprintf (
"S3 bucket '%s' has public-read ACL. All buckets must be private. [Policy: no-public-s3]" ,
[ resource . address ]
)
}
deny contains msg if {
resource := input . resource_changes [ _ ]
resource . type == "aws_s3_bucket"
resource . change . after . acl == "public-read-write"
msg := sprintf (
"S3 bucket '%s' has public-read-write ACL. This is strictly prohibited. [Policy: no-public-s3]" ,
[ resource . address ]
)
2026-01-27 17:35:45 -05:00
}
```
2026-03-24 18:02:50 -04:00
```rego
# require_encryption.rego - Require encryption on data stores
package terraform . aws . encryption
import rego . v1
deny contains msg if {
resource := input . resource_changes [ _ ]
resource . type == "aws_db_instance"
not resource . change . after . storage_encrypted
msg := sprintf (
"RDS instance '%s' does not have storage encryption enabled. [Policy: require-rds-encryption]" ,
[ resource . address ]
)
}
deny contains msg if {
resource := input . resource_changes [ _ ]
resource . type == "aws_ebs_volume"
not resource . change . after . encrypted
msg := sprintf (
"EBS volume '%s' is not encrypted. [Policy: require-ebs-encryption]" ,
[ resource . address ]
)
}
deny contains msg if {
resource := input . resource_changes [ _ ]
resource . type == "aws_s3_bucket"
not has_encryption ( resource )
msg := sprintf (
"S3 bucket '%s' does not have default encryption configured. [Policy: require-s3-encryption]" ,
[ resource . address ]
)
}
has_encryption ( resource ) if {
resource . change . after . server_side_encryption_configuration [ _ ]
}
```
```rego
# require_tags.rego - Enforce mandatory tagging
package terraform . aws . tags
import rego . v1
required_tags := { "Environment" , "Owner" , "CostCenter" , "DataClassification" }
deny contains msg if {
resource := input . resource_changes [ _ ]
tags := object . get ( resource . change . after , "tags" , {})
missing := required_tags - { key | tags [ key ]}
count ( missing ) > 0
msg := sprintf (
"Resource '%s' is missing required tags: %v. [Policy: required-tags]" ,
[ resource . address , missing ]
)
}
```
```rego
# restrict_regions.rego - Limit resource deployment to approved regions
package terraform . aws . regions
import rego . v1
approved_regions := { "us-east-1" , "us-west-2" , "eu-west-1" }
deny contains msg if {
resource := input . resource_changes [ _ ]
provider_config := input . configuration . provider_config . aws
region := provider_config . expressions . region . constant_value
not region in approved_regions
msg := sprintf (
"Resource '%s' is in region '%s'. Approved regions: %v. [Policy: approved-regions]" ,
[ resource . address , region , approved_regions ]
)
}
```
```bash
# Evaluate OPA policies against Terraform plan
terraform plan -out= tfplan
terraform show -json tfplan > tfplan.json
# Run OPA evaluation
opa eval \
--data policies/ \
--input tfplan.json \
"data.terraform.aws.s3.deny" \
--format pretty
# Use conftest for easier CI integration
conftest test tfplan.json --policy policies/ --output table
```
## Kyverno Kubernetes Policies
2026-01-27 17:35:45 -05:00
```yaml
2026-03-24 18:02:50 -04:00
# require-labels.yaml - Enforce required labels on all pods
2026-01-27 17:35:45 -05:00
apiVersion : kyverno.io/v1
kind : ClusterPolicy
metadata :
name : require-labels
2026-03-24 18:02:50 -04:00
annotations :
policies.kyverno.io/title : Require Labels
policies.kyverno.io/category : Best Practices
policies.kyverno.io/severity : medium
2026-01-27 17:35:45 -05:00
spec :
2026-03-24 18:02:50 -04:00
validationFailureAction : Enforce
background : true
2026-01-27 17:35:45 -05:00
rules :
2026-03-24 18:02:50 -04:00
- name : check-required-labels
2026-01-27 17:35:45 -05:00
match :
2026-03-24 18:02:50 -04:00
any :
- resources :
kinds :
- Pod
2026-01-27 17:35:45 -05:00
validate :
2026-03-24 18:02:50 -04:00
message : >-
Labels 'app.kubernetes.io/name', 'app.kubernetes.io/version',
and 'team' are required on all Pods.
2026-01-27 17:35:45 -05:00
pattern :
metadata :
labels :
2026-03-24 18:02:50 -04:00
app.kubernetes.io/name : "?*"
app.kubernetes.io/version : "?*"
team : "?*"
---
# disallow-privileged.yaml - Block privileged containers
apiVersion : kyverno.io/v1
kind : ClusterPolicy
metadata :
name : disallow-privileged-containers
annotations :
policies.kyverno.io/title : Disallow Privileged Containers
policies.kyverno.io/category : Pod Security
policies.kyverno.io/severity : high
spec :
validationFailureAction : Enforce
background : true
rules :
- name : deny-privileged
match :
any :
- resources :
kinds :
- Pod
validate :
message : "Privileged containers are not allowed."
pattern :
spec :
containers :
- securityContext :
privileged : "false"
=(initContainers) :
- securityContext :
privileged : "false"
---
# require-resource-limits.yaml - Enforce resource limits
apiVersion : kyverno.io/v1
kind : ClusterPolicy
metadata :
name : require-resource-limits
annotations :
policies.kyverno.io/title : Require Resource Limits
policies.kyverno.io/severity : medium
spec :
validationFailureAction : Enforce
background : true
rules :
- name : check-resource-limits
match :
any :
- resources :
kinds :
- Pod
validate :
message : "All containers must have CPU and memory limits defined."
pattern :
spec :
containers :
- resources :
limits :
memory : "?*"
cpu : "?*"
---
# disallow-latest-tag.yaml - Block usage of 'latest' image tag
apiVersion : kyverno.io/v1
kind : ClusterPolicy
metadata :
name : disallow-latest-tag
annotations :
policies.kyverno.io/title : Disallow Latest Tag
policies.kyverno.io/severity : medium
spec :
validationFailureAction : Enforce
background : true
rules :
- name : validate-image-tag
match :
any :
- resources :
kinds :
- Pod
validate :
message : "Images must use a specific tag, not 'latest'."
pattern :
spec :
containers :
- image : "!*:latest & *:*"
---
# restrict-image-registries.yaml - Allow only approved registries
apiVersion : kyverno.io/v1
kind : ClusterPolicy
metadata :
name : restrict-image-registries
annotations :
policies.kyverno.io/title : Restrict Image Registries
policies.kyverno.io/severity : high
spec :
validationFailureAction : Enforce
background : true
rules :
- name : validate-registries
match :
any :
- resources :
kinds :
- Pod
validate :
message : >-
Images must come from approved registries:
123456789012.dkr.ecr.us-east-1.amazonaws.com or ghcr.io/your-org.
pattern :
spec :
containers :
- image : "123456789012.dkr.ecr.*.amazonaws.com/* | ghcr.io/your-org/*"
---
# require-networkpolicy.yaml - Ensure namespaces have NetworkPolicies
apiVersion : kyverno.io/v1
kind : ClusterPolicy
metadata :
name : require-networkpolicy
annotations :
policies.kyverno.io/title : Require Network Policy
policies.kyverno.io/severity : high
spec :
validationFailureAction : Audit
background : true
rules :
- name : check-networkpolicy
match :
any :
- resources :
kinds :
- Deployment
preconditions :
all :
- key : "{{request.object.metadata.namespace}}"
operator : NotIn
value : [ "kube-system" , "kube-public" ]
validate :
message : "A NetworkPolicy must exist in namespace '{{request.object.metadata.namespace}}' before deploying workloads."
deny :
conditions :
all :
- key : "{{request.object.metadata.namespace}}"
operator : AnyNotIn
value : "{{request.object.metadata.namespace}}"
2026-01-27 17:35:45 -05:00
```
2026-03-24 18:02:50 -04:00
## Checkov Custom Checks
```python
# custom_checks/require_s3_versioning.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult , CheckCategories
class S3Versioning ( BaseResourceCheck ):
def __init__ ( self ):
name = "Ensure S3 bucket has versioning enabled"
id = "CUSTOM_S3_001"
supported_resources = [ "aws_s3_bucket_versioning" ]
categories = [ CheckCategories . BACKUP_AND_RECOVERY ]
super () . __init__ ( name = name , id = id ,
categories = categories ,
supported_resources = supported_resources )
def scan_resource_conf ( self , conf ):
versioning = conf . get ( "versioning_configuration" , [{}])
if isinstance ( versioning , list ):
versioning = versioning [ 0 ] if versioning else {}
status = versioning . get ( "status" , [ "Disabled" ])
if isinstance ( status , list ):
status = status [ 0 ]
return CheckResult . PASSED if status == "Enabled" else CheckResult . FAILED
check = S3Versioning ()
```
```python
# custom_checks/require_rds_backup.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult , CheckCategories
class RDSBackupRetention ( BaseResourceCheck ):
def __init__ ( self ):
name = "Ensure RDS has backup retention of at least 7 days"
id = "CUSTOM_RDS_001"
supported_resources = [ "aws_db_instance" ]
categories = [ CheckCategories . BACKUP_AND_RECOVERY ]
super () . __init__ ( name = name , id = id ,
categories = categories ,
supported_resources = supported_resources )
def scan_resource_conf ( self , conf ):
retention = conf . get ( "backup_retention_period" , [ 0 ])
if isinstance ( retention , list ):
retention = retention [ 0 ]
return CheckResult . PASSED if int ( retention ) >= 7 else CheckResult . FAILED
check = RDSBackupRetention ()
```
2026-01-27 17:35:45 -05:00
```bash
2026-03-24 18:02:50 -04:00
# Run Checkov with custom checks
checkov -d ./terraform \
--framework terraform \
--external-checks-dir ./custom_checks \
--output cli \
--compact
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
# Run specific check IDs
checkov -d ./terraform \
--check CUSTOM_S3_001,CUSTOM_RDS_001,CKV_AWS_18,CKV_AWS_19
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
# Generate SARIF output for GitHub Advanced Security integration
checkov -d ./terraform \
--framework terraform \
--output sarif \
--output-file checkov-results.sarif
# Skip specific checks with documented justification
checkov -d ./terraform \
--skip-check CKV_AWS_145 \
--skip-check CKV_AWS_79
```
## CI/CD Pipeline Integration
```yaml
# GitHub Actions - Policy enforcement in PR workflow
name : Policy Checks
on :
pull_request :
paths :
- 'terraform/**'
- 'kubernetes/**'
jobs :
terraform-policy :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Setup Terraform
uses : hashicorp/setup-terraform@v3
- name : Terraform Init and Plan
working-directory : terraform/
run : |
terraform init -backend=false
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
- name : OPA Policy Check
uses : open-policy-agent/setup-opa@v2
with :
version : latest
- run : |
RESULTS=$(opa eval \
--data policies/ \
--input terraform/tfplan.json \
--format json \
"data.terraform" | jq '.result[0].expressions[0].value')
DENY_COUNT=$(echo "$RESULTS" | jq '[.. | .deny? // empty | .[] ] | length')
if [ "$DENY_COUNT" -gt 0 ]; then
echo "::error::Policy violations found:"
echo "$RESULTS" | jq '.. | .deny? // empty | .[]'
exit 1
fi
- name : Checkov Scan
uses : bridgecrewio/checkov-action@v12
with :
directory : terraform/
framework : terraform
output_format : cli,sarif
output_file_path : console,checkov-results.sarif
soft_fail : false
external_checks_dirs : custom_checks/
- name : Upload SARIF
if : always()
uses : github/codeql-action/upload-sarif@v3
with :
sarif_file : checkov-results.sarif
kubernetes-policy :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- name : Install Kyverno CLI
run : |
curl -LO https://github.com/kyverno/kyverno/releases/latest/download/kyverno-cli_linux_amd64.tar.gz
tar -xzf kyverno-cli_linux_amd64.tar.gz
sudo mv kyverno /usr/local/bin/
- name : Test Kyverno Policies
run : |
kyverno apply policies/kyverno/ \
--resource kubernetes/manifests/ \
--detailed-results \
--output-format table
- name : Conftest Kubernetes Manifests
uses : open-policy-agent/conftest-action@v2
with :
files : kubernetes/manifests/
policy : policies/kubernetes/
```
## Policy Exception Management
```yaml
exception_workflow :
request :
fields :
- policy_id : "Which policy needs an exception"
- resource : "Specific resource requiring exception"
- justification : "Business reason for the exception"
- compensating_controls : "Alternative mitigations in place"
- duration : "Temporary (with expiry) or permanent"
- requestor : "Person requesting"
- approver : "Security team member who approved"
approval_process :
1 : "Requestor submits exception with justification"
2 : "Security team reviews and assesses risk"
3 : "Compensating controls verified"
4 : "Exception approved or denied with rationale"
5 : "Exception documented in registry"
6 : "Automated enforcement updated to allow exception"
enforcement :
opa : |
# Exception list loaded as data
# policies/exceptions.json
# {"exceptions": [{"resource": "aws_s3_bucket.public_website", "policy": "no-public-s3", "expires": "2025-06-01"}]}
kyverno : |
# Use Kyverno PolicyException resource
apiVersion: kyverno.io/v2beta1
kind: PolicyException
metadata:
name: allow-public-website
namespace: web
spec:
exceptions:
- policyName: disallow-privileged-containers
ruleNames:
- deny-privileged
match:
any:
- resources:
kinds:
- Pod
names:
- legacy-app-*
review_schedule :
- Review all active exceptions quarterly
- Expire temporary exceptions automatically
- Re-justify permanent exceptions annually
- Track exception count trends as a security metric
```
## Policy Testing
```bash
# Test OPA policies with mock input
mkdir -p policies/tests
# Create test input
cat > policies/tests/public_bucket_test.json <<'EOF'
{
"resource_changes": [{
"address": "aws_s3_bucket.test",
"type": "aws_s3_bucket",
"change": {
"after": {"acl": "public-read"}
}
}]
}
EOF
# Run test
opa eval --data policies/ --input policies/tests/public_bucket_test.json \
"data.terraform.aws.s3.deny" --format pretty
# Should output the deny message
# OPA unit tests
cat > policies/tests/s3_test.rego <<'EOF'
package terraform.aws.s3_test
import rego.v1
import data.terraform.aws.s3
test_deny_public_bucket if {
result := s3.deny with input as {"resource_changes": [{"address": "test", "type": "aws_s3_bucket", "change": {"after": {"acl": "public-read"}}}]}
count(result) > 0
}
test_allow_private_bucket if {
result := s3.deny with input as {"resource_changes": [{"address": "test", "type": "aws_s3_bucket", "change": {"after": {"acl": "private"}}}]}
count(result) == 0
}
EOF
opa test policies/ -v
2026-01-27 17:35:45 -05:00
```
## Best Practices
2026-03-24 18:02:50 -04:00
- Version control all policies alongside the infrastructure code they govern
- Start in audit/warn mode and transition to enforce after verifying no false positives
- Write unit tests for every policy to catch regressions and verify intended behavior
- Implement a formal exception process: never disable policies to bypass legitimate checks
- Use policy results as PR status checks to block non-compliant merges
- Layer policies: Checkov for static analysis, OPA for Terraform plan evaluation, Kyverno for runtime
- Tag policies with compliance framework references (e.g., SOC 2 CC6.1, PCI Req 2.2)
- Monitor policy violation trends over time to identify systemic issues
- Provide clear, actionable error messages that explain how to fix violations
- Roll out new policies gradually: inform teams, give a remediation window, then enforce