Skip to content

Verifier configuration

etminan-verifier is where every trust decision is made: it polls each enrolled agent on a schedule, replays the IMA log against the TPM-signed quote, diffs the result against an approved baseline, and is the only place a finding ever leaves the box — this chapter covers the state, intervals, and outputs you configure to run it.

The verifier needs no TPM. It runs on one separate, independently-administered device — a single host is never both agent and verifier, because the verifier's trust value comes from being administratively separate from the hosts it checks.

The verifier is the authority

A fully root-compromised monitored host cannot forge a genuine verdict or silently suppress an alarm, precisely because the host never judges itself. Keep the verifier's administration, keys, and network position separate from the fleet it watches. See Hardening the deployment.

What the verifier does each cycle

sequenceDiagram
    autonumber
    participant T as etminan-verifier.timer
    participant V as etminan-verifier run
    participant A as etminan-agent (each host)
    participant DB as baseline.db
    participant Out as Alarm path
    T->>V: fire hourly
    loop every enrolled host in state.json
        V->>A: Quote(nonce) over pinned mTLS
        A-->>V: signed quote + IMA log delta
        V->>V: verify AK, signature · replay IMA log · recompute PCR 10
        V->>DB: diff against approved baseline
        alt integrity failure
            V->>Out: critical finding (email / channels / SIEM)
        else new / changed measurement
            V->>DB: record in pending_review
        end
    end
    V->>DB: append every result to the hash-chained audit log

The environment file

Copy the shipped example to /etc/etminan-verifier/verifier.env and edit it. It is loaded through systemd's EnvironmentFile=; comments and blanks are fine.

sudo cp deploy/verifier.env.example /etc/etminan-verifier/verifier.env
sudoedit /etc/etminan-verifier/verifier.env

Core settings

# Where per-host enrollment state and the baseline live (defaults shown —
# they already match the systemd WorkingDirectory, so set them only to relocate).
ETMINAN_VERIFIER_STATE=/var/lib/etminan-verifier/state.json
ETMINAN_BASELINE_DB=/var/lib/etminan-verifier/baseline.db

# This verifier's own mTLS identity (generate once with keygen-tls).
ETMINAN_TLS_DIR=/var/lib/etminan-verifier/tls

# How long a pending_review item may sit before run / baseline review
# escalate it to a warning-severity finding. Defaults to 24h if unset.
ETMINAN_PENDING_REVIEW_SLA_HOURS=24

# Destination for alarm emails. Unset disables the email leg entirely (the
# local structured log and the tamper-evident audit_log still happen).
ETMINAN_NOTIFY_EMAIL=security@example.com
ETMINAN_NOTIFY_FROM=noreply@etminan
Variable Default Purpose
ETMINAN_VERIFIER_STATE /var/lib/etminan-verifier/state.json Per-host enrollment state: address, AK fingerprint, pinned TLS fingerprint, cumulative PCR value.
ETMINAN_BASELINE_DB /var/lib/etminan-verifier/baseline.db SQLite store: approved measurements, pending review, exclusions, boot policy, and the audit log.
ETMINAN_TLS_DIR /var/lib/etminan-verifier/tls This verifier's cert.pem/key.pem.
ETMINAN_PENDING_REVIEW_SLA_HOURS 24 Review SLA before a pending item becomes a warning finding.
ETMINAN_NOTIFY_EMAIL (unset) Alarm email destination; unset disables the email leg.
ETMINAN_NOTIFY_FROM (unset) From-address for alarm email.
ETMINAN_NOTIFY_EMAIL_SEVERITIES (all) Restrict email to these severities (comma-separated).
ETMINAN_NOTIFY on Master switch. A false-y value silences all outbound notifications.
ETMINAN_NOTIFY_SUPPRESS_KINDS (none) Comma-separated finding kinds to never notify on (still recorded).
ETMINAN_NOTIFY_CHANNELS (off) Comma-separated notify-plugin channel names to enable.
ETMINAN_NOTIFY_PLUGINS_CONF /etc/etminan-verifier/notify-plugins.conf Allowlist for notify plugins.
ETMINAN_PLUGIN_USER etminan-verifier-plugin Unprivileged user each plugin subprocess is dropped to; empty opts out.
ETMINAN_CHANGE_SOURCES (off) Comma-separated change-source plugin names to enable.

These are host env-file controls

Anyone with shell access to the verifier can change verifier.env. Notification policy set here is not a signed, audited setting — making it admin-controlled and audited is a planned Enterprise feature. For a tamper-evident record of trust-changing actions, rely on the audit log, not the env file.

Registered hosts and state

You do not list hosts in the env file. Each monitored host is registered by running etminan-verifier op enroll, which pins its AK and TLS fingerprints and seeds its baseline. From then on the host lives in state.json, and every scheduled run iterates over exactly the hosts recorded there.

etminan-verifier op enroll web-01 --addr 10.0.0.5:7620 \
  --reason "onboarding web-01"

