DevSecOps guide

Policy as code for infrastructure security

Scanning your infrastructure definitions tells you what a change intends, not what your environment is. Those two diverge from the first manual fix applied during an incident, and in most estates a meaningful share of resources was never described in code at all. So template scanning is a useful control over new work and a poor assurance mechanism over an existing estate, and treating it as the latter is how teams end up with a clean pipeline and a public storage bucket.

Why is scanning template files not enough?

Because a template is not the deployed resource. Values arrive from variables, workspaces, remote state, module defaults and provider behaviour, so the file on disk frequently does not contain the property your policy needs to inspect. A rule looking for public access on a storage bucket will pass a template where the access setting is a variable reference, and the variable resolves to public in one environment.

Modules make this worse in a way that surprises people. A team using a shared module correctly has almost nothing security-relevant in their own repository, because the resource properties live inside the module. Scanning their files reports a clean result that is true and uninformative. The security properties of that deployment are decided by the module version they pinned.

The fix is to evaluate the plan rather than the source. A plan contains resolved values for the resources about to change, which is the closest build-time representation of reality you can get, and it is the artefact your policy engine should consume. Template scanning still earns its place as fast feedback during authoring, but the gate belongs at the plan.

Which control point catches what?

There are five, they overlap partially, and each one sees something the others cannot. Choosing deliberately is more valuable than adding all five, because every control point you add is a place policy has to be maintained.

Control pointWhat it seesWhat it missesFires when
Template scanning in the editor or pre-commitObvious misconfiguration written literally in the fileAnything from a variable, module or remote stateWhile the author is still writing
Policy evaluated against the planResolved values for the resources about to changeResources not managed by this pipelineOn the pull request, before apply
Admission control in the clusterEvery workload arriving, whatever created itAnything configured outside the clusterAt the moment a workload is admitted
Posture assessment against live APIsThe estate as it actually is, including unmanaged resourcesIntent, and who is about to change whatContinuously, after the fact
Drift detection against stateManual changes made outside the pipelineResources never in state to begin withOn a schedule, or before apply

What makes admission control different?

It is the only control point that sees everything arriving, regardless of who sent it. A cluster admission policy applies equally to a pipeline deployment, an emergency change made by hand and a workload created by an operator you installed six months ago. Every build-time check can be circumvented by not using the build, which is the route most emergency changes take.

The practical consequence is that your most important invariants belong here, not only in the pipeline. Containers must not run as privileged, must not mount the host filesystem, must not run as root without an explicit exception, must carry resource limits, must reference images from your own registry by digest. Those rules stated at admission are enforced. The same rules stated only in a pull request check are advisory.

The trap is enforcement order. Enable a policy in audit mode, look at what the existing estate violates, and fix or exempt each case before switching to enforce. Turning on default-deny against a populated cluster will block an autoscaler, a monitoring agent or an ingress controller within the hour, and the resulting rollback tends to end the initiative.

How do you keep a policy set from rotting?

Treat it as software with tests, not as a document with rules. Each policy should have a fixture that must fail it and a fixture that must pass it, running in the policy repository's own pipeline. Without that, a policy silently stops matching after a provider schema change and reports success forever, which is worse than not having it because you now hold evidence of a check that is not happening.

Version and release the policy set rather than editing it live. Consumers pin a version, you publish changes with a note on what newly fails, and teams can adopt on a schedule instead of discovering at merge time that a rule appeared overnight. This one practice does more for the relationship between platform and product teams than any amount of communication about shared responsibility.

Handle exceptions in the same repository as the policies, expressed as code with an owner and an expiry date. Exceptions granted in a ticket or a chat thread are invisible six months later, and an exception set that only grows is indistinguishable from a policy that does not exist. Reviewing expiring exceptions on a fixed cadence takes under an hour and is the mechanism that keeps the set truthful.

Which rules are worth writing first?

The ones covering how real cloud incidents actually start, which is a shorter list than any vendor's default policy pack. Publicly readable object storage. Security groups or firewall rules permitting administrative ports from the internet. Identity policies granting wildcard actions on wildcard resources. Unencrypted storage or database instances where your provider offers encryption by default. Logging or audit trail disabled on an account. Resources created without the tags your ownership model depends on.

Deliberately not first: the long tail of hardening benchmarks. Enabling an entire benchmark on day one produces thousands of findings across an existing estate, no ordering, and a team that concludes policy as code means noise. Benchmarks are a good source of rules to adopt gradually and a bad way to start.

