etminan
TPM 2.0 + Linux IMA host-integrity attestation

Attestation that survives a root compromise.

Etminan measures every monitored host from the kernel, seals the result in a TPM, and hands the verdict to a separate device that the host can never reach. If root is compromised, it can refuse to answer — it cannot lie and get away with it.

etminan-agent · monitored host etminan-verifier · independent device Ed25519-signed baselines
The problem

Self-checking security tools have a structural flaw.

AIDE, Tripwire, and auditd-based integrity checks all share the same weak point: the checksum database and the alarm live on the box being watched.

Under a full-root-compromise threat model, that's fatal. A userspace agent alone can't be trusted here — root can patch the hashing binary in memory, edit the baseline it compares against, or simply lie about the result. The alarm never fires, because the thing that would fire it is the thing that got compromised.

Etminan is built to answer that threat model directly, not to catch a wider net of generic issues. Measurements have to come from somewhere root cannot forge after the fact — that means the kernel's IMA subsystem extending a TPM PCR, and a signed, remotely-verified quote checked by a device the compromised host never touches.

$ vim /opt/checker/baseline.db
# root can edit the file the checker trusts
$ ./run-integrity-check
OK — no changes detected
# ...because root also owns the thing doing the checking
$ etminan-verifier check --host web-01
FINDING: PCR digest mismatch — signed quote disagrees with replayed log
# the AK private key never left the TPM; root can't forge this
How it works

Three steps, two trust domains.

The monitored host never judges itself. Every decision — pass, fail, or "needs a human" — is made on hardware the host cannot reach.

01 / measure

The kernel measures, not an agent.

Linux IMA measures file executions and reads under policy and extends the results into TPM PCR 10 — a one-way operation even root cannot rewrite after the fact.

02 / verify

A separate device checks the math.

The verifier pulls a signed TPM2_Quote on its own schedule, replays the host's IMA log, and confirms it matches the signed digest. A compromised host can stay silent — it cannot fabricate a validly signed quote.

03 / approve

Drift is a candidate, never an event.

Any change from the last approved baseline lands in review. It only becomes trusted after an explicit, signed operator approval with a required reason.

See it in action

What day-to-day actually looks like.

Two things every operator sees: the plain-language verdict on a pending change, and exactly what fires when something's actually wrong.

Reviewing a pending change

Every new or changed measurement lands here first — a plain-language read before the raw hash-level detail, so a generalist operator can act on the first line.

$ etminan-verifier baseline review --host web-01
[42] host=web-01 path=/usr/local/bin/fds-proxy
Likely a legitimate install — matches package fds-proxy v2.3.1.
Matches change RT#4821 (resolved) via request-tracker: 'Patch Tuesday rollout'.
✓ installed via an authenticated apt transaction at 2026-07-21 10:00:00.
detail: sha256=3af9...c2 (was a1b2...) owner=uid:0/gid:0 mode=0755 size=8421312
mtime=2026-07-21T10:00:04Z first_seen=2026-07-21T10:15:00Z
$ etminan-verifier baseline approve --host web-01 \
--key operator.key --reason "patch Tuesday rollout"

When something's actually wrong

A genuine finding never waits for a human to notice — it's printed, signed into the audit log, and pushed to every configured channel in the same pass.

$ etminan-verifier run
FINDING [critical] host=db-03
AK fingerprint mismatch — presented key does not match enrollment record
FINDING [warning] host=web-02
2 pending item(s) aged past the 24h review SLA — [SLA EXCEEDED]
# same finding, as it lands in your inbox:
Subject: [etminan] CRITICAL — db-03 AK fingerprint mismatch
A host is presenting an attestation key that does not match its
enrollment record. This can mean the TPM was replaced, the host was
reimaged, or something more concerning. Re-enroll only after
confirming the cause out-of-band.
# and, if configured, the same event on PagerDuty/Slack/webhook —
# never a second-class channel, checked for breakage every cycle.
Components

Two binaries, one Cargo workspace.

