Policy as code: which security controls to encode first
Policy as code is writing security and compliance rules as version-controlled code that a policy engine, such as Open Policy Agent, Kyverno or Sentinel, evaluates automatically before changes reach production. GRCMANA recommends encoding six rules first: public exposure, network rules, privileged access, encryption, configuration baselines and change approval, each rolled out in warn mode before enforcement.
Your compliance lead has forwarded two documents: the ISO 27001 surveillance audit plan and an enterprise customer's security questionnaire. Both ask how you stop a public storage bucket or an unreviewed change reaching production. Today the honest answer is code review and good intentions.
Policy as code turns that answer into a guardrail. This guide is for platform, DevOps and security engineering leads at UK B2B tech firms running Terraform and Kubernetes in the cloud. It sets out which rules to encode first, the ISO 27001 Annex A controls and SOC 2 criteria each one supports, how to roll rules out without breaking delivery, and how the engine's records become audit evidence.
Policy as code is writing security and compliance rules as version-controlled code that a policy engine evaluates automatically. The engine blocks or flags non-compliant changes in pipelines, provisioning or Kubernetes admission control before they reach production. Open Policy Agent, Kyverno and Sentinel are common engines. The value comes from encoding first the few rules that prevent the most audit findings.
HashiCorp's Sentinel documentation describes the idea well: once policies live in text files, you can apply version control, automated testing and automated deployment to them. A rule set by clicking in a console has none of that.
The Open Policy Agent documentation adds the design choice that makes it work. The engine makes the decision; something else acts on it. Your pipeline or cluster sends the change to the engine as data, asks a question, and acts on the answer.
Policy as code usually runs at three points:
For Kubernetes, the admission point matters most. Of the three, it is the only one that catches a change made outside the pipeline.
Infrastructure as code describes what you want to build: the networks, clusters and storage, written as Terraform or similar. Policy as code describes what you will not allow to be built, whatever the infrastructure code says. One is the blueprint; the other is the building regulation that every blueprint is checked against before work starts.
The two sit side by side. Infrastructure as code gives the policy engine something to read, because a Terraform plan is structured data. Without it, policy as code can still check Kubernetes objects and cloud API calls. It just loses the earliest point of control.
Policy as code is preventive: it stops a non-compliant change before it lands. Compliance as code is detective: it checks what is already running against your controls and gathers the evidence an auditor samples. You need both, but they answer different questions and usually have different owners, the platform team for the first and the GRC lead for the second.
This guide stays on the preventive side. Detective checks, continuous control monitoring and auditor evidence packs belong to compliance as code, which GRCMANA covers in its own guide.
The common policy as code tools are Open Policy Agent, Kyverno, HashiCorp Sentinel and the policy services built into each cloud platform. They differ in policy language, where they enforce and licence. Many teams run more than one, for example Kyverno in the cluster and a cloud-native service at the account level. The table compares them neutrally; none is ranked.
| Engine | Policy language | Where it enforces | Licence and governance |
|---|---|---|---|
| Open Policy Agent (OPA) | Rego | General purpose: Kubernetes (through the Gatekeeper project), CI/CD pipelines, Terraform plans, microservices and API gateways | Apache 2.0; CNCF graduated project since January 2021 |
| Kyverno | YAML and CEL (Common Expression Language) | Kubernetes admission controller, command-line scanner and runtime scans; can also check other JSON outside Kubernetes | Apache 2.0; CNCF graduated project since March 2026 |
| HashiCorp Sentinel | Sentinel language | Inside HashiCorp products, such as policy checks on Terraform plans in HCP Terraform | HashiCorp product, not open source |
| Cloud-native policy services | JSON definitions or constraints | The cloud account or organisation: Azure Policy, AWS service control policies, Google Cloud Organization Policy | Part of the cloud platform |
Two details from the vendors' own documentation are worth knowing. HCP Terraform accepts OPA policies as well as Sentinel, so a Terraform shop is not locked to one language. And AWS service control policies never grant rights. They set the most that IAM users and roles in your organisation can do.
The engine matters less than the rules you put in it.
Encode first the rules that stop the most serious mistakes and are easy to check with few false alarms: public exposure, network rules, privileged access, encryption, configuration baselines and change approval. Each supports controls an ISO 27001 auditor or SOC 2 examiner will test. The table maps them by control ID; the sections below explain each one.
| Priority | Rule to encode | Blocks | ISO 27001 Annex A | SOC 2 criteria |
|---|---|---|---|---|
| 1 | Public exposure | Public storage buckets, databases and admin endpoints | 5.15, 8.20 | CC6.1 |
| 2 | Network rules | Inbound access from anywhere to management and database ports | 8.20 | CC6.1 |
| 3 | Privileged access | Wildcard admin roles, cluster-admin bindings, privileged containers | 8.2 | CC6.1 |
| 4 | Encryption | Unencrypted storage, volumes and plain-text listeners | 8.24 | CC6.1 |
| 5 | Configuration baselines | Unapproved images, missing owner tags, settings outside the baseline | 8.9 | CC7.1 |
| 6 | Change approval | Changes applied outside the pipeline or without review | 8.32 | CC8.1 |
We recommend this order because the first rules are yes-or-no, and a miss does harm at once. A bucket is public or it is not. A baseline rule needs more judgement, so it comes later, once the team trusts the engine.
This rule refuses any storage, database or admin interface that anyone on the internet could reach, unless someone has signed off the choice. It supports access control (5.15) and network security (8.20) in Annex A and CC6.1 in SOC 2. Enforce it at the Terraform plan and again at the cloud account level.
An illustrative rule, in words rather than code: deny any storage bucket whose access setting is public, unless an approved exception names that bucket.
This rule refuses firewall or security group entries that open admin or database ports to any address. It supports network security (8.20) and CC6.1. Enforce it at the plan, and use a cloud-native service as a second line for changes made in the console.
This rule refuses roles with wildcard permissions, cluster-wide admin bindings for ordinary workloads, and containers that request privileged mode. It supports privileged access rights (8.2) and CC6.1. Enforce it in admission control for Kubernetes, and set an account-level ceiling with service control policies or their equivalent.
This rule refuses unencrypted storage, unencrypted volumes and listeners that accept plain-text traffic. It supports use of cryptography (8.24) and CC6.1. It is easy to write, because in most resources the setting is one field.
This rule checks resources against an approved baseline: allowed image registries, required owner and data-classification tags, and hardened settings. It supports configuration management (Annex A 8.9) and CC7.1. The CIS Benchmarks give a consensus-based starting point, so you do not invent the baseline from scratch.
This rule refuses changes that did not come through the reviewed pipeline, such as an apply run from a laptop or a deployment with no linked pull request. It supports change management (Annex A 8.32) and CC8.1. It comes last because it only works once the pipeline is the normal route for every change.
Start every new rule in warn or audit mode, then move it to enforce once existing violations are fixed or excepted. Most of the engines above support this. Gatekeeper denies by default but offers dry-run and warn actions; Kyverno has Audit and Enforce; Sentinel has advisory, soft-mandatory and hard-mandatory levels; Azure Policy has audit and deny effects.
The engines' own descriptions make the trade-off clear. Gatekeeper's documentation says its warn action gives the same benefits as dry run and also tells the user why the change would have been denied. Kyverno's documentation describes policy reports as a way to see a policy's impact without causing disruption.
A rollout that does not break delivery follows five steps:
Sentinel's soft-mandatory level is a useful middle step. The documentation says the policy must pass unless someone overrides it, and that the override provides non-repudiation, because someone has to override the failed policy explicitly.
A rule that only ever warns is a log entry, not a guardrail.
Handle exceptions as records, not workarounds. Each exception names the rule, the resource, the reason, an owner and an expiry date. It goes through the same review as a code change. Kyverno builds this in with its PolicyException resource; in other engines, an exceptions file in the policy repository does the same job.
Kyverno's documentation frames the need plainly: some teams must allow a change that would violate a valid rule on a one-time basis, where the risks are known and acceptable. That is a risk acceptance, and it deserves the same treatment as any other.
We recommend four properties for every exception:
An exception without an expiry date is a second, unwritten policy.
Policy decision logs become audit evidence because they record, for each change, which rule was evaluated, what was submitted and what the engine decided. Kept alongside the policy repository's history and the exception register, they show an auditor that a control exists, that it ran, and what happened when it failed.
OPA's decision logs are described in exactly these terms. Each event includes the policy that was queried, the input to the query and the bundle metadata, with other information that enables auditing of policy decisions. Kyverno writes its results to policy reports, which are Kubernetes resources, and Gatekeeper records audit violations on each constraint.
Together, these give you four kinds of evidence:
For an ISO 27001 auditor, that is evidence for change management and configuration management. For a SOC 2 examiner, it supports CC8.1 and the access criteria. The SOC 2 trust services criteria page explains how those criteria are organised; the SOC 2 hub covers the report itself.
We recommend keeping decision logs for at least the length of your audit period. An ISO 27001 surveillance audit or a SOC 2 Type 2 examination looks back over months, not days.
The policies need the same governance as any other control. Each rule has a named owner, sits in version control, passes automated tests before release, and is reviewed on a fixed cycle. Changes go through pull requests, so the rule set has its own change record. Nobody can quietly weaken a guardrail.
In practice, that means:
The written security policy still sets the intent. The code enforces part of it, and the review keeps the two in step.
The common mistakes with policy as code are writing hundreds of rules on day one, offering no exception path, leaving rules without owners, and running everything in audit mode forever. Each one turns the engine into noise that developers learn to ignore. Noise is a poor thing to show an auditor.
The fix for all four is the same. Encode fewer rules, own each one, and set a date for every rule to start enforcing.
No. Open Policy Agent is one engine, and policy as code is the practice. Kyverno, Sentinel and the cloud-native services are policy as code engines too. The practice is defined by rules in version control that a machine evaluates, not by any one language.
No. Terraform plan checks in the pipeline and cloud-native services such as Azure Policy or AWS service control policies work without a cluster. Kubernetes adds admission control as a further enforcement point, which matters most when people can change the cluster directly.
No. ISO 27001 still expects documented policies that management approves. Policy as code enforces the technical part of them in a way you can test and evidence. Keep a reference from each rule to the policy statement it enforces, so the two can be reviewed together.
It depends on how many violations audit mode finds in what you already run. Each one needs a fix or an exception before you enforce. We recommend planning by rule, not by date: a rule moves to enforce only when its audit report is clean or fully excepted.
Every guide in this domain, grouped by level — fundamentals, intermediate and advanced. Open any entry for its core practices, implementation steps and the evidence it produces.
Work with a Trust Architect to automate evidence collection and continuous control monitoring, so your next audit and your next security questionnaire draw on the same live controls.