Policy as code
On Airframe, the platform’s rules are code: a set of tested rules, the policy pack, checks every change to a product’s Terraform, pipeline file and declared Dockerfiles before it can merge, and checks the plan again before anything is applied. People approve what a change is for; the rules check how it is done. This page explains what the rules look at, what their two verdicts do, and the gaps that are still open.
What the policy pack is
Section titled “What the policy pack is”The policy pack is a set of rules written in Rego, the language of Open Policy Agent, kept in the control-plane repository. A tool called Conftest runs them against a change. Each rule looks at one thing and says whether it is allowed, for example:
- no resource may be granted to
allUsers, which would make it public; - no one may be given the broad
owneroreditorroles; - every labeled resource carries the platform’s full set of labels, which tie it to its product for cost and audit;
- every regional resource is in
us-central1, the platform’s one region; - a product’s pipeline file includes only the platform’s own pipeline components, and none of its own jobs deploys;
- a Dockerfile builds only from the platform’s base images, pinned.
Where it runs
Section titled “Where it runs”The same rules run in four places, so a contributor meets the same answer everywhere:
- On a laptop, through
airframe validate, with no credentials, before a merge request is opened. - In the merge request’s pipeline, on the Terraform code and the pipeline file, in a job the repository cannot remove. From release 2026.10.01-1, it also runs the Dockerfile rules on every merge request that changes a Dockerfile listed in
.airframe/images.yamland namedDockerfile,Dockerfile.*,*.Dockerfileor*.dockerfile. - In the executor’s runs, at a merge request’s approval.
- On the plan, on main, in the root’s
policy:<root>job before every apply: the apply needs that job to pass. The plan shows the real values that the code alone may leave unresolved, so this is where the rules see everything.
The cost rules are the one exception: they need prices, so they run in the executor and decide the “Airframe cost” status check. The gate and executor page explains it.
Validation a product cannot switch off
Section titled “Validation a product cannot switch off”A product’s pipeline file is the team’s to change, so the important checks do not live in it. GitLab pipeline execution policies, kept in the security-policies repository, inject jobs into every pipeline:
- the gate’s integrity check, into every pipeline in the GitLab group;
- the validation, including the policy pack, and the build and test evidence, into every pipeline of a product repository.
GitLab does not let a repository’s own pipeline file remove or replace an injected job.
Deny and warn
Section titled “Deny and warn”A rule gives one of two verdicts:
- Deny stops the change. The job fails, and GitLab merges a merge request only when its pipeline succeeds. On main, a denial on the plan stops the apply.
- Warn reports something to look at, such as a resource a product declares itself instead of using a golden-path module. Today a warning appears in the job’s log and holds nothing back. The design asks for the platform’s approval and an entry in an exception register for each warning; that part is not built yet.
A new deny rule usually spends its first release as a warning, so teams can fix what it would refuse before it starts refusing.
Messages written to be acted on
Section titled “Messages written to be acted on”A failing rule names the resource it judged and what is wrong with it, and many also say what to do instead. A storage bucket granted to allUsers is denied with the advice to expose it through the platform’s edge behind single sign-on. A job that declares the prod environment is told to deploy through the platform’s tf-apply component. The messages are written for whoever reads them next, a person or a coding agent, so the loop is short: change, validate, read, fix, validate again.
How a rule is tested
Section titled “How a rule is tested”Each rule should come with a case it must accept and a case it must refuse, and the control-plane pipeline’s policy:test job runs every test on each change to the pack. GitLab merges a change to the pack only when that job passes. That every new rule brings its own tests is a convention held in review, not something a job checks.
What runs where
Section titled “What runs where”airframe validate runs the validation that needs no credentials: Terraform’s formatting and validation, OpenTofu and tflint where they are installed, and the policy pack on the Terraform roots, the pipeline file and the declared Dockerfiles. It does not run the catalog’s checks, the gate, the application’s tests, the plan or its price; those need the platform and stay in the pipeline. It reads the pack from the control-plane copy it was installed from, not from the release the product uses.
Known gaps
Section titled “Known gaps”These are open today, and the platform’s own records list them:
- A warning holds nothing back, as above.
- A Dockerfile listed in
.airframe/images.yamlunder another name, such asContainerfile, is judged only when the merge request also changes another file the validation watches. - A resource with no
labelsattribute at all passes the label rule, which checks the labels a resource declares. airframe validateruns no module tests, API specification lint or API compatibility check, although the platform’s instructions name them.
Where it lives
In the control-plane repository:
- policy/airframe/: the rules and their tests
- docs/policy-pack.md: every rule, with its level
- templates/validate/: the injected validation
- cli/airframe-validate.sh: the local validation
In the security-policies repository:
