Baseline review & drift¶
When a Genuine cycle finds a file whose hash isn't in the approved baseline, it does not decide anything — it files the change as pending review. Drift is proposed, never auto-accepted. This chapter covers the reviewer's job: reading a pending entry, approving or rejecting it, and suppressing genuinely-noisy paths with exclusions. Every one of these lands as an Ed25519-signed, audited action.
There is no auto-approve path in Etminan, and there never was one — the whole design rests on a human making each trust decision explicitly.
Every decision here is a daemon op action
All of these — op approve, op reject, op exclude create/revoke — go through the etminan-verifierd daemon: it holds the only signing key, authenticates you by peer-UID (SO_PEERCRED), gates the action against your role and scope, and signs it on your behalf. There is no operator key file and no --key/root break-glass — that path was removed in 0.10.2; op is the only way to change a baseline. The authorization model is RBAC.
The review queue¶
# Every host with pending items:
etminan-verifier baseline review
# One host:
etminan-verifier baseline review --host web-01
Every pending entry is exactly one of two kinds, and the entry tells you which:
(new file, no prior baseline)— this path has never been part of an approved baseline on this host. A genuinely new file.(was <old-sha256>)— a previously-approved path whose content hash no longer matches what was last approved. An existing, trusted file changed.
There is no third bucket: a file whose hash matches what's already approved never becomes a pending item — it's silently counted as unchanged. Entries older than the review SLA (ETMINAN_PENDING_REVIEW_SLA_HOURS, default 24h) are marked [SLA EXCEEDED] and, via run, escalated to a warning-severity finding — pending review is never a silent, indefinite state.
Reading an entry¶
[42] host=web-03 path=/usr/local/bin/agentd
⚠ An existing trusted file changed and is NOT owned by any installed
package — this pattern is a common sign of tampering. Investigate
before approving.
No approved change ticket found near this host for this time window
across any configured system — verify this wasn't unplanned.
detail: sha256=3dcbe799... (was a1b2c3...) owner=uid:0/gid:0 mode=0755
size=48213 mtime=2026-07-21T14:02:11+00:00 first_seen=2026-07-21T14:05:00+00:00
Each entry leads with a one-sentence plain-language verdict so a generalist operator can act without escalating to a security architect. Depending on what's configured, up to three context lines follow:
- Package ownership — e.g. "Likely a legitimate install — matches package nginx v1.24.0", or the loud warning above when a changed trusted file is owned by no package.
- Change-source correlation (if configured) — whether an external change/ticket system has a record explaining the change, e.g. "Matches change RT#4821 (resolved)…" or "No approved change ticket found…". See Change sources.
- Package transaction / signature trust (when a package match exists) — whether that exact version arrived via a real authenticated transaction, e.g. "✓ installed via an authenticated apt transaction…" or, on rpm, "✓ signed with trusted key…". A mismatch is a loud "⚠ … — treat with suspicion."
Only path + sha256 are cryptographic
Everything on the detail: line beyond path and sha256 — owner, mode, size, mtime — is best-effort context for a human, from a plain lstat() on the agent side. It is never part of the trust decision. What matters is that path + sha256 came from the signed, replayed IMA measurement. The (was …) field is what distinguishes a modification from a new file, and all the correlation lines are informational — none of them gate or auto-approve anything.
The drift decision¶
flowchart TD
P[Pending review item] --> Q{New file or<br/>changed file?}
Q -->|new file, no prior baseline| C{Legitimate?}
Q -->|was old-sha256| C
C -->|yes, expected change| A[op approve host<br/>daemon-signed batch → new baseline]
C -->|no, confirmed incident| R[op reject host --path --hash<br/>fires alarm immediately]
C -->|legitimate + recurs every cycle| E{Same path,<br/>re-approved repeatedly?}
E -->|yes, content never security-relevant| X[op exclude create --path --reason]
E -->|no| A
A --> D[Signed batch appended<br/>to hash-chained audit log]
R --> D
X --> D
Approve¶
Approval is the one everyday trust action the daemon performs. You name the host; the daemon authenticates you by peer-UID, checks your role, and signs the batch on your behalf, attributing it to your operator label:
op approve takes no --key: the operator identity and the audit-log allow record supply the attribution. An operator may approve only within its host scope; the daemon denies (and audits the deny) anything outside it. See RBAC.
Approving one specific hash with --matching
To approve a single pending hash for a path that currently has two, use op approve <host> --matching <path> --hash <sha256> (both flags together). A --matching approval refuses (rather than guessing) if the matched set contains more than one distinct hash for the same path — disambiguate with a narrower --matching, or just approve everything pending for the host with a plain op approve <host>.
Reject¶
Rejecting a measurement is declaring a confirmed incident — it fires the full alarm path immediately, exactly like a run finding, not a soft warning. Like every decision here it is a daemon op action, signed on your behalf and flagged in the audit log. Use the exact path and hash from baseline review, not from memory. It's idempotent: retrying an already-rejected entry does not double-record it.
Exclusions — suppressing recurring false alarms¶
Approving the same path over and over (a log rotated hourly, a cache file, an auto-generated artifact whose hash legitimately changes every cycle) is a different problem than approve solves — approve re-baselines one hash and the path comes right back next cycle once its content changes again. When you find yourself re-approving the same path with no meaningful change in what it represents, create an exclusion instead. Exclusion create/revoke are daemon op actions requiring the admin role (op exclude list is a plain read):
etminan-verifier op exclude create --path /var/log/myapp/current.log \
--reason "rotates hourly, content never security-relevant"
etminan-verifier op exclude list
etminan-verifier op exclude revoke --id 3 --reason "no longer needed"
An exclusion suppresses review, not measurement — the IMA policy still measures the path every cycle exactly as before; a match just goes to the excluded_log table instead of pending_review, so there is a permanent, auditable record of every measurement an exclusion ever hid, not a silent drop. baseline review never shows anything an active exclusion matched; exclude list is where you audit which paths are currently suppressed and why.
A trailing * is a plain prefix match
create --path accepts an exact path or a trailing-* prefix. A trailing * previews how many currently-approved measurements it would affect and requires --confirm yes. Note it is a plain string prefix — /etc* also matches an unrelated sibling like /etcxyz/anything, not just paths under /etc/; write /etc/* if that's what you mean. Prefer narrow, exact paths. Without --host, a rule applies to every enrolled host; pass --host <id> to scope it to one. --reason is required on revoke too — re-exposing whatever a rule hid is as consequential as creating it.
Verifying the signatures¶
Every approve, reject, exclude create/revoke, and the full audit-log hash chain can be re-checked in one pass:
This re-checks every approval batch's signature, every exclusion rule's create/revoke signature, every rejection's signature, and the audit-log chain — whether the action was signed by the daemon on an operator's behalf, or (for historical rows) by a legacy pre-migration operator key. Rows written before signing existed for their action are reported as "unsigned, predates this feature" rather than a failure. It also confirms each past action was authorized for its actor's role at that action's time — see RBAC. Exits non-zero if anything fails. Safe to run on demand or on a schedule alongside run.
Enterprise: four-eyes on approval¶
Enterprise edition
With ETMINAN_DUAL_CONTROL_ACTIONS including baseline-approve (or default), an op approve does not apply on one operator's action. Instead it records a pending co-sign request over the exact canonical payload; a second, distinct operator must co-sign the same payload — as their own daemon-authenticated op approve <host> — before it applies. See Dual control and RBAC.
Related¶
- The attestation loop — how a
Genuinecycle produces these pending items. - RBAC — the daemon roles and scopes that gate
op approve. - Enrolling a host — the first, bulk baseline approval.
- Change sources — the ticket-correlation line in a review entry.
- Audit log — where every one of these decisions is permanently recorded.