Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCXVI · Governance Without BureaucracyControls

Structural versus procedural controls — the difference

Advanced⏱ ~24 mingit

What you'll learn

  • Distinguish structural controls (system-enforced) from procedural controls (human-enforced)
  • Recognise why structural controls scale with team size and procedural controls drift
  • Identify the class of loss each control type is appropriate for
  • Convert a procedural control into a structural control where the loss is mechanical

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A structural control is one the system enforces; the human cannot bypass it. A procedural control is one the human is asked to enforce; the human can bypass it. The choice between the two is the choice between a check that scales with team size and a check that drifts into ceremony. This lesson teaches the difference and the rule for converting one into the other.

Structural controls

A structural control changes the system so that the loss-prevented cannot happen by construction. The human does not need to remember; the human does not need to approve; the system refuses to advance until the condition is met.

Examples:

  • Branch protection that requires two approvals before a merge is possible. The merge button is disabled until the count is met; the engineer cannot bypass.
  • Required CI checks that fail the build if policy-as-code rejects the manifest. The pipeline exits non-zero; the deploy cannot proceed.
  • OIDC identity that the cloud provider trusts in preference to long-lived AWS access keys. There is no key to leak because there is no key.
  • GitOps controller that reconciles desired state in the cluster and refuses drift. The drift is reverted, not approved.
flowchart LR
    A["Change authored"] --> B["System check:\npolicy / signature / approval"]
    B -->|"refused"| STOP["Refused:\nmerge blocked,\nbuild red,\ncluster reverts"]
    B -->|"passed"| NEXT["Advance:\nmerge, deploy"]

The characteristic of a structural control is that the mechanism is enforced by the system itself. The loss is prevented not because a human remembered to check but because the system cannot proceed without the check passing. The control’s cost is paid in CI seconds, in tool complexity, and in the upfront design of the check; the control’s benefit is that it cannot be forgotten and cannot be bypassed.

Procedural controls

A procedural control asks a human to do something. The human can choose to comply, can choose to forget, and can choose to bypass with an emergency exception. The loss is prevented only insofar as every human chooses correctly every time.

Examples:

  • Manual review of a pull request by a peer. The reviewer can approve without reading; the reviewer can self-approve when alone.
  • Quarterly access review of who has IAM access to which resource. The reviewer can skim; the reviewer can approve a list that has not changed.
  • Change advisory board that meets weekly to approve changes scheduled for the next week. The CAB can approve a backlog it does not read.
  • Documented runbook that engineers are asked to follow during an incident. The engineer can improvise and skip the runbook under pressure.
flowchart LR
    A["Change authored"] --> H["Human:\nreview / approve / follow"]
    H -->|"complies"| NEXT["Advance"]
    H -->|"forgets"| L["Loss occurs"]
    H -->|"bypasses"| L["Loss occurs"]

The characteristic of a procedural control is that the loss-prevention depends on the human’s compliance. The control’s cost is paid in human time and attention; the control’s benefit is conditional on the human choosing correctly. The procedural control scales poorly with team size because each additional engineer is an additional opportunity to forget or bypass.

The conversion rule

The rule for choosing between the two is mechanical: if the loss is mechanical, the control should be structural. If the loss is judgement, the control must be procedural. The mistake is to leave a mechanical loss to a procedural control, or to encode a judgement call as a structural rule.

  • Mechanical losses are losses that a machine can detect: an unsigned commit, a missing label, a pinned-digest container reference, a public S3 bucket, an unencrypted secret. Convert these to structural controls.
  • Judgement losses are losses that only a human can evaluate: whether the change is the right design, whether the migration is safe, whether the runbook step is correct given the failure mode. Keep these as procedural controls.
# conftest: structural check on a Kubernetes manifest
# the policy decides; the engineer cannot
conftest test --policy policy/ manifests.yaml
# OPA: structural evaluation of an input
# the engine returns a verdict; the engineer cannot override
opa eval -d policy.rego -i input.json "data.policy.allow"
flowchart TB
    L["Loss"] --> Q{"Mechanical\nor judgement?"}
    Q -->|"mechanical"| S["Structural control:\nsystem-enforced"]
    Q -->|"judgement"| P["Procedural control:\nhuman-enforced"]
    S --> E["Enforced in CI / GitOps / IAM"]
    P --> R["Reviewed by humans"]

Production discipline

  1. Audit every control for its class. For each control, ask: is the loss mechanical or judgement? Is the control structural or procedural?
  2. Convert mechanical-loss-procedural-control pairs. An unsigned-commit check that is left to human review is the canonical conversion target.
  3. Leave judgement-loss as procedural. A design review cannot be encoded as a policy; trying to do so produces a structural rule that bypasses itself.
  4. Track the conversion as a metric. The number of mechanical-loss-procedural-control pairs in the pipeline is a leading indicator of bureaucracy.

Cross-course references

  • This course, Part CXVI-01 frames the control versus the bureaucracy; this lesson narrows the control into structural and procedural classes.
  • This course, Part CIII (RepositoryAntiPatterns) lists the anti-patterns whose fixes are structural controls (branch protection, CODEOWNERS).
  • Terraform for Production Sysadmins - Part IX (DeliveryPipeline) covers the same conversion in Terraform’s CI checks.

Quiz

Knowledge check · 4 questions

  1. Q1. A team requires engineers to manually verify that container images in their manifests are pulled from an approved registry. The team has had three incidents in two years where an image from an unapproved registry reached production because an engineer forgot. Which is the correct structural conversion?

  2. Q2. A design review that evaluates whether a proposed database migration is safe cannot be encoded as a structural policy rule and must remain a procedural control.

  3. Q3. Name the rule for converting a procedural control into a structural one, and the rule for keeping a control procedural.

  4. Q4. Classify each control as structural or procedural, identify any class mismatches, and recommend the conversion.

    Team T operates a Kubernetes platform with the following controls: (1) branch protection requiring two peer approvals before merge; (2) a PR template that asks the author to confirm 'I have not introduced secrets'; (3) a CI check that scans for committed secrets with a scanner; (4) a CODEOWNERS file that names the platform team for /clusters/; (5) a weekly CAB meeting that reviews the upcoming deploys; (6) a Kyverno policy that fails the manifest on privileged containers; (7) a quarterly manual audit of IAM roles against the team roster.

Passing score: 75%. Answers are checked in this browser.