Enrollment is a signed, out-of-band-confirmed ceremony — see Enrolling a host for the full flow, including the EK credential-activation residency proof and the optional manufacturer EK-certificate requirement. state.json tracks each host's address, AK fingerprint, pinned TLS certificate fingerprint, and cumulative PCR value; baseline.db tracks approved measurements, pending items, exclusion rules, and boot policy. Enrollment itself is a trust-changing action performed through the daemon — see below; operators hold no signing key of their own.

The etminan-verifierd daemon

etminan-verifierd is the sole owner of trust-changing verifier actions and of the one Ed25519 signing key. Operators never touch that key or a key file: they run etminan-verifier op <cmd>, which connects to the daemon's Unix socket; the daemon reads the caller's kernel peer credentials (SO_PEERCRED), maps the UID to a registered identity with a role (admin / operator / viewer) and, for operators, a host-group scope, then signs the authorized action on the operator's behalf as operator:<label>. The model is default-deny / fail-closed, and every decision — allow and deny — is written to the audit log. This is Standard (free) behaviour; see Editions.

Daemon environment variables

The daemon reads these ETMINAN_VERIFIERD_* variables (put them in verifier.env or a daemon drop-in):

# The peer-credentialed socket op clients connect to.
ETMINAN_VERIFIERD_SOCKET=/run/etminan-verifierd/etminan-verifierd.sock

# The baseline DB — also holds the UID→identity registry and the audit log.
ETMINAN_VERIFIERD_DB=/var/lib/etminan-verifier/baseline.db

# The ONLY Ed25519 signing key; the daemon signs operator actions with it.
ETMINAN_VERIFIERD_KEY=/var/lib/etminan-verifier/daemon-signing.key

# TOTP session lifetime once a role requires a second factor (8h default).
ETMINAN_TOTP_SESSION_TTL_SECS=28800
Variable Default Purpose
ETMINAN_VERIFIERD_SOCKET /run/etminan-verifierd/etminan-verifierd.sock Unix socket op clients connect to; peer-UID is read from it.
ETMINAN_VERIFIERD_DB /var/lib/etminan-verifier/baseline.db Baseline DB, UID→identity registry, and audit log.
ETMINAN_VERIFIERD_KEY /var/lib/etminan-verifier/daemon-signing.key The one Ed25519 signing key (mode 0600); custody-critical.
ETMINAN_TOTP_SESSION_TTL_SECS 28800 (8h) Lifetime of an op login TOTP session.
ETMINAN_VERIFIERD_ALLOW_INSECURE_BOOTSTRAP (unset) DEV/TEST ONLY — relaxes op bootstrap preconditions. Never set in production.

daemon-signing.key is custody-critical

ETMINAN_VERIFIERD_KEY (/var/lib/etminan-verifier/daemon-signing.key, mode 0600) is the only key that produces the daemon's operator-action signatures. Losing it means past daemon signatures cannot be reproduced — back it up alongside baseline.db. See Backup & restore.

The daemon systemd unit

etminan-verifierd.service is a long-lived service (unlike the oneshot etminan-verifier.timer), running as the same unprivileged user and providing the socket under a RuntimeDirectory:

[Service]
Type=notify
User=etminan-verifier
Group=etminan-verifier
WorkingDirectory=/var/lib/etminan-verifier
EnvironmentFile=-/etc/etminan-verifier/verifier.env
ExecStart=/usr/bin/etminan-verifierd
RuntimeDirectory=etminan-verifierd
NoNewPrivileges=true
sudo systemctl enable --now etminan-verifierd.service
etminan-verifier op ping        # confirm the socket is answering

Bootstrapping the first admin

The UID→identity registry starts empty, so seed the first admin once, as root, with op bootstrap; after that, admins add operators with op identity add — no key files involved:

# One-time genesis (root); installs UID 1000 as the first admin:
sudo etminan-verifier op bootstrap --uid 1000 --label alice

# Thereafter, an admin maps day-2 operators (scoped to a host group):
etminan-verifier op identity add --uid 1007 --role operator \
  --scope 'region-a-*' --label bob

Never relax bootstrap in production

ETMINAN_VERIFIERD_ALLOW_INSECURE_BOOTSTRAP exists only for DEV/TEST and must stay unset in production — with it set, the one-time genesis preconditions no longer hold.

From then on, day-2 operator actions — enrolling a host, approving a host's pending baseline — go through op and are authorized by UID, not by any key file:

etminan-verifier op enroll web-01 --addr 10.0.0.5:7620 --reason "onboarding web-01"
etminan-verifier op approve web-01

Attestation intervals

Scheduling is systemd's job, not an env var. The verifier is a Type=oneshot service invoked by a timer:

sudo systemctl enable --now etminan-verifier.timer

etminan-verifier.timer runs etminan-verifier run hourly:

[Timer]
OnBootSec=10min
OnUnitActiveSec=1h
Persistent=true

