Skip to content

Dual control

Dual control — "four-eyes" — makes a single approval insufficient for a high-consequence action. When enabled, that action no longer applies on the initiator's approval alone: it becomes a pending request that a second, distinct authorised operator must co-sign before it takes effect. This is the separation-of-duties control security teams ask for by name, built directly on the daemon's peer-UID identity registry.

Enterprise edition

Dual control is compiled only into the Enterprise build and is off by default. It needs a notion of authorised, distinct operators to enforce against — which the daemon already provides via kernel peer-UID.

Co-signing rides on authenticated UIDs — there is no key file

There is no --key and no break-glass here (that path was removed in 0.10.2). A co-signer proves who they are exactly as any operator does: their Unix UID, read from the kernel by the daemon via SO_PEERCRED. Co-signing an action is simply a distinct authorised operator re-issuing the same op command; the daemon counts distinct UIDs and applies the action once the threshold is met.

What "no single-person path" means

Two mechanisms decide whether four-eyes is in force for an action, and they compose:

  • The env floorETMINAN_DUAL_CONTROL_ACTIONS, a set of action tokens.
  • The signed, latched policy — a signed row in baseline.db, set via op dual-control-policy set.

The effective policy is the union of the two: the env var can only ever add coverage, and the signed policy latches actions on such that unsetting the env can never remove them. The effective threshold is max(env, signed-policy, 2) — both can only raise it, never drop it below two.

The policy is authoritative over the env

A latched signed policy is the whole point of the design: once you sign it on, an operator with shell access on the verifier cannot weaken four-eyes by editing an environment variable. And changing the policy is itself four-eyesop dual-control-policy set opens a request a distinct admin must co-sign. There is no single-person path to disable it.

Configuring the env floor

ETMINAN_DUAL_CONTROL_ACTIONS is a comma-separated list of tokens. Unset / empty / off / none = off. An unknown token is a hard error — a misconfiguration can never silently fail open to single-signed.

Token Covers
baseline-approve Baseline drift approval — the four-eyes-eligible action
default Expands to baseline-approve
# Enable four-eyes on baseline approval
ETMINAN_DUAL_CONTROL_ACTIONS=baseline-approve

# Tuning knobs
ETMINAN_DUAL_CONTROL_THRESHOLD=2      # M-of-N; default 2, clamped up (never below 2)
ETMINAN_DUAL_CONTROL_TTL_HOURS=168    # how long an un-completed request stays open (default 7 days)

The signed, latched policy

op dual-control-policy show prints the current signed floor; op dual-control-policy set changes it. Because set is itself four-eyes, it does not apply immediately — it records a pending request that a distinct admin must co-sign by re-issuing the same command.

# Inspect the signed floor
etminan-verifier op dual-control-policy show

# Propose latching four-eyes on baseline-approve at threshold 2 (opens a co-sign request)
etminan-verifier op dual-control-policy set \
  --actions baseline-approve --threshold 2 \
  --reason "enforce separation of duties on baseline approval"
# → recorded as a pending request (1 of 2). NOT applied yet.
#   A DIFFERENT authorized admin co-signs by re-issuing the SAME command:
#     etminan-verifier op dual-control-policy set --actions baseline-approve --threshold 2 --reason "…"

--actions accepts baseline-approve (or off/none to disable, which is also a four-eyes change). Each request captures the effective threshold at creation time, so a later config change never retroactively alters an open request.

The co-sign flow

sequenceDiagram
    autonumber
    actor A as Operator A (initiator, UID a)
    actor B as Operator B (distinct, UID b)
    participant V as etminan-verifierd
    participant Q as dual_control_requests
    participant DB as baseline.db + audit_log

    A->>V: op approve web-01  (peer-UID a)
    V->>V: build canonical payload; record UID a's approval
    V->>Q: record pending request (payload + UID a)
    V-->>A: NOT applied — needs a distinct co-signer (1 of 2)
    Note over V: emits a `dual-control-pending` notification through the alarm channels

    Note over B: sees the dual-control-pending notification
    B->>V: op approve web-01  (peer-UID b)
    V->>V: b ≠ a? b authorised for the action + host scope?
    V->>Q: record UID b's co-signature over the SAME payload
    alt threshold reached (2 distinct UIDs)
        V->>DB: apply the action (same code path as single-signed)
        V->>Q: mark request applied
        V-->>B: APPLIED
    else still short
        V-->>B: co-signed, still needs more
    end

Concretely:

# 1. Operator A initiates — the action becomes a pending request, not applied
etminan-verifier op approve web-01

# 2. A `dual-control-pending` notification fires so a second operator knows to act

# 3. A DIFFERENT authorised operator co-signs by re-issuing the same command
etminan-verifier op approve web-01
#   → once 2 distinct UIDs have approved, it applies automatically

What makes it a genuine two-person proof

Both operators approve the exact same canonical payload, so both approvals bind to one set of bytes — two people vouching for one decision, not two loosely-related actions. On top of that, the daemon enforces at co-sign time:

  • Authorised for the action and scope. Each co-signer must independently satisfy the action's required role and host scope under the default-deny RBAC matrix — the same bar as the direct action.
  • Distinct UID. The same UID cannot approve twice; a re-run by the initiator is idempotent, never a second approval. Because the UID is supplied by the kernel (SO_PEERCRED), it cannot be forged — the strongest defence against one operator standing in for two, short of an external identity system.

M-of-N, not just two

Set ETMINAN_DUAL_CONTROL_THRESHOLD (or the policy --threshold) above 2 to require more distinct approvers. Every value is clamped up to a floor of 2 — below that isn't four-eyes.

Storage, audit, and verification

Requests and approvals live in dual_control_requests / dual_control_approvals in baseline.db; every initiate, co-sign, apply, and expiry is also appended to the hash-chained audit log. Un-completed requests past their TTL are swept to expired.

baseline verify-signatures re-checks the four-eyes backing after the fact: a batch (or the signed policy row) marked dual-control must be backed by a request whose stored canonical payload equals the row's current signed payload, with the required number of distinct, authorised co-signatures — so a hand-edited row or a forged single-signed apply is caught.

Where to go next

  • RBAC — the peer-UID identity registry and role/scope matrix each co-signer is checked against.
  • Baseline review & drift — the op approve action four-eyes protects.
  • The audit log — where every request and co-signature is recorded.