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 op check 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 op review 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 op approve web-01
# goes through the RBAC daemon — no key file; the daemon signs it, authenticated by your UID

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

Four binaries, one Cargo workspace.

A single host is never both agent and verifier — the verifier's trust value comes from being administratively separate from the hosts it checks. On the verifier side two processes run: the etminan-verifier CLI and the etminan-verifierd RBAC daemon, which owns the state and the signing key. The reasoning is applied once more to the verifier itself — what it says about its own integrity is checked by etminan-witness on a third machine.

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 trust-changing action goes through the RBAC daemon (etminan-verifierd) — operators run etminan-verifier op <cmd>, are authenticated by their Unix UID (no key files), and the daemon signs the action into the tamper-evident audit log.
  • 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.
Runs beside the verifier

etminan-verifierd

The RBAC daemon, and the only process that touches verifier state or the signing key. Operators reach it as etminan-verifier op <cmd> and hold no key themselves.

  • Authenticated by kernel UIDSO_PEERCRED, unforgeable for a non-root peer, against a default-deny role and host scope.
  • Signs on the operator's behalf — an operator with a shell on the verifier still cannot sign a decision the daemon would refuse.
  • Roles may come from a directory — an AD or LDAP group, off until one is mapped, and never consulted by an installation that maps none.
Runs on a third machine

etminan-witness

The verifier does not check itself. A verifier that has been taken over keeps signing perfectly well, and will hand you a fresh, valid summary over a frozen audit head — so the evidence it produces about its own integrity is checked somewhere else.

  • Its own package — no daemon, no database, no signing key, no TPM, no service user. Nobody puts a second verifier on a mail host to read one line of JSON.
  • Two commands — check a signed integrity summary, or an exported evidence bundle, against the published key.
  • Raises an alarm rather than failing quietly — a head that went backwards, a summary seen twice, or one signed by somebody else is a finding, not a warning.
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 op 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 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, built directly against RT's REST2 API — not yet verified against a live instance, so treat it as a working starting point to test against your own deployment, not a production guarantee out of the box.

  • 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 ships today as a reference plugin under the same Plugin API as change correlation — built against PagerDuty's documented Events API, not yet verified against a live account.

  • 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.
  • A broken or misconfigured plugin is never a silent failure — etminan-verifier plugins verify checks every configured plugin on demand, turning it into an ordinary warning-severity finding.

These are one example each — see the Plugin Directory for every plugin shipped today (FreeITSM, Slack, and a generic webhook too), each with an honest status, not a blanket "supported" claim. Writing one for your own ticket system or paging tool is the same recipe — see writing a new plugin — and contributions are welcome.

Editions

Free and self-hosted — with an Enterprise edition for the SOC.

Etminan ships as a free Standard edition with everything core, and an Enterprise edition that adds SIEM output and dual-control approvals for larger, regulated deployments — opt-in and off by default. The Enterprise features are in development and not yet published. See the full side-by-side editions comparison on the Enterprise page.

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. Enable the IMA-policy unit now; start
# etminan-agent.service only AFTER writing agent.env (next step).
sudo systemctl enable --now etminan-agent-ima-policy.service
# ... configure agent.env, then:
sudo systemctl enable --now etminan-agent.service

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

Configure the agent host (before starting it)

Copy deploy/agent.env.example to /etc/etminan-agent/agent.env before you start the agent service. 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. And with no verifier fingerprint pinned yet, the agent refuses to start unless you opt into the enrollment bootstrap with ETMINAN_ALLOW_PERMISSIVE_TLS=1 — after enrolling (below) you pin ETMINAN_VERIFIER_CERT_FINGERPRINT and remove that flag.

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

# first boot, before you know the verifier's fingerprint: opt into the
# enrollment bootstrap. Remove this and pin the fingerprint after enrolling.
ETMINAN_ALLOW_PERMISSIVE_TLS=1
#ETMINAN_VERIFIER_CERT_FINGERPRINT=<sha256 hex from `etminan-verifier keygen-tls`>

# 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

Start the RBAC daemon and mint mTLS identities

Operators hold no signing key: the etminan-verifierd RBAC daemon holds the single signing key and signs on their behalf. Start it, bootstrap the first admin by Unix UID, and give each device its mandatory-mTLS identity (the agent↔verifier channel is mTLS-only, no plaintext fallback).

# on the verifier — start the RBAC daemon, then bootstrap the first admin (as root)
sudo systemctl enable --now etminan-verifierd.service
sudo etminan-verifier op bootstrap --uid 1000 --label "admin1"

# 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, proves the AK lives in a real TPM via an EK credential-activation challenge (refusing a software-generated key), and prints the AK fingerprint — confirm it out-of-band (e.g. read it off the host's console) before trusting it. The residency proof establishes it's a genuine TPM; this out-of-band step confirms it's the right host's. On discrete hardware you can also add --ek-roots <dir> to require the TPM's manufacturer EK certificate chain, proving genuine hardware rather than only same-TPM residency.

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

Approve the initial baseline

Every measurement from the first quote lands as a pending candidate, never auto-trusted. Review it once, then approve it through the RBAC daemon. Which operators may approve is set by role and scope in the daemon's identity registry; the daemon authenticates you by your Unix UID and signs the approval itself — there is no operator key file to hold. See the operator guide.

etminan-verifier op review web-01

etminan-verifier op approve web-01
Downloads

Signed packages, one per confinement layer.

Hosted directly from this site for the moment — every Standard release is a GPG-signed native .deb and .rpm package. Always verify the signature before installing; never trust an unsigned package.

Beta

etminan

etminan-agent, etminan-verifier and its privilege-separated daemon etminan-verifierd — three binaries from one source, one version (the daemon ships inside the verifier package).

  • Package name: etminan
  • Standard: signed .deb + .rpm
  • Enterprise: .deb / .rpm by arrangement — not downloaded here
Packaged — publish pending

AppArmor profiles

Mandatory access control — one profile for each of the three binaries (the agent, the verifier, and its daemon). Packaged for Debian with release CI; no signed package is published here yet — build via a tagged release, then drop the .deb/.rpm into downloads.

  • apparmor-etminan-agent
  • apparmor-etminan-verifier
  • apparmor-etminan-verifierd
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)
  • selinux-etminan-verifierd (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 <package>.deb.asc <package>.deb # or <package>.rpm
$ sha256sum -c <package>.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.

Open a ticket at our support desk and follow it through to resolution — questions about deploying Etminan, feedback on the design, or a security report each have their own queue. Prefer email? Reach us directly.

Reporting something security-sensitive? Submit it privately through the write-only security intake — or email it encrypted 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).