Blueprint 6draftv1.0.0

A review gate for infrastructure change

How to run an infrastructure plan, hold it, and apply exactly what was approved: the plan artifact as the unit of approval, an allow-list projection of the change, deterministic guardrails deciding eligibility, and an advisory reviewer that can only veto.

By
Theo Zourzouvillys
Published
Requirements
56 · ZBP-6-R1…R56
Tags
infraawsllmsecurityci
My Personal LLM Policy: Extract, not generate

The thinking is mine, whether it’s years old or from this week. What a model does is get it out of my head and onto the page — writing time I’d otherwise never spend, not substance I didn’t have. I read every line, I can defend any sentence, and the errors are mine: the same bar I hold everything here to, model or no model.

Pull this in

https://zrz.io/zbp/6-review-gate-for-infrastructure-change/v1.md

Version-pinned. Latest tracks revisions; the pinned URL does not. Requirements are cited individually as ZBP-6-Rk, so an implementation can annotate and a review can check them one at a time.

Use when
One party proposes an infrastructure change and another decides whether it applies, through a pipeline or an approval step, and applying something other than what was reviewed would be a serious incident.
Not for
  • Deciding what your infrastructure should contain; this governs how a proposed change is reviewed and applied, not what it declares.
  • Configuration management against running hosts, where there is no durable plan artifact to hold between decision and execution.
  • A single operator applying changes from a laptop, where the operator is the review gate and there is no second party.
Provides
infrastructure-review-gateplan-artifact-integritymachine-assisted-approval

TL;DR

This specifies how a proposed infrastructure change travels from “someone asked for it” to “it happened”, when the decision to apply belongs to somebody other than the person who proposed it. The shape of the answer: produce a durable plan artifact, treat that artifact as the unit of approval, apply it without re-deciding, and gate it behind deterministic rules that a language model may narrow but never widen.

Two properties carry the design, and both are structural rather than behavioural. The first is that the thing approved and the thing executed are the same bytes, which turns version pinning and staleness detection from hygiene into correctness. The second is ordering: deterministic rules decide what is eligible for automatic approval, and only then does an advisory reviewer get to object. A reviewer that could grant approval would be an instruction-following component holding the authority of the execution identity, reading text an attacker can write.

Applicability

Use this when a change to infrastructure is proposed through one path and applied through another — a pipeline, a change-approval step, a service that holds plans for a team — and where applying something other than what was reviewed would be a serious incident. It assumes a declarative provisioning tool that can emit a plan as a durable artifact and later execute that artifact rather than recomputing it. It is written for an estate where most proposed changes are unremarkable and a minority are dangerous, and where the cost of reviewing every one by hand is why changes queue.

Do not use this where the proposer and the approver are the same person with the same credentials at the same terminal: the gate then costs latency and buys nothing. Do not use it for imperative configuration management, where nothing corresponds to a plan artifact and the requirements below about applying what was approved cannot be met. Do not reach for the reviewer at all until the deterministic layer exists, because the reviewer is specified here only as a veto inside an envelope that layer defines, and without it there is no envelope.

Scope and non-goals

In scope: the lifecycle of a run from proposal to terminal state; the integrity of the plan artifact between decision and execution; the identity under which execution happens; the projection of a plan that may cross a trust boundary; the deterministic eligibility rules; the reviewer contract and its failure modes; approval, identity and separation of duties; and the audit record.

Out of scope, deliberately: what your infrastructure should declare, which is a design question this document has no opinion on; the provisioning tool’s own semantics, its state format, and how it computes a diff; the authoring of the policy a reviewer reads, which is workspace-specific prose; and the issuance of credentials to workloads — ZBP-2Blueprint · v1.0.0ZBP-2 — A Security Token Service for workload identityServices in your platform need to authenticate to each other and you want every call attributable to a named workload holding no long-lived secret.Why it's cited here: Named as one way to obtain the short-lived execution credentials this document requires, rather than as a dependency. Where that blueprint specifies how a workload obtains credentials, this one specifies only what execution may hold.Open ZBP-2 → specifies one such mechanism, but this document requires only that execution credentials be short-lived, however they are obtained.

Also out of scope: making the gate fast. Every requirement here is chosen for correctness under adversarial input first. Where a requirement costs latency, that is a stated price, not an oversight.

Vocabulary

Workspace — a named unit of managed infrastructure with exactly one state, one execution identity, and one policy. The unit of concurrency and of authorization.

Configuration — the declarative source for a workspace, captured as an immutable, content-addressed bundle at the moment a run is proposed.

Run — one traversal from proposal to a terminal outcome. A run is plan-only (speculative, never applicable) or applicable.

Plan artifact — the durable, machine-executable output of the plan phase, which the apply phase consumes verbatim. Distinct from any rendering of it.

Plan document — the machine-readable description of a plan, from which projections are derived. Not the plan artifact, and not executable.

Projection — a derived, allow-listed summary of a plan document, and the only representation of a change permitted to cross a trust boundary.

Guardrails — deterministic, offline rules over a plan document that decide whether a run is eligible for automatic approval.

Eligible — a property of a run, decided solely by guardrails, meaning automatic approval is permitted to be considered. Not itself an approval.

Reviewer — an advisory component that inspects a projection and returns a verdict. In practice a language model, but the requirements bind any implementation.

Verdict — a reviewer’s structured output: either approve or escalate, with findings.

Escalate — route to human approval. The outcome of every reviewer failure.

Control plane — the components that orchestrate runs, hold the audit record, and serve the API. Distinct from the execution environment and from the estate.

