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 exactly what the gate covers, 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.
It is worth being precise about what this is not, because three nearby mechanisms are easy to conflate. 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. 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: setting or repointing a destination, creating, editing, enabling or disabling an identity-provider connection, setting the discovery token, applying an engine update, minting a support credential, and 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 mutation | How it is gated instead |
|---|---|
| Cloudflare config restore | Dual control, but a separate restore-approval gate of its own |
| Key ceremony | Plain 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 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 |
| The discovery token, engine updates, support-credential mint, break-glass-token retire | Dual-control owner-action gated, armed only by the requireConfigApproval toggle |
| Canary configuration | Plain owner-only; deliberately not dual-controlled |
| Licence activation | Plain 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, editing, enabling or disabling an identity-provider connection; 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, 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, while 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.
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.
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.
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, with nothing queued. The proposer learns immediately, rather than after a wasted approval.
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 record behind it holds the change’s real parameters, and the pending-change list returns them unredacted to a reader with the config-read capability.
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.
Approval rules:
- 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. A display-email comparison is kept as a belt-and-braces floor, so a self-approval is refused even across an email change, and a pre-subject record can still never be self-approved.
- 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 role-write, approving a notify change needs the notify capability, 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 role-write, so a lone compromised owner cannot mint a second owner through the approval path either.
- 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.
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.
| State | What it means |
|---|---|
| Pending | Queued and awaiting a second approver. It is the only state that can be approved or rejected. |
| Applied | A distinct approver approved it and the validated replay committed. This is terminal, and a change applies exactly once. |
| Rejected | It was declined by a second party, or withdrawn by the proposer, and nothing was applied. |
| Superseded | The 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 console renders these as status badges whose labels match the engine states: Pending shows as “Awaiting approval”, Applied as “Applied”, Rejected as “Rejected”, and Superseded as “Superseded”. 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.
| Direction | Who | What happens |
|---|---|---|
| Arm the gate | An owner | Applies immediately. |
| Disarm, attributable owner | A passkey, Access or identity-provider owner | Routed through a second-owner approval; it only takes effect when a distinct second owner approves. |
| Disarm, break-glass | The bare-token owner | Applies 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, and it is disabled with a stated reason for a caller whose capability set does not hold the change’s write capability. 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 any authenticated role, on the config-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, the config screens behave honestly about it. A config action that gets queued surfaces a queued-for-approval state with a link to the inbox, rather than 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.
| Status | Outcome | When | What to do |
|---|---|---|---|
| 200 | Applied inline | The gate is off | Nothing; the change applied as normal. |
| 202 | Queued | The gate is on and the change validated | Note the change id; a second approver actions it in the inbox. |
| 400 | Rejected at propose | The change failed validation, exceeded the proposer’s authority, or would break the last-owner guard | Fix the request and propose again; nothing was queued. |
| 400 | Cannot self-approve | The approver is the proposer on the subject or email axis | A different authorised person must approve. |
| 400 | Missing capability | The approver does not hold the change’s write capability | An approver who holds that capability must action it. |
| 400 | Integrity check failed | The stored record’s hash did not recompute | The record cannot be applied; raise the change again. |
| 400 | Superseded | The configuration moved since the change was proposed | Raise the change again against the current configuration. |
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:
| Covered | Which actions, and why they are here |
|---|---|
| Every dual-control owner operation except four | Setting, 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 destination | Neither 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 restore | The 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 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.ceremonythat 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 .