A single host is never both — the verifier's trust value comes from being administratively separate from the hosts it checks.

Runs on the monitored host

etminan-agent

A relay, not a trust anchor. On request, it sweeps in-scope paths, requests a signed TPM quote, and returns it with the IMA log delta.

  • Needs a TPM — physical or vTPM, plus kernel IMA enabled.
  • Makes no pass/fail decision and sends no alerts — that's the verifier's job.
  • Watched paths are config, not code — a named profile or a direct path list, drawn from a centralized, verifier-pushed profile registry, never a hardcoded default.
  • Best-effort package lookup (dpkg/rpm) on a separate connection, never mixed with quote-taking.
Runs on a separate device

etminan-verifier

No TPM required — quote verification is pure offline signature math against the enrolled AK public key. Not just no runtime device access: zero TPM library dependency in the shipped binary, which cross-compiles clean to a static musl target. The only place an alarm ever fires from.

  • Polls hosts hourly, replays each IMA log, diffs against the approved baseline.
  • Plain-language verdicts — e.g. "Likely a legitimate install — matches package nginx v1.24.0" — so a generalist operator can act.
  • Every approval is signed and requires an operator --reason, folded into the tamper-evident batch payload.
  • Correlates against change/ticket systems (Request Tracker today, pluggable for more) — flags whether a pending change matches a known approved change window.
  • Fires the alarm on any mismatch, silence, or rejection — a self-contained, hash-chained audit log, email, and PagerDuty/Slack/webhook via the Etminan Plugin API, no external tool or key required.
Integrations

A pending change gets more context, from more sources.

Two shapes, chosen deliberately per integration. Package-manager checks are an in-process Rust trait — a closed, well-known set of package managers. Change-ticket systems and alert channels are external plugins under the shared Etminan Plugin API — nobody is switching ticket systems or paging tools for an attestation tool, so the verifier ships zero in-process HTTP clients for either and instead runs a separately-vetted, allowlisted, SHA-256-pinned script per integration.

Shipped

Package ownership

Does an installed package own the changed path? Checked against dpkg/rpm on a dedicated, non-quote-taking connection.

  • Feeds the plain-language verdict in baseline review.
  • Pluggable PackageChecker trait — apk/pacman are a new impl away.
  • Also reports install/upgrade timing (dpkg log / rpmdb) and trust-store verification — a real rpm GPG signature check, or dpkg's weaker "authenticated apt transaction" signal, called out as weaker rather than blurred together — so "a package update explains this" is backed by evidence, not just plausibility.
Shipped

Change/ticket-system correlation

Was this change actually planned? The verifier runs every enabled plugin and shows every confirmation it finds — never just the first match.

  • Request Tracker ships today as a reference plugin via its REST2 API.
  • External plugin process, not a hardcoded client — a ServiceNow/Jira/GitLab Issues plugin is a new script and an allowlist entry, no verifier rebuild.
  • Purely informational — never gates or auto-approves a baseline change.
Shipped

Alarm / notification channels

Beyond the self-contained, hash-chained audit log and email: PagerDuty, Slack, and a generic webhook ship as reference plugins under the same Plugin API as change-source correlation.

  • Findings are piped as JSON over the plugin's stdin — never argv — so free-form finding text is never shell-escaped into a command line.
  • Per-channel severity filtering: a review-backlog notice doesn't have to page anyone, a genuine tamper finding can.
  • A broken or misconfigured plugin is never a silent failure — etminan-verifier plugins verify checks every configured plugin on demand, and run does the same automatically every cycle, turning it into an ordinary warning-severity finding.
Install & configure

From a fresh host to a verified baseline.

The short path below covers the common case. For profiles, package-manager plugins, and day-to-day review, see the full user guide.

Install the package

The postinst step creates the etminan-agent (group tss) and etminan-verifier system users and their config/state directories — but enables nothing. You choose which role(s) this host plays.

# install the package
sudo dpkg -i etminan_<version>_amd64.deb