Estate — the infrastructure a workspace manages.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as in RFC 2119 and RFC 8174.

Architecture

Four boundaries do the work, and the requirements below are largely about what may cross each one.

    proposer                    CONTROL PLANE                        ESTATE
   (human, CI)  ──proposal──▶  ┌──────────────────┐
                               │ run record       │
                               │ orchestration    │  assume ident.  ┌──────────────┐
                               │ audit            │ ──────────────▶ │  EXECUTION   │
                               └────────┬─────────┘                 │  plan/apply  │──▶ estate
                                        │                           └──────┬───────┘
                    ┌───────────────────┼────────────────┐                 │
                    ▼                   ▼                ▼          plan artifact
              guardrails            reviewer        approval          + document
             (offline, det.)      (advisory)       (identity)               │
                    │                   ▲                                   │
                    │                   └────── projection ◀────────────────┘
                    └──── eligibility ──────────▶ (allow-listed)


                                              external surfaces

The control plane never holds the estate’s authority. It can start an execution under a workspace’s identity, and it can record what happened; it cannot change infrastructure itself. This is the boundary that makes a compromised orchestrator survivable, and it is the reason the delegation permission is constrained as tightly as the identities it delegates to.

The plan artifact never leaves the trust boundary, and the projection is the only thing that does. A plan document is dense with values an operator would not paste into a chat window; the projection exists so that no component outside the boundary — a reviewer at a third-party inference provider, a notification surface, a browser — ever receives more than an enumerated set of fields.

Guardrails run before the reviewer and independently of it. Their output is an input to the reviewer’s invocation, never the reverse. Drawn as a pipeline, the reviewer sits downstream of a gate it cannot reach back through.

Normative requirements

Workspaces, runs, and configuration

  • ZBP-6-R1 A workspace MUST have a stable identifier that is not derived from mutable human-facing naming, and MUST map to exactly one state and one execution identity. Two workspaces MUST NOT share a state.
  • ZBP-6-R2 A run MUST have an enumerated status drawn from a closed set, and MUST reach exactly one terminal status. An implementation MUST NOT represent a run’s condition as the absence of a record or the presence of a lock alone.
  • ZBP-6-R3 At most one applicable run per workspace MUST be capable of reaching the apply phase at a time. This MUST be enforced by a mutual-exclusion mechanism below the API layer, and MUST NOT rely on the client refraining from concurrent proposals.
  • ZBP-6-R4 The configuration for a run MUST be captured as an immutable bundle, addressed by a cryptographic digest of its content, before the plan phase begins. The plan and apply phases of a run MUST use the same bundle.
  • ZBP-6-R5 A run record MUST carry the configuration digest, the provisioning tool version, the digest of the dependency lock, the execution identity used, and the identity that proposed it.
  • ZBP-6-R6 A run record MUST be append-only once the run reaches a terminal status. Correcting an error MUST produce a new record referencing the old, and MUST NOT rewrite it.
  • ZBP-6-R7 A plan-only run MUST NOT be capable of transitioning to apply under any input. This MUST be a property of the run, fixed at proposal, and MUST NOT depend on an approver declining to press a control.

The plan artifact

  • ZBP-6-R8 The plan phase MUST produce a durable plan artifact that the provisioning tool can execute without recomputing the change.
  • ZBP-6-R9 The apply phase MUST execute the plan artifact produced by that run’s plan phase, and MUST NOT compute a new plan. An implementation that re-plans at apply time does not conform, however similar the result usually is.
  • ZBP-6-R10 The provisioning tool version MUST be pinned per workspace, recorded on the run, and identical across that run’s plan and apply phases. A version mismatch MUST fail the apply.
  • ZBP-6-R11 The set of provider or plugin versions MUST be pinned by a dependency lock carried in the configuration bundle, and MUST resolve identically in both phases. An implementation MUST NOT permit resolution to a newer version between plan and apply.
  • ZBP-6-R12 The identity of the state as read during the plan phase — its lineage and its monotonic version counter — MUST be recorded on the run, and MUST be re-verified immediately before apply. A mismatch MUST fail the apply, and MUST NOT be reconciled, retried against fresh state, or reported as a conflict the approver may override.
  • ZBP-6-R13 A plan artifact MUST have an expiry, and applying an expired artifact MUST fail. Expiry MUST be enforced by the control plane rather than by a client declining to submit.
  • ZBP-6-R14 A plan that contains no changes MUST short-circuit: the run MUST reach a successful terminal status without evaluating guardrails, invoking a reviewer, or requesting approval.
  • ZBP-6-R15 A plan artifact MUST be treated as secret-bearing. It MUST be encrypted at rest and access-controlled no less strictly than the state it was computed against. An implementation MUST NOT treat it as a build artifact, publish it to a general artifact store, or attach it to a notification.

Execution identity and isolation

  • ZBP-6-R16 Execution MUST occur under an identity belonging to the workspace, distinct from the control plane’s own identity.
  • ZBP-6-R17 The control plane’s identity MUST NOT be sufficient to modify any estate it orchestrates. Its authority over a workspace MUST be limited to starting an execution under that workspace’s identity and recording the outcome.
  • ZBP-6-R18 The plan phase SHOULD execute under an identity that cannot mutate the estate, distinct from the identity used for apply.
  • ZBP-6-R19 No phase of a run MAY use a long-lived credential. Execution identity MUST be obtained as a short-lived credential at run time.
  • ZBP-6-R20 Where the control plane delegates execution identity, its permission to do so MUST be constrained to an explicitly enumerated set of execution identities. An unconstrained delegation permission MUST be treated as equivalent to holding every identity it can delegate.
  • ZBP-6-R21 A workspace MUST hold references to secrets rather than secret values. Dereferencing MUST occur inside the execution boundary, under the workspace’s execution identity, and MUST NOT occur in the control plane.
  • ZBP-6-R22 Secret values MUST NOT be written to the run record, the audit record, or any notification. Where execution logs are retained, values known to be secret MUST be masked before retention.

