$npx -y skills add BagelHole/DevOps-Security-Agent-Skills --skill policy-as-codeImplement policy as code with OPA, Sentinel, and Kyverno. Automate policy enforcement in CI/CD and infrastructure. Use when enforcing compliance through automation.
| 1 | # Policy as Code |
| 2 | |
| 3 | Automate policy enforcement through code using OPA/Rego, Kyverno, Checkov, and CI/CD integration to prevent compliance violations before they reach production. |
| 4 | |
| 5 | ## When to Use |
| 6 | |
| 7 | - Enforcing security and compliance policies on infrastructure-as-code changes |
| 8 | - Preventing misconfigured Kubernetes workloads from deploying |
| 9 | - Automating guardrails in CI/CD pipelines for Terraform, CloudFormation, or Helm |
| 10 | - Implementing organizational standards that must be consistently applied |
| 11 | - Replacing manual approval gates with automated policy checks |
| 12 | |
| 13 | ## Open Policy Agent (OPA) Rego Policies |
| 14 | |
| 15 | ```rego |
| 16 | # deny_public_s3.rego - Deny S3 buckets with public access |
| 17 | package terraform.aws.s3 |
| 18 | |
| 19 | import rego.v1 |
| 20 | |
| 21 | deny contains msg if { |
| 22 | resource := input.resource_changes[_] |
| 23 | resource.type == "aws_s3_bucket" |
| 24 | resource.change.after.acl == "public-read" |
| 25 | msg := sprintf( |
| 26 | "S3 bucket '%s' has public-read ACL. All buckets must be private. [Policy: no-public-s3]", |
| 27 | [resource.address] |
| 28 | ) |
| 29 | } |
| 30 | |
| 31 | deny contains msg if { |
| 32 | resource := input.resource_changes[_] |
| 33 | resource.type == "aws_s3_bucket" |
| 34 | resource.change.after.acl == "public-read-write" |
| 35 | msg := sprintf( |
| 36 | "S3 bucket '%s' has public-read-write ACL. This is strictly prohibited. [Policy: no-public-s3]", |
| 37 | [resource.address] |
| 38 | ) |
| 39 | } |
| 40 | ``` |
| 41 | |
| 42 | ```rego |
| 43 | # require_encryption.rego - Require encryption on data stores |
| 44 | package terraform.aws.encryption |
| 45 | |
| 46 | import rego.v1 |
| 47 | |
| 48 | deny contains msg if { |
| 49 | resource := input.resource_changes[_] |
| 50 | resource.type == "aws_db_instance" |
| 51 | not resource.change.after.storage_encrypted |
| 52 | msg := sprintf( |
| 53 | "RDS instance '%s' does not have storage encryption enabled. [Policy: require-rds-encryption]", |
| 54 | [resource.address] |
| 55 | ) |
| 56 | } |
| 57 | |
| 58 | deny contains msg if { |
| 59 | resource := input.resource_changes[_] |
| 60 | resource.type == "aws_ebs_volume" |
| 61 | not resource.change.after.encrypted |
| 62 | msg := sprintf( |
| 63 | "EBS volume '%s' is not encrypted. [Policy: require-ebs-encryption]", |
| 64 | [resource.address] |
| 65 | ) |
| 66 | } |
| 67 | |
| 68 | deny contains msg if { |
| 69 | resource := input.resource_changes[_] |
| 70 | resource.type == "aws_s3_bucket" |
| 71 | not has_encryption(resource) |
| 72 | msg := sprintf( |
| 73 | "S3 bucket '%s' does not have default encryption configured. [Policy: require-s3-encryption]", |
| 74 | [resource.address] |
| 75 | ) |
| 76 | } |
| 77 | |
| 78 | has_encryption(resource) if { |
| 79 | resource.change.after.server_side_encryption_configuration[_] |
| 80 | } |
| 81 | ``` |
| 82 | |
| 83 | ```rego |
| 84 | # require_tags.rego - Enforce mandatory tagging |
| 85 | package terraform.aws.tags |
| 86 | |
| 87 | import rego.v1 |
| 88 | |
| 89 | required_tags := {"Environment", "Owner", "CostCenter", "DataClassification"} |
| 90 | |
| 91 | deny contains msg if { |
| 92 | resource := input.resource_changes[_] |
| 93 | tags := object.get(resource.change.after, "tags", {}) |
| 94 | missing := required_tags - {key | tags[key]} |
| 95 | count(missing) > 0 |
| 96 | msg := sprintf( |
| 97 | "Resource '%s' is missing required tags: %v. [Policy: required-tags]", |
| 98 | [resource.address, missing] |
| 99 | ) |
| 100 | } |
| 101 | ``` |
| 102 | |
| 103 | ```rego |
| 104 | # restrict_regions.rego - Limit resource deployment to approved regions |
| 105 | package terraform.aws.regions |
| 106 | |
| 107 | import rego.v1 |
| 108 | |
| 109 | approved_regions := {"us-east-1", "us-west-2", "eu-west-1"} |
| 110 | |
| 111 | deny contains msg if { |
| 112 | resource := input.resource_changes[_] |
| 113 | provider_config := input.configuration.provider_config.aws |
| 114 | region := provider_config.expressions.region.constant_value |
| 115 | not region in approved_regions |
| 116 | msg := sprintf( |
| 117 | "Resource '%s' is in region '%s'. Approved regions: %v. [Policy: approved-regions]", |
| 118 | [resource.address, region, approved_regions] |
| 119 | ) |
| 120 | } |
| 121 | ``` |
| 122 | |
| 123 | ```bash |
| 124 | # Evaluate OPA policies against Terraform plan |
| 125 | terraform plan -out=tfplan |
| 126 | terraform show -json tfplan > tfplan.json |
| 127 | |
| 128 | # Run OPA evaluation |
| 129 | opa eval \ |
| 130 | --data policies/ \ |
| 131 | --input tfplan.json \ |
| 132 | "data.terraform.aws.s3.deny" \ |
| 133 | --format pretty |
| 134 | |
| 135 | # Use conftest for easier CI integration |
| 136 | conftest test tfplan.json --policy policies/ --output table |
| 137 | ``` |
| 138 | |
| 139 | ## Kyverno Kubernetes Policies |
| 140 | |
| 141 | ```yaml |
| 142 | # require-labels.yaml - Enforce required labels on all pods |
| 143 | apiVersion: kyverno.io/v1 |
| 144 | kind: ClusterPolicy |
| 145 | metadata: |
| 146 | name: require-labels |
| 147 | annotations: |
| 148 | policies.kyverno.io/title: Require Labels |
| 149 | policies.kyverno.io/category: Best Practices |
| 150 | policies.kyverno.io/severity: medium |
| 151 | spec: |
| 152 | validationFailureAction: Enforce |
| 153 | background: true |
| 154 | rules: |
| 155 | - name: check-required-labels |
| 156 | match: |
| 157 | any: |
| 158 | - resources: |
| 159 | kinds: |
| 160 | - Pod |
| 161 | validate: |
| 162 | message: >- |
| 163 | Labels 'app.kubernetes.io/name', 'app.kubernetes.io/version', |
| 164 | and 'team' are required on all Pods. |
| 165 | pattern: |
| 166 | metadata: |
| 167 | labels: |
| 168 | app.kubernetes.io/name: "?*" |
| 169 | app.kubernetes.io/version: "?*" |
| 170 | team: "?*" |
| 171 | --- |