Skip to content
downpipes docs

Four-eyes change control for config changes

Four-eyes change control is an opt-in gate that holds a config change until a second authorised person approves it. It is the config-mutation counterpart of the dual control that protects a restore: the person who proposes a change is not the person who approves it, and the approver must hold the same permission the change itself requires.

This page is for an approver and an owner deciding whether to turn it on. It covers which changes the gate holds, what happens to a change when the gate is armed, the deliberately asymmetric off-switch, and the inbox where changes are reviewed. The recurring theme is that the gate adds a second approval without adding a second authority model: a change still runs the same validation and the same capability checks it would without the gate.

Three nearby mechanisms are easy to conflate with change control. Change control is not the dual control over a live-data restore, which binds to a restore plan hash and lives under dual control for restores. It is not the Cloudflare config backup and restore feature, which backs up your Cloudflare account configuration and is covered in the operations section. And it is not the config version history, which records past config versions rather than queuing future ones.

Off by default, and identical to an inline apply when off

The setting that arms the gate, requireConfigApproval, defaults to off. While it is off, a config mutation applies inline exactly as it would if the feature did not exist. The code path is byte-identical to a plain inline apply: there is no second-approver requirement, no pending record, and no change to the response a route returns. Apart from the auto-armed cases later on this page, nothing about change control affects an account until an owner arms it.

That property is what makes the gate safe to leave off and safe to turn on. Turning it on does not retroactively change anything, and turning it off returns to the inline path. The only difference the gate makes is in the window where it is armed.

The same requireConfigApproval toggle does more than gate config mutations. Arming it also arms second-owner dual control over a closed set of high-blast-radius owner operations that are not config-history mutations:

  • Adding, editing, repointing or removing a destination, and repointing the default destination
  • Opening or repointing the SIEM push destination or the OTLP metrics push destination
  • Creating, deleting, enabling or disabling an identity-provider connection, or rolling its certificate
  • Setting the discovery token or the browsed accounts
  • Attaching a source
  • Applying an engine update, or keeping one after its canary
  • Minting a support credential
  • Retiring the break-glass token

Those operations carry their own dual-control approval, run through a parallel approval store rather than the config-change inbox, but they are armed by this one switch. So turning the gate on is a single decision that brings four-eyes to both config changes and these owner operations at once.

What the gate covers, and what it does not

When the gate is on, it covers a closed set of config-mutating routes uniformly. The set is exactly the config mutations that record into config history, and it is a fixed union in the code, so a crafted request cannot reach the apply path through some unlisted kind.

The gate covers:

  • Saving or deleting a downpipe
  • Setting or deleting a role
  • Setting or deleting a custom role
  • Setting or deleting a group mapping
  • A notify channel, rule or webhook change
  • Accepting or un-accepting a posture risk
  • A tracked expiry item change
  • A coverage inventory update
  • A Cloudflare-config downpipe’s capture-mode change (auto or manual)

Several privileged actions are not covered as config mutations, and they fall into two groups that are easy to conflate. Some are dual-control owner operations armed by the same requireConfigApproval toggle, so when the gate is on they do get a second owner, just through the parallel owner-action store rather than the config-change inbox. Others are plain owner-only operations that this toggle never dual-controls at all, gated only by requiring an owner. Do not assume either group routes through the config-change inbox.

Not covered as a config mutationHow it is gated instead
Cloudflare config restoreRestore dual control, a separate gate armed by its own owner toggle (requireRestoreApproval), which is off by default
Key ceremonyPlain owner-only, on the keys.ceremony capability
Destinations (set, repoint or remove)Dual-control owner-action gated, auto-armed as soon as a second owner exists, independent of the requireConfigApproval toggle
Identity-provider connections (create, delete, enable, disable or certificate change)Dual-control owner-action gated, auto-armed as soon as a second owner exists, independent of the toggle
SIEM push destination (set or repoint)Dual-control owner-action gated, auto-armed as soon as a second owner exists, independent of the toggle
OTLP metrics push destination (set or repoint)Dual-control owner-action gated, auto-armed as soon as a second owner exists, independent of the toggle
Repointing the default destination, the discovery token and browsed accounts, attaching a source, engine updates, support-credential mint, break-glass-token retireDual-control owner-action gated, armed only by the requireConfigApproval toggle
Canary configurationPlain owner-only; deliberately not dual-controlled
Licence activationPlain owner-only on keys.ceremony; deliberately not dual-controlled

