Skip to content

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:

  1. 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.
  2. 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.
  3. 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 sha256owner, 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:

# Per host — all its pending items in one daemon-signed batch:
etminan-verifier op approve web-01

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.

etminan-verifier op reject web-01 --path /usr/local/bin/fds-proxy --hash <sha256>

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:

etminan-verifier baseline verify-signatures

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.

  • The attestation loop — how a Genuine cycle 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.