Each run checks every enrolled host in one pass and fires the alarm path if anything is wrong. To change cadence, edit the timer (e.g. with systemctl edit etminan-verifier.timer); to check a single host interactively without the timer:

etminan-verifier check --host web-01 --addr 10.0.0.5:7620

See The attestation loop for exactly what each cycle does — AK and signature checks, the load-bearing IMA-log-replay-to- PCR-10 binding, baseline diffing, SLA escalation, and per-cycle audit-chain and plugin verification.

Where findings go

The verifier is the single place an integrity finding leaves the box. The outputs, from always-on to opt-in:

  • The local structured log and the tamper-evident audit_log (inside baseline.db) always happen — they are unaffected by ETMINAN_NOTIFY and need no external tool or key.
  • Email — set ETMINAN_NOTIFY_EMAIL (and optionally ETMINAN_NOTIFY_FROM / ETMINAN_NOTIFY_EMAIL_SEVERITIES). Unset disables just the email leg.
  • Notify plugins (PagerDuty, Slack, generic webhook, or your own) via ETMINAN_NOTIFY_CHANNELS plus an allowlist in notify-plugins.conf. The verifier has no in-process HTTP client — every channel is a separately-vetted, root-owned, SHA-256-pinned script. See Notification channels.
  • SIEM output (CEF/LEEF over syslog) — Enterprise only; see below.

Confirm whatever you enable actually works:

etminan-verifier plugins verify
etminan-verifier notify-test

Enterprise edition

SIEM output (ETMINAN_SIEM_FORMAT=cef|leef, ETMINAN_SIEM_SYSLOG_ADDR) emits one syslog event per finding and per signed operator action, for a SOC ingesting into Splunk / QRadar / Sentinel / Elastic. It is compiled out of the free Standard binary — the variables are read by nothing there and setting them has no effect. Available only in an enterprise-feature build. See SIEM output.

Plugin execution sandboxing

Every change-source and notify plugin runs as a separate, unprivileged etminan-verifier-plugin user (default), so an allowlisted-but-later-compromised plugin still can't read baseline.db or the daemon signing key. This is defense in depth on top of the existing integrity checks (allowlist, root-owned, pinned SHA-256), not a replacement.

From the .deb, postinst already creates the user and the etminan-verifier.service unit already carries the AmbientCapabilities=CAP_SETUID CAP_SETGID grant that makes the privilege drop possible. Confirm both:

getent passwd etminan-verifier-plugin
systemctl show etminan-verifier.service -p AmbientCapabilities

Set ETMINAN_PLUGIN_USER= (explicitly empty) to opt out — rarely wanted outside local development. See Plugin API.

The systemd unit

etminan-verifier.service runs as a non-root user and does the full checked-every-host pass once per invocation:

[Service]
Type=oneshot
User=etminan-verifier
Group=etminan-verifier
WorkingDirectory=/var/lib/etminan-verifier
EnvironmentFile=-/etc/etminan-verifier/verifier.env
ExecStart=/usr/bin/etminan-verifier run
AmbientCapabilities=CAP_SETUID CAP_SETGID
CapabilityBoundingSet=CAP_SETUID CAP_SETGID
NoNewPrivileges=true

Key file and state locations

Path What it holds
/etc/etminan-verifier/verifier.env The environment file (this chapter).
/var/lib/etminan-verifier/daemon-signing.key The daemon's one Ed25519 signing key (0600) — custody-critical, back up with the DB.
/run/etminan-verifierd/etminan-verifierd.sock The daemon's peer-credentialed op socket (runtime; recreated on start).
/var/lib/etminan-verifier/state.json Per-host enrollment state.
/var/lib/etminan-verifier/baseline.db Baseline, pending, exclusions, boot policy, UID→identity registry, audit log (0600).
/var/lib/etminan-verifier/tls/cert.pem · key.pem This verifier's mTLS identity.
/etc/etminan-verifier/notify-plugins.conf Notify-plugin allowlist.
/etc/etminan-verifier/change-source-plugins.conf Change-source allowlist.

Protect the key and database

Neither daemon-signing.key nor baseline.db should be readable by etminan-verifier-plugin. In practice this already holds — the key is 0600 and baseline.db is created 0600 owned by etminan-verifier, with the plugin user deliberately in a separate group. If you loosened permissions while debugging, tighten them back.

Verifying the configuration

There is no self-check subcommand. Confirm the daemon is answering and that the signed state is intact:

etminan-verifier op ping                 # daemon socket is up
etminan-verifier op verify-signatures    # audit-log chain + signed state

op verify-signatures re-verifies the hash-chained audit log, every signed baseline action, and their authorization against the recorded identities, exiting non-zero if any signature, chain, or authorization check fails. Confirm the notify/SIEM config with etminan-verifier notify-test and etminan-verifier plugins verify. See Troubleshooting.

Because the verifier is the single place an alarm can fire, silence looks exactly like "all healthy" — so also stand up an independent heartbeat on a separate system. See Hardening the deployment.

Next steps