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.
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:
etminan-verifier.timer runs etminan-verifier run hourly:
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:
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(insidebaseline.db) always happen — they are unaffected byETMINAN_NOTIFYand need no external tool or key. - Email — set
ETMINAN_NOTIFY_EMAIL(and optionallyETMINAN_NOTIFY_FROM/ETMINAN_NOTIFY_EMAIL_SEVERITIES). Unset disables just the email leg. - Notify plugins (PagerDuty, Slack, generic webhook, or your own) via
ETMINAN_NOTIFY_CHANNELSplus an allowlist innotify-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:
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¶
- Stand up the transport both binaries require: Mutual TLS.
- Configure the monitored side: Agent configuration.
- Enrol your first host: Enrolling a host.
- Understand each polling cycle: The attestation loop.