The change projection

  • ZBP-6-R23 A projection MUST be constructed by explicitly enumerating the fields it includes. Fields not enumerated MUST NOT be read into it.
  • ZBP-6-R24 An implementation MUST NOT construct a projection by removing known-sensitive fields from a plan document. Redaction by removal does not conform, because it fails open on any field the removal list does not anticipate, including fields added by a later version of the provisioning tool.
  • ZBP-6-R25 Where a plan document provides sensitivity masks shadowing its before and after values, those masks MUST be applied to every value the projection includes. An implementation MUST handle both mask shapes: a scalar denoting that nothing is sensitive, and a structure mirroring the value with sensitive leaves marked.
  • ZBP-6-R26 A value that the plan reports as not yet known MUST be represented in the projection as unknown, and MUST NOT be rendered as absent, null, or empty. The distinction between “will be computed” and “will be cleared” is material to review.
  • ZBP-6-R27 A projection MUST have a size budget. A plan whose projection would exceed it MUST be ineligible for automatic approval. An implementation MUST NOT truncate a projection and proceed.
  • ZBP-6-R28 The projection requirements MUST hold identically for every consumer, regardless of whether that consumer executes inside the same trust boundary as the control plane. An implementation MUST NOT define a more permissive projection for a reviewer on the grounds of its location.
  • ZBP-6-R29 External surfaces — notifications, chat, browsers, third-party inference providers — MUST receive the projection and MUST NOT receive the plan document or the plan artifact.

Deterministic guardrails

  • ZBP-6-R30 Guardrails MUST be evaluated before a reviewer is invoked, MUST be deterministic functions of the plan document and the workspace policy, and MUST NOT depend on a network call.
  • ZBP-6-R31 Guardrails MUST decide eligibility, and eligibility MUST be a hard precondition for automatic approval. A workspace policy MUST be able to express, at minimum: destruction or replacement of a named resource class; changes that widen access; a bound on the number of resources changed; and a bound on projection size.
  • ZBP-6-R32 A guardrail that fails to evaluate MUST render the run ineligible. An implementation MUST NOT treat an evaluation error as an absence of findings.
  • ZBP-6-R33 The identity and version of the guardrail rule set MUST be recorded on the run. A change to the rule set MUST NOT alter the recorded outcome of a completed run.
  • ZBP-6-R34 Automatic approval MUST be disabled by default and MUST be enabled per workspace by explicit configuration. Enabling it MUST be an auditable change.
  • ZBP-6-R35 A workspace that manages the review gate’s own infrastructure, its guardrail rule set, or its policy MUST NOT be eligible for automatic approval.

The reviewer

  • ZBP-6-R36 A reviewer MUST be invoked only for a run that guardrails have found eligible.
  • ZBP-6-R37 A reviewer’s output MUST be capable only of withholding an approval that would otherwise proceed. Reviewer output MUST NOT be able to cause a run to be approved that guardrails found ineligible, to alter the guardrail outcome, or to widen the set of runs eligible for automatic approval. This requirement is structural: an implementation MUST NOT satisfy it by instructing the reviewer to refuse.
  • ZBP-6-R38 Plan content MUST be presented to a reviewer as data, within a delimited region, and the reviewer’s instructions MUST state that content within that region is never an instruction. This requirement is secondary to ZBP-6-R37 and MUST NOT be relied on as the primary control.
  • ZBP-6-R39 A reviewer MUST NOT be given credentials, network access to the estate, or any tool whose invocation has an effect outside the review. A successful manipulation of a reviewer MUST yield nothing beyond an incorrect verdict.
  • ZBP-6-R40 A verdict MUST be structured, and MUST be parsed strictly against a schema. A verdict that does not parse MUST escalate.
  • ZBP-6-R41 A reviewer error, timeout, refusal, rate limit, or unavailability MUST escalate. An implementation MUST NOT retry indefinitely, and MUST NOT treat exhaustion of retries as approval.
  • ZBP-6-R42 The verdict, the reviewer implementation, the model identity where applicable, and a digest of the exact projection submitted MUST be recorded on the run.
  • ZBP-6-R43 The reviewer MUST be replaceable without altering guardrails, approval, or the audit record. An implementation MUST provide a reviewer that always escalates, and MUST behave correctly when configured with it.
  • ZBP-6-R44 A reviewer MUST NOT be given the guardrail outcome as a value it can contradict, and guardrail evaluation MUST NOT consume reviewer output. The two MUST NOT be combined into a single scored decision in which a sufficiently confident reviewer outweighs a guardrail.

