Policy as Code
Encoding organizational rules as executable checks that run in the pipeline, so guardrails scale without a review board.
Policy as code replaces “the architecture review board said no public S3 buckets” with a rule that runs on every plan and fails the build. The rule is version controlled, testable, and applies uniformly — including to the people who wrote it.
Why review boards do not scale
A human review gate has a fixed throughput and a variable quality. As change volume grows, one of two things happens: the gate becomes the bottleneck and teams route around it, or reviewers start rubber-stamping. Both outcomes leave you with the cost of the process and none of the protection.
flowchart LR
subgraph "Manual gate"
A1[Change] --> A2[Queue for review] --> A3[Human judgement]
A3 --> A4["Inconsistent · slow ·<br/>scales with reviewer headcount"]
end
subgraph "Policy as code"
B1[Change] --> B2[Automated evaluation<br/>seconds]
B2 --> B3["Consistent · immediate ·<br/>scales infinitely"]
B3 --> B4["Humans review the policies,<br/>not every change"]
end
The shift is that humans move from reviewing instances to reviewing rules. That is a much smaller, much higher-leverage workload.
Where policies run
Policy should be enforced at multiple points, because each catches a different escape route.
flowchart TD
A[Developer's editor<br/>pre-commit hook] -->|fastest feedback,<br/>bypassable| B[Pull request<br/>policy check on the plan]
B -->|blocks merge| C[Deployment gate<br/>re-evaluate before apply]
C -->|blocks apply| D[Admission controller<br/>at the cluster/cloud API]
D -->|blocks creation| E[Continuous audit<br/>scan running resources]
E -->|detects what slipped through| F[Alert or auto-remediate]
The layers are not redundant. Pre-commit is fast but bypassable; pull-request checks are authoritative for the pipeline path; admission control catches anything created outside the pipeline; continuous audit catches what existed before the policy did.
What to write policies about
Start with rules that are unambiguous, objectively checkable, and materially consequential.
| Domain | Example rules |
|---|---|
| Network exposure | No security group allows 0.0.0.0/0 on admin ports; no storage bucket is publicly readable |
| Encryption | All volumes, buckets, and managed databases encrypted at rest; TLS required in transit |
| Identity | No wildcard actions in IAM policies; no long-lived access keys; no privileged service accounts in default namespaces |
| Cost | Instance types restricted to an approved list; no untagged resources; storage lifecycle rules required |
| Availability | Production databases have backups enabled and multi-AZ; deletion protection on stateful resources |
| Tagging/ownership | Every resource carries owner, cost-centre, and environment tags |
Ownership tagging looks trivial next to encryption but is often the highest-value policy in the list, because everything else — cost attribution, incident routing, drift ownership, decommissioning — depends on knowing who owns a resource.
Severity levels and enforcement modes
A policy set with one severity level is unusable: either everything blocks and teams are stuck, or nothing blocks and nothing changes.
| Mode | Behaviour | Use for |
|---|---|---|
| Advisory | Reports, never blocks | New policies during rollout; style preferences |
| Soft-fail | Blocks unless explicitly acknowledged with a reason | Rules with legitimate exceptions |
| Hard-fail | Blocks unconditionally | Security and regulatory requirements |
The standard rollout sequence for any new policy is advisory → soft-fail → hard-fail, with a published date for each transition. Introducing a hard-fail policy against an existing estate breaks every pipeline at once and burns the credibility of the whole programme.
Exceptions must exist, and must expire
Every policy will eventually meet a legitimate exception. If there is no mechanism for one, teams will disable the check entirely — the worst possible outcome, because you lose the rule and the visibility.
A workable exception record:
exception:
policy: "storage-must-not-be-public"
resource: "s3://public-docs-site-assets"
justification: "Static marketing site assets, no customer data. Reviewed by security 2026-07-14."
approver: "security-team"
expires: "2027-01-14"
ticket: "SEC-4821"
The expires field is the one that matters. Permanent exceptions become
invisible policy holes. An expiring exception forces periodic re-justification,
and expiry should re-enable the block automatically rather than merely notifying
someone.
Policies are code and need tests
A policy with a bug is worse than no policy: a false negative gives false assurance, and a false positive blocks legitimate work and destroys trust in the system.
Every policy should ship with test cases in both directions:
policy: no-public-storage-buckets
should FAIL:
- bucket with public-read ACL
- bucket with policy allowing Principal "*" on GetObject
- bucket with public access block disabled
should PASS:
- private bucket with public access block enabled
- bucket with a policy scoped to a specific account principal
- bucket fronted by a CDN using origin access identity
The pass cases matter as much as the fail cases. A policy that blocks correct configurations will be worked around within a week.
Making failures actionable
A policy failure should tell the engineer what is wrong, why the rule exists, and how to fix it. Compare:
DENY: rule aws-s3-002 violated
with:
Policy failed: storage bucket must not be publicly accessible Resource:
aws_s3_bucket.reportsThe bucket’s public access block is disabled, which allows a future ACL change to expose its contents. Customer data must not be reachable without authentication (control: data-protection-3). Fix: addaws_s3_bucket_public_access_blockwith all four settings set totrue. Example:modules/private-data-bucket. Exception: if this bucket genuinely serves public content, request an exception via SEC intake.
The second version resolves itself. The first generates a support ticket.
Continuous audit of what already exists
Pipeline policies only govern changes going forward. The existing estate needs a separate scan, because it was built before the policy existed and no plan will ever evaluate it.
Run policy evaluation against live resources on a schedule, and treat the result as a backlog with an owner per finding rather than a report nobody reads. This is also the mechanism that produces continuous compliance evidence — see Compliance as Code.
Adoption checklist
- Policies are version-controlled, reviewed, and tested with pass and fail cases.
- Enforcement happens at plan time, deploy time, and admission time.
- Every new policy rolls out advisory → soft-fail → hard-fail on a published schedule.
- An exception process exists, is used, and every exception has an expiry date.
- Expired exceptions automatically re-enable enforcement.
- Failure messages state the rule, the rationale, and the fix.
- Existing resources are scanned continuously, not just new changes.
Last updated 19 Aug 2026, 00:00 UTC.