# monitored host — needs a TPM
sudo systemctl enable --now etminan-agent-ima-policy.service etminan-agent.service

# verifier device
sudo systemctl enable --now etminan-verifier.timer

Configure the agent host

Copy deploy/agent.env.example to /etc/etminan-agent/agent.env. There is no baked-in default for watched paths — an agent started with neither setting watches nothing and warns loudly on startup, rather than silently assuming another project's file list.

# listen address for quote requests from the verifier
ETMINAN_AGENT_LISTEN=0.0.0.0:7620

# pick ONE: a named profile from profiles.conf, or an inline override
ETMINAN_PROFILE=fds-host
#ETMINAN_WATCHED_PATHS=/usr/local/bin/myapp,/etc/myapp

Configure the verifier

Copy deploy/verifier.env.example to /etc/etminan-verifier/verifier.env and point it at where enrollment state and the baseline database should live.

ETMINAN_VERIFIER_STATE=/var/lib/etminan-verifier/state.json
ETMINAN_BASELINE_DB=/var/lib/etminan-verifier/baseline.db
ETMINAN_NOTIFY_EMAIL=security@example.com   # unset disables the email alarm leg

Generate keys

Two independent key pairs: an operator signing key (makes every baseline approval tamper-evident) and a per-device mTLS identity (the agent↔verifier channel is mandatory mTLS, no plaintext fallback).

# on the verifier — operator signing key, keep it safe
etminan-verifier keygen --out /etc/etminan-verifier/operator.key

# on both the agent and the verifier — mTLS identity
etminan-agent keygen-tls
etminan-verifier keygen-tls

Enroll a host

From the verifier, for each monitored host. This takes a first quote and prints the AK fingerprint — confirm it out-of-band (e.g. read it off the host's console) before trusting it. It's the one trust-on-first-use moment in the whole design.

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

Approve the initial baseline

Every measurement from the first quote lands as a pending candidate, never auto-trusted. Review it once, then approve as a single signed batch with a reason.

etminan-verifier baseline review --host web-01

etminan-verifier baseline approve --host web-01 \
  --key /etc/etminan-verifier/operator.key \
  --reason "initial baseline for web-01"
Downloads

Signed packages, one per confinement layer.

Hosted directly from this site for the moment — every release is a GPG-signed tar.gz (plus a .deb where packaging exists). Always verify the signature before installing; never trust an unsigned tarball.

Beta

etminan

etminan-agent + etminan-verifier, one package, one version.

  • Package name: etminan
  • .deb and signed tar.gz
Not yet packaged

AppArmor profiles

Mandatory access control for both binaries — built and scaffolded locally, not yet built into a signed package or dropped in here.

  • apparmor-etminan-agent
  • apparmor-etminan-verifier
Planned

SELinux policy

Not started yet. Tracked as a follow-on once the AppArmor packages are published and the policy-authoring pass has a go/no-go.

  • selinux-etminan-agent (planned name)
  • selinux-etminan-verifier (planned name)
# fetch the key from a keyserver — or import downloads/gpg-etminan-team.asc offline
$ gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x73874214090F9137862D0AF1E1E541B7B42436DF
$ gpg --fingerprint team@etminan.dev
7387 4214 090F 9137 862D 0AF1 E1E5 41B7 B424 36DF
# verify any package before installing it — never skip this
$ gpg --verify <archive>.tar.gz.asc <archive>.tar.gz
$ sha256sum -c <archive>.tar.gz.sha256
Etminan Team <team@etminan.dev> — packages hosted directly from this site use this key, not the shared org-wide key.
Contact

Questions, feedback, or a security report.

There's no ticketing system on this site — just email. Questions about deploying Etminan, feedback on the design, or a security report all go to the same place.

✉  Click to reveal & email us

Reporting something security-sensitive? Encrypt it with the same key packages on this site are signed with — 7387 4214 090F 9137 862D 0AF1 E1E5 41B7 B424 36DF, fetchable from hkps://keys.openpgp.org (see the Downloads section above).