RBAC¶
Role-based access control in Etminan is enforced by a process, not a policy file. Every trust-changing action on the verifier — enrol a host, approve a baseline, change the operator registry — is reached through a long-running privilege-separated daemon, etminan-verifierd, over a local Unix socket. The daemon is the only process that touches verifier state and the signing key, so the daemon is the wall: it can allow or deny an operator regardless of what that operator can type.
This supersedes the old key-file role model
Earlier releases enforced roles with per-operator Ed25519 key files — an opt-in Enterprise ETMINAN_RBAC=on model with roles operational / operator / approver / reviewer / viewer and --scope globs, driven by operator-key --role. That design is replaced by the daemon model on this page. The --key CLI flow is removed in 0.10.2 — op is the only operator path (only read-only verify-signatures reconciliation of legacy-signed records remains).
RBAC is part of the Standard build and is on by default. There is no feature flag to turn it on and no edition to buy: the daemon is how you operate the verifier.
Why a process boundary is required¶
A plain CLI can never enforce anything against a user who is allowed to run it. If the authorisation check and the signing key live inside the same binary the operator invokes, that operator can read the key, patch the check, or call the underlying code directly — the "gate" is decoration. Only a process boundary can enforce authority: a separate, differently-privileged process that holds the key, makes the decision, and hands back nothing but a result.
etminan-verifierd is that process. It runs as its own service (etminan-verifierd.service), owns the baseline database and the signing key, and listens on a Unix socket. Operators never touch either; they send a request and receive an allow or a deny.
Authentication — kernel peer-UID¶
When a client connects to the socket, the daemon reads the peer's credentials straight from the kernel via SO_PEERCRED. The connecting UID is supplied by the kernel, not by the client, so a non-root peer cannot forge it. That single fact is the whole authentication story:
- No key files to steal, copy, or leave on a monitored host.
- No PAM, no passwords, no shared secrets on the request path.
The operator's Unix identity is their credential. Log in as yourself and the daemon already knows who you are.
Authorisation — a default-deny role/scope matrix¶
Authenticated is not authorised. Each UID is mapped, in the daemon's identity registry, to exactly one role and a scope. The matrix is default-deny: a UID that is unmapped, or whose mapping was revoked, is refused every trust-changing action. There is no implicit fallback — fail-closed is the only behaviour.
| Role | May do | Scope |
|---|---|---|
admin |
Manages the identity registry — add, list, and revoke operator identities; every operational action | Always unscoped (whole fleet) |
operator |
Approves baselines and drives day-to-day trust changes within an assigned host-group | Confined to one host-group |
viewer |
Read-only — status, history, listings. Signs nothing | Read view only |
admin is the only role that changes who else is trusted; operator does the daily work but is boxed into its host-group; viewer is a pure read credential. Any UID not present in the registry — or present but revoked — is denied.
Host scope¶
An operator mapping carries a scope: a host-group the operator is confined to. Scope is checked after the role check, on host-targeted actions, so a region-A operator can approve region-A hosts and nothing else. admin identities are always unscoped and see the whole fleet.
The op client¶
Operators drive the daemon with the thin client etminan-verifier op <cmd>. It carries no key and holds no authority of its own — it just marshals your request onto the socket, where the daemon authenticates and authorises it.
etminan-verifier op whoami # what the daemon thinks you are: uid, role, scope
etminan-verifier op ping # liveness check against the socket
# Identity registry (admin only)
etminan-verifier op identity list
etminan-verifier op identity add --uid 1007 --role operator \
--scope region-a --label "carol (region-A operator)"
etminan-verifier op identity revoke --uid 1007
# Day-to-day
etminan-verifier op approve web-01 # operator: approve a baseline in scope
Bootstrapping the first admin¶
Genesis is a chicken-and-egg problem: an empty registry has no admin to authorise the first one. So the first admin is installed once, as root, directly on the verifier host:
# Run as root, one time at verifier bring-up
etminan-verifier op bootstrap --uid 1001 --label "admin-1"
Requiring root for genesis means the bootstrap authority is the machine's own root, not a forgeable request. The only non-root path is the escape hatch ETMINAN_VERIFIERD_ALLOW_INSECURE_BOOTSTRAP, which is DEV/TEST only — never set it in production. From that first admin onward, all further identities are added through op identity add under the default-deny matrix.
Two host prerequisites (or nothing works)¶
Neither of these is a code step; both are one-time host setup that must hold, and both fail loudly if they don't.
1. The daemon-owned state files must be readable/writable by the daemon's user. etminan-verifierd runs as etminan-verifier (see etminan-verifierd.service). If a file it needs ends up owned by root — e.g. it was created by a root-run bootstrap — the daemon fails. The two that bite:
daemon-signing.key— read at startup; aroot-owned key crash-loops the daemon withreading daemon key … Permission denied (os error 13).state.json— the pinned AK/EK/TLS fingerprint store, written byop enroll/op rotate-*; aroot-owned one fails those withreading state file … Permission denied (os error 13). Runningopasroot(e.g. a root-run bootstrap) can leave it root-owned; run routineopcommands as the operator's own account so it is created correctly.
Ensure both are owned by the daemon user:
chown etminan-verifier:etminan-verifier /var/lib/etminan-verifier/daemon-signing.key \
/var/lib/etminan-verifier/state.json
chmod 600 /var/lib/etminan-verifier/daemon-signing.key
systemctl restart etminan-verifierd
2. Operator accounts must be in the etminan-verifier group to reach the socket. The daemon socket /run/etminan-verifierd/etminan-verifierd.sock is 0660 etminan-verifier:etminan-verifier. A non-root user that is not in that group is refused by the filesystem — could not connect … Permission denied (os error 13) — before RBAC is ever consulted. Add each operator's Linux account to the group:
This grants socket access only — it does not grant read of baseline.db or the signing key, which stay 0600 owner-only. Peer-UID (SO_PEERCRED) plus the role/scope matrix still decide what the operator may actually do.
The daemon holds the only key¶
Operators hold no signing key at all. The daemon holds the single Ed25519 key (/var/lib/etminan-verifier/daemon-signing.key) and, once it has authenticated and authorised the caller, signs the action on that operator's behalf. The signed record is attributed operator:<label>, so accountability is preserved without any key ever leaving the daemon or reaching an operator.
This is what makes the boundary meaningful: even an operator with a shell on the verifier cannot sign a decision the daemon would refuse, because they never possess the key that turns a request into a trusted record.
Every decision is on the audit chain¶
Allow and deny alike are appended to the hash-chained, externally-anchored audit log. A refused request is not silent — it is evidence. Because the chain is tamper-evident and its head is anchored outside the box, the record of who was denied what, when is as durable as the record of what was approved.
Optional TOTP two-factor¶
The daemon supports native TOTP (RFC 6238) as a second factor on top of peer-UID, off by default and configurable per role:
etminan-verifier op totp-policy --role operator --required true # require TOTP for operators
etminan-verifier op enroll-totp # prints an otpauth:// URI to scan
etminan-verifier op login --code 123456 # open a session
etminan-verifier op logout # end it early
A login opens a session with an 8-hour TTL (ETMINAN_TOTP_SESSION_TTL_SECS). No key files are involved — TOTP layers a possession factor onto the identity the kernel already proved.
The --key CLI flow is removed¶
The old --key flow (baseline approve/reject/exclude, operator-key init/authorize/revoke/rotate, enroll, assign-profile, rotate-tls/rotate-ak, dual-control approve/reject) is removed in 0.10.2 — for every caller, root included. Each such command is now refused as an unknown subcommand; there is no break-glass and no root escape hatch. op (over the daemon socket) is the only path that changes trust. Read-only commands (check, run, baseline review/verify-signatures) stay open to everyone.
No CLI recovery when the daemon is down
Because --key is gone, there is no command-line way to change trust while etminan-verifierd is stopped — this is the intended op-only end state. Recover the daemon (see Troubleshooting); do not expect a key-file fallback.
The request flow¶
sequenceDiagram
autonumber
actor Op as Operator (op client, no key)
participant Sock as Unix socket
participant D as etminan-verifierd
participant Reg as identity registry
participant Log as audit_log (hash-chained)
participant DB as baseline.db + signing key
Op->>Sock: op approve web-01
Sock->>D: connect (peer-UID via SO_PEERCRED)
D->>D: kernel-supplied UID — unforgeable
D->>Reg: UID → role + scope?
alt mapped, role permits, host in scope, TOTP (if required) valid
Reg-->>D: operator / region-a / ok
D->>Log: append ALLOW
D->>DB: sign on operator's behalf, apply
D-->>Op: applied (attributed operator:carol)
else unmapped / revoked / out of scope / no session
Reg-->>D: denied (default-deny)
D->>Log: append DENY
D-->>Op: refused
end
How it composes with dual control¶
RBAC decides whether one operator may act; dual control decides whether one is enough. They stack: with four-eyes in force, a pending action needs a second, distinct authenticated UID to co-sign, and each co-signer must independently satisfy the action's required role and scope under the same default-deny matrix. Dual control remains Enterprise and off by default; its old --key co-sign UX is removed, with the four-eyes concept now riding on authenticated UIDs rather than key files (the policy latch itself is set via the daemon-mediated op dual-control-policy).
Where to go next¶
- Dual control — require a second distinct operator to co-sign (Enterprise).
- The audit log — where every allow and deny is recorded and anchored.
- Editions — what is Standard vs Enterprise.