The ownership tag rule is the one people skip and later regret. Almost every triage problem later in the programme reduces to not knowing which team owns a resource, and the only cheap moment to enforce that is at creation. It is also the rule with the least argument attached, which makes it a good first enforcement win.

Write rules against resource shape rather than against named resources wherever you can. A policy that exempts three specific bucket names ages badly, because the exemption outlives the reason and nobody dares remove it. A policy that permits public access only where a resource carries an explicit and expiring marker keeps working as the estate changes.

What can you test this week?

Take a change that was applied by hand to production in the last month, write it as a pull request, and see whether your policy set would have caught it. That single exercise tells you more about coverage than any dashboard, and it usually reveals that the manual change route bypasses every check you have.

Then pick your three most important infrastructure invariants and check where each is enforced. If all three are enforced only on the pull request path, an emergency change at midnight satisfies none of them, and moving at least one to admission or to a provider-level guardrail such as an organisation policy is the highest-value change available.

Last, break a policy deliberately in a test fixture and confirm the pipeline fails. Policies that have never been observed to fail should be assumed not to work, and this is a five minute test that regularly finds a rule that stopped matching months ago.

Common questions

What is policy as code for infrastructure?
It is expressing the rules your infrastructure must satisfy as versioned, testable code that a policy engine evaluates automatically, rather than as guidance in a document checked by a human reviewer. In practice the engine evaluates either a planned change before it is applied or a workload as it is admitted to a cluster, and it returns a pass or fail with the specific resource and rule that failed.
Why is scanning Terraform files not enough?
Because the file often does not contain the value your rule needs to inspect. Properties arrive from variables, workspaces, remote state and module defaults, so a bucket whose access setting is a variable reference will pass a scan and still deploy publicly. Teams using a shared module correctly have almost nothing security-relevant in their own repository at all. Evaluate the resolved plan instead, and keep file scanning as fast authoring feedback.
Should infrastructure policy run in the pipeline or at admission?
Both, because they cover different gaps. Pipeline evaluation gives the author feedback before anything is applied and can block a merge. Admission control sees every workload arriving regardless of who created it, including emergency changes made by hand and objects created by operators, which is exactly the traffic that bypasses the pipeline. Invariants you genuinely rely on should be enforced at admission, not only reviewed at merge.
Which infrastructure policies should you write first?
Public object storage, administrative ports open to the internet, identity policies with wildcard actions on wildcard resources, unencrypted storage where encryption is available, audit logging disabled at account level, and resources created without an ownership tag. Avoid enabling a full hardening benchmark on day one: it produces thousands of unordered findings against an existing estate and teaches the team that policy as code means noise.
How do you stop a policy set going stale?
Give every policy a fixture that must fail and one that must pass, running in the policy repository's own pipeline, so a rule that stops matching after a provider schema change is caught rather than reporting success forever. Version and release the set so consumers pin a version and can see what newly fails. Keep exceptions in the same repository as code, each with an owner and an expiry date, and review the expiring ones on a fixed cadence.
How do you introduce default-deny policies safely?
Run each policy in audit mode first, list what the current estate violates, and resolve or exempt every case before switching to enforce. Turning default-deny on against a populated cluster typically blocks an autoscaler, a monitoring agent or an ingress controller within the hour, and the emergency rollback that follows usually ends the initiative. Audit mode data is also the evidence you need to justify enforcement to the affected teams.

More on DevSecOps implementation

Let’s create something out of this world together.

Have a project in mind? Contact us for expert design and development solutions. Let’s discuss how we can help grow your business.

Azaadi Offer

Claim a free security assessment

Until 31 August we're covering the cost of a full vulnerability assessment and penetration test. Mention it in your message and we'll scope it with you.

  • Web application testing, authenticated and unauthenticated
  • Mobile application testing across iOS and Android
  • External network and infrastructure assessment
  • Manual exploitation by engineers, not scanner output

Testing and the report are free. Fixing what we find is quoted separately, with no obligation to accept.

Read the full offer

Tell us what you are trying to build and we will tell you plainly whether we are the right people for it. Book a call with an expert to work through the detail, or ask for a fixed quote if the scope is already clear. No obligation either way.

Four fields is all we need to get started.

Fastnexa Logo

© 2026 fastnexa. All rights reserved.