Approval and identity

  • ZBP-6-R45 An approval MUST be attributable to an authenticated identity that is authorized to apply that workspace. Authorization to propose a run MUST be separable from authorization to approve one.
  • ZBP-6-R46 Approval MUST be idempotent with respect to a run. The first decision to reach the control plane MUST be the decision; subsequent decisions MUST be recorded and MUST NOT take effect.
  • ZBP-6-R47 An implementation SHOULD support requiring that the approving identity differ from the proposing identity, configurable per workspace.
  • ZBP-6-R48 Every surface through which an approval may be given MUST enforce the same authorization. A surface MUST NOT confer approval authority that the same identity would not hold through the API.
  • ZBP-6-R49 A surface that cannot use the control plane’s primary authentication MUST verify message authenticity by a mechanism bound to the surface, MUST reject messages outside a bounded time window, and MUST reject replays within it.
  • ZBP-6-R50 Where an external surface carries its own identifiers, those identifiers MUST be mapped to an approver identity by control-plane configuration. An identifier presented in a payload MUST NOT be trusted as an identity.
  • ZBP-6-R51 An approval MUST fail if the run’s plan artifact has expired, the run is not awaiting approval, or the run has already reached a terminal status.

Audit and notification

  • ZBP-6-R52 Every run MUST leave a durable record of the proposing identity, the configuration digest, the guardrail outcome and rule-set version, the verdict and reviewer identity where one ran, the approving identity or the fact of automatic approval, and the terminal outcome.
  • ZBP-6-R53 Execution logs MUST be retained beyond the lifetime of the execution environment, and MUST be retrievable for a completed run.
  • ZBP-6-R54 A notification MUST carry only the projection and identifiers. It MUST NOT carry the plan document, the plan artifact, or an unprojected rendering of the change.
  • ZBP-6-R55 A scheduled or drift-detection plan MUST be an ordinary plan-only run subject to the same guardrails, projection, and audit requirements.
  • ZBP-6-R56 The gate MUST NOT be the only path by which infrastructure can be changed in an emergency. A break-glass path MUST exist that does not depend on the control plane, the reviewer, or their dependencies, and its use MUST be auditable after the fact.

Wire formats and interfaces

The projection

A projection is a JSON document. Its shape is fixed so that a consumer — a reviewer prompt, a notification renderer, a conformance test — reads one format regardless of the provisioning tool underneath. Every value appears as a value representation, exactly one of three forms:

{"known": "t3.micro"}
{"unknown": true}
{"sensitive": true}

The three are disjoint on purpose. Collapsing “sensitive” into “unknown” loses the distinction between a value that will be computed and a value that exists but may not be shown, and an approver reads those differently.

{
  "format": 1,
  "counts": { "create": 1, "update": 0, "delete": 0, "replace": 0 },
  "resources": [
    {
      "address": "random_pet.name",
      "type": "random_pet",
      "actions": ["create"],
      "attributes": {
        "prefix":    { "after": {"known": "team-platform"} },
        "id":        { "after": {"unknown": true} }
      }
    }
  ],
  "outputs": [
    { "name": "plain_out", "actions": ["create"], "after": {"known": "not-a-secret"} },
    { "name": "secret_out", "actions": ["create"], "after": {"sensitive": true} }
  ]
}

The source allow-list

A conforming projection reads these paths of a plan document and no others. The list is the interface; extending it is a specification change, not an implementation detail.

Source pathUse
resource_changes[].addressresource identity
resource_changes[].typeresource class, for guardrail and reviewer context
resource_changes[].change.actionsthe action set
resource_changes[].change.before / .afterattribute values, before masking
resource_changes[].change.before_sensitive / .after_sensitivesensitivity masks
resource_changes[].change.after_unknownunknown-value mask
output_changes[].actions, .after, .after_sensitive, .after_unknownoutput changes

Everything else in a plan document — the variables supplied to the run, the configuration, the planned values, the prior state — is never read. That is the whole mechanism: the sensitive paths are not removed, they are never visited.

The verdict

{
  "decision": "approve",
  "reasoning": "Tag-only change on two resources; no replacement, no access change.",
  "findings": [
    { "severity": "info", "address": "random_pet.name", "note": "prefix changed" }
  ]
}

decision is approve or escalate and nothing else. A reviewer has no vocabulary for “reject”, because rejection and escalation are the same action — a human decides — and offering both invites an implementation to treat one as final.

Algorithms

Constructing a projection

Given a plan document D, produce projection P, reading only the allow-listed paths.

  1. Initialise P with format, zeroed counts, and empty resources and outputs.
  2. For each entry c in D.resource_changes:
    1. Take address, type, actions. If actions is ["no-op"], skip the entry.
    2. Increment the count for the action: a ["delete","create"] or ["create","delete"] action pair counts as replace, not as one of each.
    3. For each attribute key k present in c.change.after or c.change.after_unknown:
      1. If mask(c.change.after_sensitive, k) is true, emit {"sensitive": true}.
      2. Else if mask(c.change.after_unknown, k) is true, emit {"unknown": true}.
      3. Else emit {"known": c.change.after[k]}.
    4. Apply the same three steps to before using before_sensitive, where a before value exists.
  3. For each entry in D.output_changes, apply step 2.3 to its after.
  4. If the serialized size of P exceeds the budget, return over-budget. Do not truncate.

mask(m, k) is defined for both mask shapes: if m is false or absent, the result is false; if m is true, the result is true; if m is an object, the result is mask(m[k], …) recursively, and absent keys are false. A mask that is true at a branch marks the whole subtree.

Failure outcomes: over-budget renders the run ineligible under ZBP-6-R27. A plan document that cannot be parsed renders the run ineligible under ZBP-6-R32, and MUST NOT be reported as an empty change set — an unparseable plan and a plan with no changes are opposite conclusions.