Four of these owner operations do not wait for the toggle. Adding, repointing or removing a destination (the dest-set, dest-put and dest-remove actions); creating, deleting, enabling or disabling an identity-provider connection, or rolling its certificate; opening or repointing the SIEM push destination; and opening or repointing the OTLP metrics push destination all auto-arm second-owner dual control as soon as a second owner exists, even when requireConfigApproval is off, because a lone compromised owner could otherwise redirect every future backup, destroy proven copies, change who can sign in, or redirect audit or telemetry egress. This auto-arm does not extend to repointing which destination is the default seal target, to the discovery token or browsed accounts, to attaching a source, to engine updates, to a support-credential mint or to break-glass-token retirement: those stay governed by the toggle. With only one owner none of these auto-arm, since there would be no second owner to approve, and the bare-token break-glass owner is exempt so the first owner can still wire the first identity provider.

One kind auto-arms independent of the toggle for a different reason: setting a role to Owner auto-arms dual control as soon as a second owner exists, exactly like the destination and identity-provider auto-arm above, so a lone owner cannot mint a second owner without a second owner approving.

The gate covers config mutations, in other words, not the whole surface of privileged operations. The plain owner-only operations carry only an owner check. The dual-control owner operations share this toggle but live in their own approval store rather than this inbox.

What happens on propose when the gate is on

When the gate is armed, a covered config route does not apply the change. It validates the change fully, and if the change is sound it queues it as a pending change request. The validation is not a lightweight pre-check: it runs the real mutation.

  1. The real mutation runs under a checkpoint

    The engine takes a checkpoint over its whole storage keyspace, and captures the durable alarm separately because a downpipe change can re-arm it. It then runs the actual validated mutation method, the same one a gate-off apply would run, including its no-escalation and last-owner guards and its per-method capability re-check.

  2. The change is rolled back

    The engine rolls the checkpoint back unconditionally, restoring every changed key, deleting any key the mutation added, and resetting the alarm to its prior state. A mere proposal therefore leaves no side effect at all, not even a re-armed alarm.

  3. A bad or unauthorised request is rejected now

    Because the real method ran, a request that fails validation, exceeds the proposer’s authority, or would break the last-owner guard throws at propose time. The route returns a 400 then and there, or a 403 for an authority failure, with nothing queued. The proposer learns immediately, rather than after a wasted approval.

  4. A sound request is queued as a pending change

    A clean run yields the configuration the change would have produced, which the engine diffs against the current configuration to render a plain-English change list. It stores a pending change record carrying that diff, the proposer, the time and an integrity binding, and the route answers with a 202 and the change id.

The diff the engine renders is the same plain-English diff the config version history uses, reading only named configuration metadata, so the diff the console shows you is redaction-safe: a webhook reads as its host only, and a routing key reads as present rather than as its value. The stored record is more than that diff, though. Alongside the id, the kind of change, the diff, the proposer’s identity and a timestamp, the engine keeps the verbatim validated mutation body so it can replay the change on approval, and for a webhook or channel change that body can include a URL token or a routing key.

So do not read a change record as carrying no secret. The diff is safe to show; the stored record holds the change’s real parameters. The pending-change list and the approve and reject responses redact them: a URL reads as its host only, and a routing key or API key is left out. When a change becomes applied, rejected or superseded, the engine also removes those fields from the stored record. A change that stays pending keeps them in storage until someone decides it.

The approval rules

Approving a pending change applies it, by replaying the same validated mutation through the single apply path. Three rules gate an approval, and all of them are enforced server-side inside one atomic read-modify-write so the decision and the apply cannot diverge. Before any of them runs, approving needs a fresh step-up sign-in; rejecting does not, because only the direction that applies a change is gated.

Approval rules:

  1. The first rule is maker is not checker. The approver must differ from the proposer on the stable-subject axis, which is the same immutable identity the role table keys on, exactly as the restore dual control compares subjects, so a self-approval is refused even across an email change. The engine also compares the display email as a second check, so it refuses a self-approval on either axis.
  2. The second rule is that the approver must hold the same write capability the original mutation requires, re-resolved live from the engine’s own tables rather than trusted from anything forwarded. Approving a role change needs roles.write, approving a notify change needs notify.config, and so on. The mapping is the same per-route capability the change would have needed inline, so the gate adds no new authority. One exception: a role change that grants the Owner role can only be approved by an Owner, never by an access-admin merely holding roles.write, so a lone compromised owner cannot mint a second owner through the approval path either.
  3. The third rule is no stale or superseded apply. The pending change binds a content hash over its kind, its parameters, the proposer’s identity and the config-history head it was computed against. At approve time the engine recomputes that hash and re-reads the current head. If the parameters or the proposer identity were tampered, the hash no longer matches and the change is refused. If the head has moved since the proposal, the diff the approver reviewed is stale, so the change is marked superseded and refused, and it must be raised again.

Two further checks apply. The approval must arrive at least five seconds after the proposal. At approve time, the engine also re-checks that the proposer still holds the change’s write capability.

The approver approves exactly what they reviewed

