Implement policy as code with OPA, Sentinel, and Kyverno. Automate policy enforcement in CI/CD and infrastructure. Use when enforcing compliance through automation.
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
Automate policy enforcement through code using OPA/Rego, Kyverno, Checkov, and CI/CD integration to prevent compliance violations before they reach production.
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
# deny_public_s3.rego - Deny S3 buckets with public access
package terraform.aws.s3
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]
)
}
# 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[_]
}
# 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]
)
}
# 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
# require-labels.yaml - Enforce required labels on all podsapiVersion:kyverno.io/v1kind:ClusterPolicymetadata:name:require-labelsannotations:policies.kyverno.io/title:RequireLabelspolicies.kyverno.io/category:BestPracticespolicies.kyverno.io/severity:mediumspec:validationFailureAction:Enforcebackground:truerules:-name:check-required-labelsmatch:any:-resources:kinds:-Podvalidate:message:>-
Labels 'app.kubernetes.io/name', 'app.kubernetes.io/version',
and 'team' are required on all Pods.
pattern:metadata:labels:app.kubernetes.io/name:"?*"app.kubernetes.io/version:"?*"team:"?*"---# disallow-privileged.yaml - Block privileged containersapiVersion:kyverno.io/v1kind:ClusterPolicymetadata:name:disallow-privileged-containersannotations:policies.kyverno.io/title:DisallowPrivilegedContainerspolicies.kyverno.io/category:PodSecuritypolicies.kyverno.io/severity:highspec:validationFailureAction:Enforcebackground:truerules:-name:deny-privilegedmatch:any:-resources:kinds:-Podvalidate:message:"Privileged containers are not allowed."pattern:spec:containers:-securityContext:privileged:"false"=(initContainers):-securityContext:privileged:"false"---# require-resource-limits.yaml - Enforce resource limitsapiVersion:kyverno.io/v1kind:ClusterPolicymetadata:name:require-resource-limitsannotations:policies.kyverno.io/title:RequireResourceLimitspolicies.kyverno.io/severity:mediumspec:validationFailureAction:Enforcebackground:truerules:-name:check-resource-limitsmatch:any:-resources:kinds:-Podvalidate: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 tagapiVersion:kyverno.io/v1kind:ClusterPolicymetadata:name:disallow-latest-tagannotations:policies.kyverno.io/title:DisallowLatestTagpolicies.kyverno.io/severity:mediumspec:validationFailureAction:Enforcebackground:truerules:-name:validate-image-tagmatch:any:-resources:kinds:-Podvalidate:message:"Images must use a specific tag, not 'latest'."pattern:spec:containers:-image:"!*:latest & *:*"---# restrict-image-registries.yaml - Allow only approved registriesapiVersion:kyverno.io/v1kind:ClusterPolicymetadata:name:restrict-image-registriesannotations:policies.kyverno.io/title:RestrictImageRegistriespolicies.kyverno.io/severity:highspec:validationFailureAction:Enforcebackground:truerules:-name:validate-registriesmatch:any:-resources:kinds:-Podvalidate: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 NetworkPoliciesapiVersion:kyverno.io/v1kind:ClusterPolicymetadata:name:require-networkpolicyannotations:policies.kyverno.io/title:RequireNetworkPolicypolicies.kyverno.io/severity:highspec:validationFailureAction:Auditbackground:truerules:-name:check-networkpolicymatch:any:-resources:kinds:-Deploymentpreconditions:all:-key:"{{request.object.metadata.namespace}}"operator:NotInvalue: ["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:AnyNotInvalue:"{{request.object.metadata.namespace}}"
Checkov Custom Checks
# custom_checks/require_s3_versioning.pyfrom checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories
classS3Versioning(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)
defscan_resource_conf(self, conf):
versioning = conf.get("versioning_configuration", [{}])
ifisinstance(versioning, list):
versioning = versioning[0] if versioning else {}
status = versioning.get("status", ["Disabled"])
ifisinstance(status, list):
status = status[0]
return CheckResult.PASSED if status == "Enabled"else CheckResult.FAILED
check = S3Versioning()
# custom_checks/require_rds_backup.pyfrom checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories
classRDSBackupRetention(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)
defscan_resource_conf(self, conf):
retention = conf.get("backup_retention_period", [0])
ifisinstance(retention, list):
retention = retention[0]
return CheckResult.PASSED ifint(retention) >= 7else CheckResult.FAILED
check = RDSBackupRetention()
# Run Checkov with custom checks
checkov -d ./terraform \
--framework terraform \
--external-checks-dir ./custom_checks \
--output cli \
--compact
# Run specific check IDs
checkov -d ./terraform \
--check CUSTOM_S3_001,CUSTOM_RDS_001,CKV_AWS_18,CKV_AWS_19
# 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
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:-Reviewallactiveexceptionsquarterly-Expiretemporaryexceptionsautomatically-Re-justifypermanentexceptionsannually-Trackexceptioncounttrendsasasecuritymetric
Policy Testing
# Test OPA policies with mock inputmkdir -p policies/tests
# Create test inputcat > 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 testscat > 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
Best Practices
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