Deciding a run

  1. Execute the plan phase; capture the plan artifact, the plan document, and the state identity.
  2. If the plan reports no changes, terminate successfully (ZBP-6-R14).
  3. Construct the projection. On over-budget, mark ineligible.
  4. Evaluate guardrails against the plan document. Any veto marks ineligible. An evaluation error marks ineligible (ZBP-6-R32).
  5. If the workspace does not permit automatic approval, or the run is ineligible, request human approval and wait.
  6. Otherwise invoke the reviewer with the projection and the workspace policy. On approve, proceed. On escalate, on a malformed verdict, or on any error, request human approval and wait.
  7. On approval — human or automatic — re-verify the state identity and the artifact’s expiry. On any mismatch, fail the run (ZBP-6-R12, ZBP-6-R51).
  8. Execute the plan artifact. Record the terminal outcome.

Step 7 is deliberately after approval rather than before. Checking freshness at the moment of decision proves nothing about the moment of execution, and the gap between them is exactly where a competing apply lands.

Reviewer bindings

The reviewer contract is a single call: projection and policy in, verdict out. Everything that varies between providers — transport, authentication, how a structured response is obtained — sits below that line, which is why ZBP-6-R43 requires the reviewer to be replaceable without touching guardrails or approval.

  • In-account inference. The execution identity authenticates directly; no key is stored, which satisfies ZBP-6-R19 without additional machinery. The projection remains subject to ZBP-6-R28: an in-account reviewer receives exactly what a third-party one would.
  • A first-party model API. Requires a credential held as a reference and dereferenced under ZBP-6-R21. Structured output is obtained by a schema-constrained response or a tool call, and validated again on receipt regardless — ZBP-6-R40 is a requirement on the consumer, not a statement of trust in the provider.
  • An aggregating gateway. Adds a party to the trust boundary. Conformance is unchanged, but the boundary the projection defends is now wider, and the workspace policy SHOULD say so.
  • A local model. No external boundary is crossed at all, which does not relax any requirement, because ZBP-6-R37 exists to contain a compromised reviewer rather than an eavesdropped one.
  • The always-escalate reviewer. Required by ZBP-6-R43. It is the correct configuration for a workspace whose changes are all dangerous, and it is the control against which the rest are tested.

A binding MUST NOT introduce a fourth verdict, a confidence score consumed as a threshold, or a retry loop that terminates in approval. Each is a way of reintroducing the widening this specification forbids.

AWS binding

The requirements above are cloud-neutral. This is the concrete mapping, and three limits that are properties of the platform rather than of the design.

ConcernServiceNotes
Run lifecycleStep Functions, Standard classOne execution per run, one state per phase. Executions run up to a year, so the wait between a planned and a decided run needs no durable-timer machinery of its own.
Approval gateA .waitForTaskToken stateApproval becomes an inbound event that resumes the execution, rather than a status column swept by a schedule. TimeoutSeconds on that state is ZBP-6-R13.
ExecutionECS Fargate via ecs:runTask.syncOne task definition; overrides.taskRoleArn supplies the workspace identity per invocation, so ZBP-6-R16 needs no task definition per workspace. 20 GB of ephemeral storage by default covers provider downloads.
Run records, lockingDynamoDB, one item per runA conditional write on a per-workspace item is the mutual exclusion of ZBP-6-R3.
Configuration, artifactsS3Configuration keyed by content digest and written by the proposer through a pre-signed PUT, satisfying ZBP-6-R4 without granting bucket-wide access. Artifacts encrypted under the state’s key policy, per ZBP-6-R15.
LogsCloudWatch Logs via the awslogs driverTailed live by filtering on the run id; copied to S3 on completion for ZBP-6-R53.
StateS3 with conditional-write lockinguse_lockfile = true from OpenTofu 1.10 onward removes the separate lock table entirely.
SecretsSSM Parameter Store, by referenceResolved inside the task under the workspace identity, never by the orchestrator (ZBP-6-R21).
APIAPI Gateway HTTP APIAWS_IAM on the primary routes; a separate route with NONE and its own signature verification for surfaces that cannot sign requests (ZBP-6-R49).

Three platform limits

A state payload is capped at 256 KB. Exceeding it fails the execution with States.DataLimitExceeded, and the cap covers input and output together. This is why every artifact in this design travels by reference: the plan artifact, the plan document and the log are S3 keys on the execution’s state, never values in it. It also gives the projection budget of ZBP-6-R27 a concrete ceiling rather than an arbitrary one — a projection is the one derived object that may plausibly be carried inline, and the moment it cannot be, the run is over-budget by definition. The same 256 KB applies to the output of SendTaskSuccess, so an approval decision carries an identifier and a verdict, not a rendering.

ecs:RunTask together with iam:PassRole is a privilege-escalation path. Overriding the task role per run is what makes one task definition serve every workspace, and it is also what makes the orchestrator’s iam:PassRole the most dangerous permission in the system. Scoped to *, the orchestrator can pass itself any role in the account, and ZBP-6-R17’s isolation is decorative — the shape CWE-269CWE-269 — Improper Privilege ManagementThe weakness class covering a component that can grant itself, or pass on, more privilege than it should hold. The relevant shape here is a control plane permitted to delegate an unbounded set of execution identities, which makes the isolation boundary between control plane and estate decorative.cwe.mitre.org ↗ describes. Constrain it to an explicit resource list or a path prefix covering exactly the workspace execution roles, and write the trust policy of each of those roles to name the ECS tasks service principal and nothing else. This is the single control on which the threat model’s “compromised control plane” analysis depends.

