Expressing security and compliance rules as code so they are automated, auditable, and consistent — replacing manual checklists with policy engines that run in pipelines, admission controllers, and continuous monitors.
Expressing security and compliance rules as code so they are automated, auditable, and consistent — replacing manual checklists with policy engines that run in pipelines, admission controllers, and continuous monitors.
Lesson outline
Traditional compliance runs on annual or quarterly audit cycles: a spreadsheet of controls, a human checking each one, evidence collected by email. By the time an auditor finds an unencrypted database, it has been running in production for months.
Compliance as code (also called policy as code) replaces the spreadsheet with machine-readable rules that run continuously — in your CI pipeline, in Kubernetes admission webhooks, and as scheduled checks against live infrastructure. A violation that would have taken weeks to discover in a manual audit is caught in seconds, before it ever reaches production.
Mental model
Think of compliance as code as turning an audit checklist into unit tests. Instead of a human checking "is this S3 bucket public?" once a quarter, the policy runs on every Terraform plan and every hour against live resources. It either passes or it fails — automatically, consistently, with a paper trail.
The four core benefits
deny if input.resource.public == true
pattern:
spec:
securityContext:
runAsNonRoot: truedeny[msg] {
not input.encrypted
msg := "disk must be encrypted"
}managed rule: s3-bucket-public-read-prohibited
resource "aws_s3_bucket" "data" { bucket = "my-insecure-bucket" # missing: public_access_block # missing: tags}resource "aws_db_instance" "main" { identifier = "prod-db" engine = "postgres" instance_class = "db.t3.micro" # missing: storage_encrypted # missing: tags}Pipeline blocked — 3 policy violations
Fix and re-run.
| Policy Rule | CIS Benchmark | PCI-DSS | SOC 2 | HIPAA |
|---|---|---|---|---|
| S3 public access block | CIS 2.1.5 | Req 6.4 | CC6.1 | §164.312 |
| RDS encryption at rest | CIS 2.3.1 | Req 3.5 | CC6.7 | §164.312(a) |
| No root container | CIS 5.1.6 | Req 6.2 | CC6.1 | N/A |
| Mandatory resource tags | — | — | CC2.1 | §164.308 |
Compliance as code runs at three distinct layers, each catching different classes of violation at different stages of the software lifecycle.
| Layer | When it runs | What it evaluates | Tools | Enforcement action |
|---|---|---|---|---|
| Pipeline (pre-apply) | Before terraform apply or helm upgrade | Infrastructure plan (JSON), Helm values, Kubernetes manifests | Conftest + OPA/Rego, Checkov, tfsec, Terrascan | Block pipeline — change never applied |
| Admission (pre-schedule) | Before every kubectl apply / create | Kubernetes resources: Pods, Deployments, Services, Ingress | OPA Gatekeeper, Kyverno | Reject resource — pod never scheduled |
| Continuous (runtime) | Scheduled or event-driven against live resources | Live cloud resources (EC2, S3, RDS, IAM), K8s workloads | AWS Config, Cloud Custodian, Prowler | Alert, auto-remediate, or create ticket |
Use all three layers — they catch different things
Pipeline catches new violations before they are created. Admission catches runtime misconfigurations in Kubernetes. Continuous catches drift: things that were compliant when created but changed later (console click, config drift, new CVE). You need all three for full coverage.
Open Policy Agent (OPA) is the most widely used policy engine for infrastructure. Policies are written in Rego, a declarative query language. OPA evaluates Rego policies against JSON data (Terraform plans, Kubernetes manifests, API request payloads).
The key insight: a Terraform plan is just JSON. A Kubernetes manifest is just JSON. A Dockerfile is just text. All can be evaluated by a policy engine that reads structured data.
1# OPA/Rego policy: S3 buckets must have public access blockedpackage path: conftest/OPA uses this to organize and reference policies2# Run with: conftest test plan.json --policy policies/3package terraform.aws.s345import future.keywords.if6import future.keywords.in78# Deny any S3 bucket that lacks a public access block resource9deny[msg] if {deny rules: any message added to this set causes the policy check to fail10# Find every aws_s3_bucket resource in the plan11resource := input.resource_changes[_]12resource.type == "aws_s3_bucket"1314bucket_name := resource.name1516# Check if there is a matching public access block resource17not has_public_access_block(bucket_name)1819msg := sprintf(20"S3 bucket '%v' must have aws_s3_bucket_public_access_block with block_public_acls=true",21[bucket_name]22)23}Helper rule keeps the main deny rule readable — Rego is declarative, not procedural2425# Helper: returns true if a matching public access block exists26has_public_access_block(bucket_name) if {27block := input.resource_changes[_]28block.type == "aws_s3_bucket_public_access_block"29block.change.after.bucket == bucket_name30block.change.after.block_public_acls == true31block.change.after.block_public_policy == true32block.change.after.restrict_public_buckets == true33}3435# Deny RDS instances without encryptionEach deny rule is independent — one failure does not prevent others from running36deny[msg] if {37resource := input.resource_changes[_]38resource.type == "aws_db_instance"39resource.change.after.storage_encrypted != true40Always embed the compliance framework reference in the message — helps developers understand why41msg := sprintf(42"RDS instance '%v' must have storage_encrypted = true (PCI-DSS Req 3.5, CIS 2.3.1)",43[resource.name]44)45}4647# Deny EC2 instances without required tags48deny[msg] if {49resource := input.resource_changes[_]50resource.type == "aws_instance"5152required_tags := {"Environment", "Team", "CostCenter"}53actual_tags := {k | resource.change.after.tags[k]}54missing := required_tags - actual_tags55count(missing) > 05657msg := sprintf(58"EC2 instance '%v' is missing required tags: %v",59[resource.name, missing]60)61}
In Kubernetes, admission controllers intercept API server requests before they are persisted. OPA Gatekeeper and Kyverno both run as admission webhooks — when you kubectl apply a pod, the API server sends it to the webhook, which evaluates policies and either allows or rejects it.
The difference between the pipeline layer and admission layer: pipeline catches what you are about to create; admission catches what you are actually submitting to the cluster, including changes made outside of your pipeline (kubectl apply directly, Helm releases from a local machine, operator-created resources).
1# Kyverno policies for compliance-as-code in Kubernetes2# Deploy with: kubectl apply -f kyverno-compliance-policies.yaml34---5# Policy 1: Containers must not run as root6apiVersion: kyverno.io/v17kind: ClusterPolicy8metadata:9name: disallow-root-containers10annotations:11policies.kyverno.io/category: Pod Security12policies.kyverno.io/controls: "CIS 5.1.6, SOC2 CC6.1"13spec:validationFailureAction: Enforce blocks the request. Use Audit during rollout to baseline before enforcing.14validationFailureAction: Enforce15rules:16- name: no-root-user17match:18any:19- resources:20kinds: [Pod]21namespaces: [production, staging]namespace scoping: only enforce in production and staging — gives dev teams flexibility while protecting what matters22validate:Always include the compliance framework control in the error message — helps developers fix the right thing23message: >24Containers must not run as root (runAsUser: 0).25Set securityContext.runAsNonRoot: true and runAsUser >= 1000.26Controls: CIS 5.1.6, SOC2 CC6.127pattern:28spec:29containers:30- securityContext:31runAsNonRoot: true32runAsUser: ">=1000"3334---35# Policy 2: Resource limits must be set (prevents noisy-neighbour attacks)36apiVersion: kyverno.io/v137kind: ClusterPolicy38metadata:39name: require-resource-limits40annotations:41policies.kyverno.io/category: Resource Management42policies.kyverno.io/controls: "CIS 5.1.3"43spec:44validationFailureAction: Enforce45rules:46- name: check-resource-limits47match:48any:49- resources:50kinds: [Pod]51namespaces: [production, staging]52validate:53message: >54All containers must define CPU and memory limits.55Controls: CIS 5.1.356pattern:57spec:58containers:59- resources:60limits:61memory: "?*"62cpu: "?*"6364---65# Policy 3: Disallow hostPath volumes (container escape risk)hostPath volumes allow containers to mount host filesystem paths — a common container escape technique66apiVersion: kyverno.io/v167kind: ClusterPolicy68metadata:69name: disallow-host-path70annotations:71policies.kyverno.io/controls: "CIS 5.1.2, PCI-DSS Req 6.2"72spec:73validationFailureAction: Enforce74rules:75- name: no-hostpath76match:77any:78- resources:79kinds: [Pod]80validate:81message: "hostPath volumes are not allowed — use PersistentVolumeClaims instead."82deny:83conditions:84any:85- key: "{{ request.object.spec.volumes[].hostPath | length(@) }}"86operator: GreaterThan87value: 0
The real leverage of compliance as code is framework mapping: each policy rule maps to one or more compliance controls. When an auditor asks "how do you ensure S3 buckets are not public?", you show them the Rego rule, the CI pipeline run results, and the continuous compliance check history. No spreadsheet needed.
| Policy Rule | CIS Benchmark | PCI-DSS | SOC 2 | HIPAA |
|---|---|---|---|---|
| S3 public access block | CIS 2.1.5 | Req 6.4 | CC6.1 | §164.312 |
| RDS encryption at rest | CIS 2.3.1 | Req 3.5 | CC6.7 | §164.312(a)(2)(iv) |
| No root containers | CIS 5.1.6 | Req 6.2 | CC6.1 | N/A |
| Resource limits set | CIS 5.1.3 | Req 6.2 | A1.2 | N/A |
| No hostPath volumes | CIS 5.1.2 | Req 6.2 | CC6.1 | N/A |
| Required resource tags | — | — | CC2.1 | §164.308(a)(1) |
| TLS 1.2+ on ALB listeners | CIS 3.9 | Req 4.1 | CC6.7 | §164.312(e)(2)(ii) |
Compliance report generation
Tools like Prowler generate compliance reports by running hundreds of checks mapped to CIS, PCI-DSS, SOC 2, and HIPAA controls. Output is JSON/CSV/HTML — directly shareable with auditors. This replaces weeks of manual evidence collection with a single command: prowler aws --compliance cis_aws_2.0.
Compliance as code questions appear in senior DevSecOps, platform engineering, and cloud security architect interviews. Expect both conceptual questions ("explain policy as code") and practical ones ("write a Rego rule that denies S3 buckets without encryption"). Companies with SOC 2, PCI-DSS, or FedRAMP requirements will weight this heavily.
Common questions:
Strong answer: Mentioning the gap between pipeline and continuous checks — and why you need both. Knowing that Kyverno can also mutate resources (add security contexts automatically). Understanding that compliance-as-code produces audit evidence natively. Mentioning Prowler for AWS compliance report generation.
Red flags: Thinking compliance as code is the same as vulnerability scanning. Not knowing what an admission controller is. Treating compliance as an annual audit activity. Conflating writing policies with enforcing them (enforcement requires admission controllers or pipeline gates, not just code).
Quick check · Compliance as Code
1 / 4
Key takeaways
A new developer asks why you run both OPA Conftest in the CI pipeline AND Kyverno in Kubernetes. Explain why both are needed.
Conftest catches violations before terraform apply or helm install — blocking the creation of bad resources. Kyverno catches resources applied directly to Kubernetes outside of the pipeline (kubectl from a laptop, operator-created resources, Helm releases that bypass CI). The two layers are complementary: pipeline = before creation, admission = before scheduling, regardless of how the resource was submitted.
How do you handle the transition from no policies to enforced policies without causing production outages?
Roll out in audit mode first. Deploy the admission controller with validationFailureAction: Audit — it logs all violations but allows resources. Review the audit log to find all existing violations. Fix them (update deployments, add security contexts, etc.). Only then switch to Enforce mode. This prevents the scenario where flipping to Enforce immediately breaks existing workloads.
From the books
Hacking the Cloud — The Art of Cloud Security (Springer, 2023)
Covers OPA policy-as-code patterns for AWS and Kubernetes environments, including framework mapping to CIS Benchmarks and PCI-DSS controls.
💡 Analogy
Automated building code inspections
⚡ Core Idea
Building codes define what is allowed (load-bearing walls, electrical standards, fire egress). Inspectors check compliance. Compliance as code does the same thing for cloud infrastructure — but instead of annual inspections, every commit and every deployment is checked automatically.
🎯 Why It Matters
Manual compliance audits find violations months after they are introduced. By then, fixing them requires downtime, data migrations, or emergency patches. Compliance as code catches violations at the point of introduction — when a developer adds a misconfigured resource — making fixes cheap and fast.
Ready to see how this works in the cloud?
Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.
View role-based pathsSign in to track your progress and mark lessons complete.
Questions? Discuss in the community or start a thread below.
Join DiscordSign in to start or join a thread.