The stale check is the defence against a change being approved against a configuration it was never reviewed against. An approver approves the precise change they saw, against the precise base it was computed against, or nothing applies. A concurrent config change between proposal and approval supersedes the pending change rather than silently applying it against a moved base.

A rejection has a lighter rule. Only a still-pending change can be rejected, but a rejection does not require maker is not checker, because the proposer is allowed to withdraw their own change.

The states a pending change moves through

Once queued, a change carries one of four lifecycle states, so a reader always knows where it stands.

StateWhat it means
PendingQueued and awaiting a second approver. It is the only state that can be approved or rejected.
AppliedA distinct approver approved it and the validated replay committed. This is terminal, and a change applies exactly once.
RejectedIt was declined by a second party, or withdrawn by the proposer, and nothing was applied.
SupersededThe configuration moved between propose and approve, so the diff the approver reviewed was stale. The change is refused and must be raised again against the current configuration.

The engine lists pending changes only, so the console inbox shows each change with one status badge. The label of that badge is “Awaiting approval”. A change leaves the inbox when a second approver applies or rejects it, or when the proposer withdraws it. A change that the engine marks superseded leaves the inbox too. The config history screen shows the configuration versions that the engine records after it applies a change. A superseded change is a stale change to raise again against the current configuration, not a timeout: there is no time-based expiry for a pending change.

The asymmetric off-switch

Arming and disarming the gate are deliberately not symmetric, because they carry very different risk. Arming a control, or re-stating it, is always safe, so it applies immediately. Disarming is the one dangerous direction: a single compromised owner who could flip the gate off, run every covered change inline, and flip it back on would have bypassed the whole guarantee.

So the off-switch is gated against itself, with one escape.

DirectionWhoWhat happens
Arm the gateAn ownerApplies immediately. An attributable owner can arm it only when a second owner exists.
Disarm, attributable ownerA passkey, Access or identity-provider ownerRouted through a second-owner approval; it only takes effect when a distinct second owner approves.
Disarm, break-glassThe bare-token ownerApplies immediately, because the break-glass owner is the lockout escape and cannot itself propose or approve owner actions.

There is no deadlock in this design. Arming is always immediate and the break-glass owner can always disarm immediately, so an account can never be locked out of its own off-switch. And every disarm, by whichever path, fires a real-time alert through the notify channels, because disarming the whole guarantee is something to be told about, not merely audited.

The toggle is owner-only. It is gated on the owner-exclusive keys.ceremony capability, which an access-admin does not hold, and the engine additionally re-resolves the caller’s role and requires owner. An access-admin can manage roles and the access policy, but cannot arm or disarm the four-eyes gate.

The change-request inbox

The console surfaces pending changes in a change-request inbox, which mirrors the restore approvals inbox in look and behaviour. It is a polled list of cards. Each card shows the engine-rendered plain-English diff, who proposed the change, when it was proposed, and Approve or Reject actions.

The inbox mirrors the engine’s rules so a button is never a dead end. Approve is hidden on a caller’s own proposal, with a note that they proposed it and are awaiting another approver. For a caller whose capability set does not hold the change’s write capability, Approve and Reject are both hidden, and a note names the capability the change needs. The engine remains the authority and re-checks both rules; the console mirror only saves the approver a futile click.

Reading the inbox is allowed for every built-in role, on the downpipe.read capability, the same floor that lets a caller view the configuration at all. The approve and reject actions are gated on the change’s own write capability.

When the gate is armed, a queued config action shows a queued-for-approval state with a link to the inbox. It does not show a false success. You learn that your change is waiting, not that it applied.

The bare token cannot take part in four-eyes

Four-eyes needs two attributable, stable identities to compare. The bare-token break-glass owner has no email and no subject, so it can neither propose a config change nor approve one. It refuses both. Its one related power is the immediate disarm above, the lockout escape. For why a subjectless caller cannot be a maker or a checker, see the identity and access overview.

Reference: the propose and approve outcomes

These are the outcomes a covered config route and the approve route return when the gate is armed.

StatusOutcomeWhenWhat to do
200Applied inlineThe gate is offNothing; the change applied as normal.
202QueuedThe gate is on and the change validatedNote the change id; a second approver actions it in the inbox.
400Rejected at proposeThe change failed validation, or would break the last-owner guardFix the request and propose again; nothing was queued.
403Forbidden at proposeThe change exceeded the proposer’s authorityAsk someone who holds that authority to propose it; nothing was queued.
400Cannot self-approveThe approver is the proposer on the subject or email axisA different authorised person must approve.
400Missing capabilityThe approver does not hold the change’s write capabilityAn approver who holds that capability must action it.
400Integrity check failedThe stored record’s hash did not recomputeThe record cannot be applied; raise the change again.
400SupersededThe configuration moved since the change was proposedRaise the change again against the current configuration.
403Proposer authority lapsedThe proposer no longer holds the change’s write capabilityA second approval does not help. Someone who holds that capability must propose the change again.
401Step-up requiredThe approver has not freshly re-authenticated with a passkey or AccessRe-authenticate, then approve again.
429Too soonThe approval arrived within five seconds of the proposalWait and approve again; the response carries a Retry-After header.