A task token outlives nothing and expires from nothing. Tokens are at most 2048 characters and carry no intrinsic lifetime; an approval state without TimeoutSeconds waits until the execution’s one-year ceiling, holding the workspace lock and billing for the privilege. The expiry in ZBP-6-R13 is therefore not only a safety property but the only thing preventing a forgotten run from blocking its workspace indefinitely.

Backend injection

The workspace’s source need not declare a backend at all. Writing an override file into the working directory before init{"terraform":[{"backend":[{"s3":[{}]}]}]} as _override.tf.json — and supplying bucket, key and locking through -backend-config initializes cleanly, and also overrides a backend the source does declare. Verified against OpenTofu 1.11.6.

The property this buys is worth more than the tidiness: source carrying no marker tying it to the gate remains executable by hand by any operator holding the execution identity, which is what makes ZBP-6-R56’s break-glass path nearly free. A design that requires a proprietary block in the source — the shape hosted offerings generally take — makes the escape hatch a migration.

Threat model and failure modes

A reviewer manipulated through plan content. The plan document contains strings an attacker can often write: resource names, tags, descriptions, commit messages surfaced as metadata. Instruction-level defences against this are probabilistic. The design’s answer is that a manipulated reviewer can only produce a verdict, and the only verdict that changes an outcome is one that withholds approval from a run guardrails already found eligible. The worst achievable result is an unnecessary escalation.

A compromised control plane. It can start executions, so it can cause a previously approved artifact to be applied, and it can lie in the audit record. It cannot construct a new change, because it holds no estate authority (ZBP-6-R17) and cannot forge an artifact the tool will execute. This degrades to “the attacker can replay a change you approved”, which is why ZBP-6-R13 bounds artifact lifetime. If ZBP-6-R20 is violated, this analysis collapses entirely and the control plane becomes equivalent to every identity it may delegate.

A racing apply. Two runs plan against the same state; one applies; the second’s artifact is now computed against a state that no longer exists. Detection is ZBP-6-R12, and the check must happen after approval (step 7) because a pre-approval check leaves the whole approval window uncovered.

A stale or substituted artifact. Version drift between plan and apply produces an artifact the tool will refuse or, worse, interpret differently. ZBP-6-R10 and ZBP-6-R11 close this; the failure mode they prevent is subtle because most version differences are harmless most of the time.

Leakage through the projection. The dominant failure is a projection built by removal. In a representative plan document, the value of one variable marked sensitive appears in five distinct locations, of which one carries a sensitivity flag; the other four are in sections a removal-based implementation has no reason to visit. ZBP-6-R23 and ZBP-6-R24 exist because of this specific shape.

The gate as an incident dependency. A review gate sits directly in the path of the fix during an incident, and its own dependencies — the workflow engine, the reviewer’s provider, the notification surface — are things that can be down. ZBP-6-R56 requires a path around it.

Approval replay. An approval message captured from an external surface and resubmitted. ZBP-6-R49 bounds the window and rejects replays; ZBP-6-R46 makes a second approval inert even if one succeeds.

Anti-patterns

Re-planning at apply. The most common shape, because it is what running the tool twice does naturally, and because the diff is usually identical. It means no one ever approved what executed.

Treating the reviewer as an approver. Expressed as “the model approves low-risk changes”. It inverts the ordering, and makes every string in the plan an input to an authorization decision.

A confidence threshold. A reviewer returning a score, with approval above a cutoff. This is widening with extra steps: raising the score is exactly what a manipulation attempts.

Redaction by removal. Enumerating the sensitive paths and deleting them. It fails open on the first field the enumeration missed, and provisioning tools add fields.

Truncating an oversized projection. The plans too large to summarize are disproportionately the plans that most need review.

Auto-approving the gate’s own workspace. A change to the guardrail rule set that the guardrail rule set evaluates. ZBP-6-R35 forbids it.

One identity for plan and apply. Convenient, and it means a read-only operation is performed with authority to destroy the estate.

Reporting an unparseable plan as no changes. An empty change set and a failure to parse are opposite conclusions, and conflating them makes a broken plan look like a clean one.

Test vectors

Vectors are given against a plan document in the format documented for a widely used provisioning tool. V1 and V2 are taken from a plan actually executed against OpenTofu 1.11.6, of a configuration declaring one variable marked sensitive with the value hunter2-super-secret, one resource with a sensitive computed attribute, one ordinary resource, and two outputs of which one is sensitive. V3V14 are constructed inputs covering mask shapes and decision paths the executed plan does not exhibit. All fourteen were verified against a reference implementation of the algorithms above.

V1 — the five locations. In that plan document, the string hunter2-super-secret occurs at:

.variables.db_password.value
.configuration.root_module.variables.db_password.default
.planned_values.outputs.secret_out.value
.prior_state.values.outputs.secret_out.value
.output_changes.secret_out.after

Only the last carries a sensitivity flag (output_changes.secret_out.after_sensitive: true). Expected: a conforming projection of this document contains the string zero times.

V2 — sensitive computed attribute. Input change fragment:

{ "address": "random_password.pw", "type": "random_password",
  "change": { "actions": ["create"], "before": null,
    "after": { "length": 16, "special": true },
    "after_unknown": { "id": true, "result": true, "bcrypt_hash": true },
    "before_sensitive": false,
    "after_sensitive": { "result": true, "bcrypt_hash": true } } }

Expected: result and bcrypt_hash project as {"sensitive": true} — sensitivity is checked before unknownness, so a value that is both is reported as sensitive. id projects as {"unknown": true}. length projects as {"known": 16}.

