---
title: "Policy as code: which security controls to encode first"
canonical: https://www.grcmana.io/vault/grc-engineering/policy-as-code
published: 2026-10-21
updated: 2026-10-21
author: "Harry West"
framework: "GRC Engineering"
control: "Guide Policy as code: which security controls to encode first"
journeyStage: advanced
---

# Policy as code: which security controls to encode first

## Key takeaways

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.

- **Decision logs** — Open Policy Agent decision log events include the policy queried, the input to the query and bundle metadata, which the OPA documentation says enables auditing of policy decisions.
- **Sentinel enforcement levels** — HashiCorp Sentinel has three enforcement levels: advisory (the default, allowed to fail with a warning), soft mandatory (must pass unless overridden) and hard mandatory (must pass).
- **Gatekeeper enforcement actions** — OPA Gatekeeper denies admission requests that violate a constraint by default; its dryrun and warn enforcement actions let teams test a constraint without enforcing it.
- **Kyverno policy exceptions** — A Kyverno PolicyException is a namespaced resource that lets named resources past a given policy and rule, for one-time deviations whose risks are known and acceptable.

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.

## What is policy as code?

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](https://www.openpolicyagent.org/docs/latest/) 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:

- **The CI/CD pipeline.** Rules check a Terraform plan or a Kubernetes manifest when a pull request is raised, so the developer sees the failure before merge.
- **Infrastructure provisioning.** Rules check the plan again at apply time, where the change is about to become real.
- **Kubernetes admission control.** Rules check every object the cluster is asked to create or change, including ones nobody put through the pipeline.

For Kubernetes, the admission point matters most. Of the three, it is the only one that catches a change made outside the pipeline.

## How does policy as code differ from infrastructure as code?

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.

## How does policy as code differ from compliance as code?

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.

## Which policy as code tools are there?

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.

## Which security controls should you encode first?

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.

### 1. Public exposure

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.

### 2. Network rules

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.

### 3. Privileged access

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.

### 4. Encryption

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.

### 5. Configuration baselines

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](https://www.grcmana.io/vault/iso-27001/annex-a-8-9-configuration-management)) and CC7.1. The [CIS Benchmarks](https://www.grcmana.io/vault/center-for-internet-security) give a consensus-based starting point, so you do not invent the baseline from scratch.

### 6. Change approval

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](https://www.grcmana.io/vault/iso-27001/annex-a-8-32-change-management)) and CC8.1. It comes last because it only works once the pipeline is the normal route for every change.

## Should a policy start in enforce mode or warn mode?

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](https://open-policy-agent.github.io/gatekeeper/website/docs/violations/) 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:

1. **Write and test the rule** against sample inputs, including known-good and known-bad cases.
2. **Run it in audit mode** against the existing estate, and list what it would have blocked.
3. **Fix or except** each existing violation, with an owner for each one.
4. **Switch to warn mode** in the pipeline, so developers see the message before it can stop them.
5. **Enforce on a published date,** and tell the teams it affects before that date arrives.

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.

## How do you handle exceptions and waivers?

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 owner** who can explain the exception to an auditor.
- **A reason** that links to the risk register entry or the ticket that approved it.
- **An expiry date,** after which the rule applies again automatically.
- **A narrow scope,** naming the resource rather than switching the rule off for a whole account.

An exception without an expiry date is a second, unwritten policy.

## How do policy decision logs become audit evidence?

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](https://www.openpolicyagent.org/docs/latest/management-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:

- **The rule itself,** with its version history, reviewer and approval date, showing design.
- **Decision logs,** showing the rule ran on real changes and what it blocked.
- **The exception register,** showing deviations were approved, owned and time-limited.
- **Audit-mode reports,** showing how existing resources stand against each rule.

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](https://www.grcmana.io/vault/soc-2/trust-services-criteria) page explains how those criteria are organised; the [SOC 2 hub](https://www.grcmana.io/vault/soc-2) 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.

## Who should govern the policies themselves?

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:

- **Ownership.** One owner per rule, recorded in the repository, usually in the platform or security team.
- **Testing.** Unit tests for each rule with passing and failing inputs; OPA and Kyverno both ship testing tools.
- **Versioning.** Tagged releases of the rule set, so you can say which version was in force on a given date.
- **Review.** A scheduled review, which we recommend at least annually, of whether each rule still matches the written policy and the risk it was meant to cover.

The written security policy still sets the intent. The code enforces part of it, and the review keeps the two in step.

## What are the common mistakes with policy as code?

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.

- **Hundreds of rules on day one.** Import a full rule library and you get a wall of findings nobody can sort. Start with the six above.
- **No exception path.** If the only way past a rule is to disable it, people will disable it.
- **Rules nobody owns.** Nobody reviews a rule without an owner, so it drifts from the risk it was written for.
- **Audit mode forever.** Reports pile up and nothing is blocked. The control exists on paper only.

The fix for all four is the same. Encode fewer rules, own each one, and set a date for every rule to start enforcing.

## Frequently asked questions

### Is policy as code the same as Open Policy Agent?

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.

### Do you need Kubernetes to use policy as code?

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.

### Does policy as code replace written security policies?

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.

### How long does it take to roll out a first set of rules?

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.