The exact request and response bodies for the policy toggle, the pending-change list, and the approve and reject routes are catalogued in the admin endpoints reference.

Require Change Number

Require Change Number is a separate, optional policy that sits alongside four-eyes approval on the same approval-policy view. It is owner opt-in and off by default. When it is on, every change-controlled action prompts for a change reference before it proceeds, and the reference is recorded in the audit log against that change.

The set it covers is not the config-mutation set above, and reading it as though it were is the mistake this section exists to prevent. Change control and four-eyes are two axes over two different populations, and an action can sit on either, both or neither.

What Require Change Number covers:

CoveredWhich actions, and why they are here
Every dual-control owner operation except fourSetting, repointing, editing or removing an archive destination, and repointing the default. Opening, replacing or repointing the SIEM push destination and the OTLP metrics push destination. Creating, deleting, enabling, disabling or rolling the certificate on an identity-provider connection. Setting the account-browsing discovery token and choosing the browsed accounts. Minting a support credential. Retiring the break-glass bearer token.
Clearing either push destinationNeither clear is a dual-control owner operation, because closing an egress is the safe direction and queuing it for a second owner would hold a suspect path open. Each is change-controlled at its own engine route instead, so a clear under this policy still carries a reference.
Applying a restoreThe router asks the engine to enforce the policy and record the reference before it writes a byte.

What it does not cover:

  • The four exempt owner operations: applying an engine update, settling one as keep or rollback, attaching a source, and disarming the four-eyes gate. An update is exempt because the apply is already canary-gated and its rollback is the safe recovery direction; disarming four-eyes is exempt because a change-number requirement on the off switch would be one more thing between an account and its own escape hatch.
  • Every config mutation in the covered-by-four-eyes list above. Saving a downpipe, setting a role, composing a custom role, mapping a group, changing a notify rule or channel, accepting a posture risk, tracking an expiry item, updating a coverage inventory and setting a Cloudflare-config capture mode all record into config history and can be queued for a second approver, but none of them prompts for a change number. If your CAB expects a reference against a role grant, the audit log’s own record of who granted what and when is the evidence available, not a change reference.

The prompt asks for one of two things. A normal change carries a change number, your CAB or ITIL ticket reference of up to sixty-four characters. A change with no number to hand is raised as an Emergency Change instead: a loud, deliberately visible mode that requires a justification of up to five hundred characters and takes an optional retrospective number, so an out-of-hours fix is still recorded and stands out on the trail.

The console refuses both bounds at the field. At the engine, the two bounds behave differently past their limit, and that is deliberate. A change number longer than sixty-four characters is refused, by the console at the field and by the engine at the request, so nothing runs until it is corrected. A change number is a reference to a record in your own change-management system, so a shortened one would still look like a reference and match nothing, which is the one job it has. A justification longer than five hundred characters that reaches the engine is accepted and its tail is dropped, because it is prose: losing the end costs detail rather than identity, and the full account belongs in the change record itself. Control characters and leading or trailing whitespace are stripped before either bound is measured, so a reference pasted with a stray line break is not refused for the sake of it.

The reference is a process and compliance control, not a security control. No gate reads it and it confers no authority: it rides on the request exactly like the operator’s source IP, request-scoped provenance the engine records but never trusts. It is operator-attested free text, bounded and stripped of control characters, in the same redaction-safe class as a restore reason, never a secret. So the change number tells your auditors which CAB change a configuration edit belonged to; it does not decide whether the edit is allowed. Whether an edit needs a second approver is the four-eyes gate above, a different control.

When the policy is on, the security centre shows it as enabled, the posture score’s emergency-change-review check watches for emergency changes, and a change-requests report lists the references recorded. Turning the policy off is itself owner-only, on the same keys.ceremony capability, though, unlike the four-eyes toggle above, this one is never itself dual-control gated.

Where this fits

  • Dual control for restores is the live-data counterpart, gating a restore apply rather than a config change, and it is a separate mechanism.
  • Roles and capabilities defines the write capabilities an approver must hold, and confirms that keys.ceremony that gates the toggle is owner-reserved.
  • Identity and access overview places change control alongside the rest of the section and explains the stable-subject axis the maker-is-not-checker rule compares.
  • Audit log records the propose, the approve, any supersede, and every disarm, and is tamper-evident.

Last updated .