V3 — scalar mask. "before_sensitive": false with a populated before. Expected: every before value projects as {"known": …}; the scalar false is handled without attempting a key lookup.

V4 — nested mask. "after_sensitive": {"config": {"password": true}} against "after": {"config": {"user": "svc", "password": "s3cr3t"}}. Expected: config.user projects known; config.password projects {"sensitive": true}.

V5 — subtree mask. "after_sensitive": {"config": true} against the same after. Expected: the whole config subtree projects as {"sensitive": true}; no leaf beneath it is emitted.

V6 — replacement counting. "actions": ["delete", "create"]. Expected: counts.replace is 1; counts.delete and counts.create are 0.

V7 — no-op exclusion. "actions": ["no-op"]. Expected: the entry contributes no resource and no count.

V8 — eligibility, protected class. A workspace policy protecting random_password, against a change with "actions": ["delete","create"] on random_password.pw. Expected: ineligible, reason protected-resource-replaced.

V9 — eligibility, count bound. A policy bounding changed resources at 10, against a plan with 11 non-no-op changes. Expected: ineligible, reason change-count-exceeded.

V10 — eligibility, clean. A policy as in V8 and V9, against a single update on random_pet.name touching only prefix. Expected: eligible.

V11 — over-budget. A projection serializing above the configured budget. Expected: ineligible, reason projection-over-budget, and no truncated projection is produced.

V12 — unparseable plan. A plan document that is not valid JSON. Expected: ineligible, reason plan-unparseable. Distinguishable from V13.

V13 — no changes. resource_changes present and empty. Expected: the run terminates successfully without guardrail evaluation, per ZBP-6-R14.

V14 — malformed verdict. A reviewer returning {"decision": "reject"}. Expected: escalate; reject is not in the verdict vocabulary and MUST NOT be interpreted as either outcome.

Conformance checklist

Runs and configuration

  • Workspace identifiers are stable and map to exactly one state (ZBP-6-R1)
  • Run status is drawn from a closed set with exactly one terminal value (ZBP-6-R2)
  • Concurrency is enforced below the API, not by client discipline (ZBP-6-R3)
  • Configuration is immutable and content-addressed before planning (ZBP-6-R4)
  • The run record carries config digest, tool version, lock digest, and both identities (ZBP-6-R5)
  • Terminal run records are append-only (ZBP-6-R6)
  • A plan-only run cannot transition to apply under any input (ZBP-6-R7)

The plan artifact

  • The plan phase produces a durable executable artifact (ZBP-6-R8)
  • Apply executes that artifact and never re-plans (ZBP-6-R9)
  • Tool version is pinned, recorded, and identical across phases (ZBP-6-R10)
  • Provider versions resolve identically in both phases (ZBP-6-R11)
  • State identity is captured at plan and re-verified before apply; mismatch fails (ZBP-6-R12)
  • Artifacts expire, and expiry is enforced server-side (ZBP-6-R13)
  • A zero-change plan short-circuits to success (ZBP-6-R14)
  • Artifacts are encrypted and access-controlled as state is (ZBP-6-R15)

Execution identity

  • Execution uses a workspace identity distinct from the control plane’s (ZBP-6-R16)
  • The control plane cannot modify any estate it orchestrates (ZBP-6-R17)
  • No long-lived credential is used by any phase (ZBP-6-R19)
  • Delegation permission is constrained to an enumerated set (ZBP-6-R20)
  • Secrets are held by reference and dereferenced inside execution (ZBP-6-R21)
  • Secret values never reach run records, audit, or notifications (ZBP-6-R22)

Projection

  • The projection is built from an enumerated allow-list (ZBP-6-R23)
  • No removal-based redaction is used anywhere (ZBP-6-R24)
  • Both mask shapes are handled (ZBP-6-R25)
  • Unknown values are represented as unknown, not absent (ZBP-6-R26)
  • Over-budget projections make a run ineligible and are never truncated (ZBP-6-R27)
  • The projection is identical for in-boundary and out-of-boundary consumers (ZBP-6-R28)
  • External surfaces receive only the projection (ZBP-6-R29)

Guardrails

  • Guardrails run before the reviewer, deterministically and offline (ZBP-6-R30)
  • Policy expresses destruction, access widening, count bounds, and size bounds (ZBP-6-R31)
  • A guardrail evaluation error renders the run ineligible (ZBP-6-R32)
  • Rule-set version is recorded and completed runs are not retroactively altered (ZBP-6-R33)
  • Automatic approval is off by default and enabled per workspace auditably (ZBP-6-R34)
  • The gate’s own workspaces are never eligible (ZBP-6-R35)

Reviewer

  • The reviewer runs only on eligible runs (ZBP-6-R36)
  • No reviewer output can widen eligibility, structurally (ZBP-6-R37)
  • Plan content is delimited and declared non-instructional (ZBP-6-R38)
  • The reviewer holds no credentials and no side-effecting tools (ZBP-6-R39)
  • Verdicts are strictly parsed; unparseable escalates (ZBP-6-R40)
  • Every reviewer failure mode escalates (ZBP-6-R41)
  • Verdict, reviewer, model identity, and projection digest are recorded (ZBP-6-R42)
  • An always-escalate reviewer exists and the system works with it (ZBP-6-R43)
  • Guardrails and reviewer are not merged into one scored decision (ZBP-6-R44)

Approval

  • Approval is attributable and separately authorized from proposal (ZBP-6-R45)
  • Approval is idempotent; the first decision wins (ZBP-6-R46)
  • Every surface enforces identical authorization (ZBP-6-R48)
  • Non-primary surfaces verify authenticity, bound time, and reject replays (ZBP-6-R49)
  • External identifiers map to identities by configuration, not by payload (ZBP-6-R50)
  • Approval fails on expired, non-pending, or terminal runs (ZBP-6-R51)

Audit

  • Every run leaves the full durable record (ZBP-6-R52)
  • Logs outlive the execution environment (ZBP-6-R53)
  • Notifications carry only the projection and identifiers (ZBP-6-R54)
  • Scheduled and drift plans are ordinary plan-only runs (ZBP-6-R55)
  • A break-glass path exists that does not depend on the gate (ZBP-6-R56)

Rationale

The rationale for provisioning declaratively, and for the tool this specification is shaped around, is in ZFN-42Conviction · currentZFN-42 — My one cloud is AWSApplying the one-cloud principle (ZFN-32), my pick is AWS — 100%, a league of its own. Go native: skip Kubernetes, use ECS; lean into SQS, SNS, Kinesis, IAM, ALB, RDS. The one exception to native — provision with OpenTofu, not CloudFormation, now that LLMs write .tf so well.Why it's cited here: The choice of declarative provisioning tool this specification assumes, and the reason plans are a machine-readable artifact at all rather than a rendered diff.Open ZFN-42 →. The credential requirements restate nothing: ZFN-9Field Note · currentZFN-9 — No long-lived cloud keys; workloads authenticate by federated identityNo static AWS or GCP keys anywhere — not in code, secret stores, or env. Workloads use their runtime's own identity and cross clouds by exchanging it (OIDC) for short-lived credentials via federation. Static keys are a documented carve-out only.Why it's cited here: The position behind the credential requirements here. Execution identity is federated and short-lived, so a stored key is never the thing an attacker takes.Open ZFN-9 → argues the position, ZBP-2Blueprint · v1.0.0ZBP-2 — A Security Token Service for workload identityServices in your platform need to authenticate to each other and you want every call attributable to a named workload holding no long-lived secret.Why it's cited here: Named as one way to obtain the short-lived execution credentials this document requires, rather than as a dependency. Where that blueprint specifies how a workload obtains credentials, this one specifies only what execution may hold.Open ZBP-2 → specifies one issuance mechanism among several, and this document only says what execution may hold. ZFN-35Field Note · currentZFN-35 — Reference secrets in config; dereference, refresh, and re-fetchDon't put secret values in config — store a reference (a path in a secret store) and dereference it at runtime via your workload identity. Refresh on a signal or expiry so rotation needs no redeploy; re-fetch on auth failure so a rotated secret self-heals.Why it's cited here: Why a workspace holds a reference to a secret rather than its value, and why dereferencing happens inside the execution boundary rather than in the control plane.Open ZFN-35 → is why a workspace stores a reference rather than a value. ZFN-55Field Note · currentZFN-55 — On AWS, the account is the unit of isolationIAM inside one account is access control; the account boundary is isolation. Quotas, billing, credential scope, and blast radius are all account-shaped, and cross-account access fails closed. One workload per account, guardrailed by SCPs and RCPs — plumbing friction is the point.Why it's cited here: Behind the isolation requirements: the execution identity per workspace exists so that a compromised control plane is not a compromised estate.Open ZFN-55 → is why the execution identity is per-workspace at all — the blast radius of a compromised gate is bounded by what it may delegate, and that is a decision made when the identities are created, not when a run starts. ZFN-64Field Note · currentZFN-64 — A config change is a deployConfig changes cause outages as often as code — and ride to production with none of code's safeguards. Anything that changes production behaviour is a deploy, whatever file it lives in: versioned, validated, canaried, staged, observable, and revertible in one motion.Why it's cited here: Why an infrastructure plan is treated as a deploy — versioned, reviewable, and revertible — rather than as an administrative action outside the release process.Open ZFN-64 → is why a plan is treated as a deploy rather than as an administrative side channel. ZFN-4Field Note · currentZFN-4 — Incident tooling must not depend on what it recoversAnything you need to respond to an incident — deploy/rollback, kill switches, observability, break-glass access — must not depend, directly or transitively, on the systems likely to be down during it. Never gate incident tooling behind a system it might need to recover.Why it's cited here: Why the gate must not become a dependency of incident response: the mechanism that applies a fix cannot require the machinery most likely to be broken during one.Open ZFN-4 → is why ZBP-6-R56 exists, and why it is a MUST rather than an operational nicety.

The one argument this document does make for itself concerns ordering. A reviewer is genuinely useful: most changes are unremarkable, deterministic rules cannot read intent, and a model can tell a tag edit from a tag edit that also replaces a database. But usefulness is not authority. The moment a model’s output can widen what is permitted, every string a plan can carry becomes an input to an authorization decision, and plan documents carry strings that attackers write. Putting the deterministic layer first and giving the reviewer only a veto costs almost nothing — the changes it would have widened were the dangerous ones — and it converts an unbounded class of prompt-injection outcomes into, at worst, an unnecessary page to a human.

Changelog

  • 2026-08-23 (1.0.0): First published. Specifies the run lifecycle and configuration capture, the plan artifact as the unit of approval with version pinning and state-identity verification, execution identity and delegation limits, the allow-list projection and its mask handling, deterministic guardrails deciding eligibility, the reviewer contract and its uniform escalation on failure, approval and identity across surfaces, and the audit record. Includes vectors V1–V14, of which V1 and V2 are taken from an executed plan and the remainder are constructed.