Skip to content

Agent configuration

etminan-agent is a relay: it measures the host's files into the TPM, hands the verifier a TPM-signed quote plus the raw IMA log, and makes no trust decision of its own — this chapter covers everything you configure on a monitored host to make that relay work.

The agent has a deliberately small configuration surface. Almost everything lives in a single environment file, /etc/etminan-agent/agent.env, loaded by systemd. There is no bespoke config-file format to learn, no in-agent policy language, and no watched-path list baked into the binary.

Where the agent fits

The agent needs a TPM 2.0 device and the kernel IMA subsystem; it never judges its own integrity. All pass/fail logic lives on a separate box — see Verifier configuration and The attestation loop. If you have not installed the two binaries yet, start with Install the agent.

What the agent does at a glance

flowchart LR
    subgraph host["Monitored host (etminan-agent)"]
        IMA["Linux IMA<br/>measures files"] -->|extends| PCR10["TPM PCR 10"]
        sweep["sweep.rs<br/>pre-cycle read sweep"] -.->|forces measurement| IMA
        watch["watch.rs<br/>fanotify write triggers"] -.->|immediate sweep| sweep
        quote["quote.rs<br/>TPM2_Quote"]
        PCR10 --> quote
    end
    verifier["etminan-verifier"] -->|"Quote(nonce) over mTLS"| quote
    quote -->|"signed quote + IMA log delta"| verifier
    verifier -->|"replays log, recomputes PCR 10,<br/>matches the signature"| decision["trust decision<br/>(on the verifier only)"]

The agent returns evidence; the verifier decides. Nothing you configure here can turn the agent into an authority on its own state — that separation is the whole point of the design.

The environment file

Copy the shipped example and edit it:

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

agent.env is a plain KEY=value file consumed through systemd's EnvironmentFile= — comments (#) and blank lines are fine. The full set of variables the agent reads:

Variable Default Purpose
ETMINAN_AGENT_LISTEN 0.0.0.0:7620 Address the agent listens on for the verifier's quote requests.
ETMINAN_TLS_DIR /var/lib/etminan-agent/tls Where this agent's own mTLS cert.pem/key.pem live (see Mutual TLS).
ETMINAN_VERIFIER_CERT_FINGERPRINT (unset) SHA-256 fingerprint of the verifier's certificate to pin. The agent refuses to start without it unless permissive mode is opted into.
ETMINAN_ALLOW_PERMISSIVE_TLS (unset) Set to 1 for the first-boot enrollment window only. See Mutual TLS.
ETMINAN_PROFILE (unset) Name of a watched-path profile from profiles.conf.
ETMINAN_PROFILES_PATH /etc/etminan-agent/profiles.conf Location of the profiles file.
ETMINAN_WATCHED_PATHS (unset) Comma-separated path list; overrides ETMINAN_PROFILE.
ETMINAN_ASSIGNED_PROFILE_PATH /var/lib/etminan-agent/assigned_profile Where a verifier-pushed profile selection is persisted.
ETMINAN_IMA_LOG_PATH /sys/kernel/security/ima/ascii_runtime_measurements_sha256 The SHA-256 IMA measurement log to read.
ETMINAN_IMA_POLICY_PATH /sys/kernel/security/ima/policy The securityfs policy write node.
TCTI (unset → /dev/tpmrm0) Standard TSS TCTI selector — override for lab/emulated-TPM use only, warned at startup.

The IMA log must be the SHA-256 variant

ETMINAN_IMA_LOG_PATH must point at ascii_runtime_measurements_sha256, not the generic ascii_runtime_measurements. The generic file uses legacy SHA1-sized template hashes regardless of which PCR bank is actually in use, which the verifier's replay cannot reconcile with the SHA-256 PCR 10 the TPM signed. The default is already correct — only override it if securityfs is mounted somewhere non-standard.

What the agent measures

The agent does not decide what to hash — Linux IMA does, according to the policy the agent loads at boot. What you configure on the agent is which paths get actively read before each attestation cycle, so a file that was written but never reopened doesn't stay invisible to IMA.

Watched paths — three ways to set them, in precedence order

There is no hardcoded watched-path list. An agent started with neither ETMINAN_WATCHED_PATHS nor ETMINAN_PROFILE set watches nothing and prints a loud startup warning — a deliberate refusal to silently fall back to some other project's paths. Every resolved path is also existence-checked at startup, with an individual warning for any that don't exist.

The three mechanisms, highest precedence first:

  1. ETMINAN_WATCHED_PATHS — a direct comma-separated list inlined in this host's agent.env. Wins over everything. Use it for a one-off host that fits no shared profile.
ETMINAN_WATCHED_PATHS=/usr/local/bin/myapp,/etc/myapp,/etc/systemd/system
  1. A verifier-pushed assigned profile — a profile name centrally assigned with op assign-profile, persisted locally in ETMINAN_ASSIGNED_PROFILE_PATH. Overridden by ETMINAN_WATCHED_PATHS, but takes precedence over ETMINAN_PROFILE. Only the selected name is ever written or sent over the wire — the path list itself always stays in the agent's own profiles.conf.

  2. ETMINAN_PROFILE=<name> — looks <name> up in the profiles file so many hosts can share one reusable named path set.

ETMINAN_PROFILE=fds-host

profiles.conf is a hand-rolled [name] + one-path-per-line format (no TOML/YAML); # comments and blank lines are ignored:

[myapp-host]
/usr/local/bin/myapp
/etc/myapp

Confirm what the agent resolved

The agent logs each resolved watched path at startup — and a distinct warning for any that don't resolve or aren't present on this host. Read it back with journalctl -u etminan-agent.service -b. See Troubleshooting.

Two detection paths, both automatic

The agent closes IMA's write-without-read gap two ways, and neither needs extra configuration beyond the watched-path list above:

  • Per-cycle sweep (sweep.rs) runs immediately before every quote, reading each regular file under the watched roots so IMA measures any pending change. This is the sole guarantee that a change is measured within one polling interval. The walk is bounded by a wall-clock budget and a total-node cap so an adversarially large or slow tree can't stall a quote.
  • Event-triggered write detection (watch.rs) additionally watches every watched path via unprivileged fanotify and triggers an immediate targeted sweep the instant a write completes — shrinking the latency from up to a full interval down to near-zero. It needs Linux 5.13+ and no capability beyond the CAP_DAC_READ_SEARCH already granted for IMA log access; on an older or fanotify-disabled kernel it logs one startup warning and falls back to per-cycle-sweep-only detection (still fully correct, just slower to notice a given write).

Both change when a measurement happens, never what is trusted: the only evidence the verifier ever checks is still IMA → TPM.

The quote / PCR 10 relay

When the verifier connects and sends a Quote request with a fresh nonce, the agent:

  1. Re-derives its Attestation Key (AK) from a fixed, deterministic creation template via TPM2_CreatePrimary — no persistent key storage is needed.
  2. Requests a TPM2_Quote over PCR 10 in the SHA-256 bank, with the nonce as the quote's qualifyingData.
  3. Returns the signed attestation structure, the signature, the AK public, and the raw IMA measurement-log delta.

The AK template must never change

The AK is derived deterministically from the TPM's owner-hierarchy seed and a fixed template (RSA-2048, RSASSA, SHA-256, restricted signing key). Changing any field of that template derives a different key and breaks the enrollment fingerprint the verifier pinned. This is why the AK uses TPM2_CreatePrimary with a constant template rather than the tpm2_createak EK-child pattern, which is genuinely non-deterministic.

Quote-taking is the only code path that touches the TPM, and it is deliberately kept free of any subprocess call to an external TPM-touching CLI — a subprocess would itself be a measured BPRM_CHECK event and could inject a spurious IMA log entry between "quote taken" and "log read". Package-ownership lookups, which do shell out, run on a separate connection (PackageLookup) and are never in the same round trip as a quote.

The TPM device

The agent always uses /dev/tpmrm0 — the resource-managed device the unprivileged tss-group user can reach — never /dev/tpm0 (the raw device, group-owned by root). The standard TSS TCTI environment variable can override this, but it is intended only for a lab or emulated TPM and is warned about loudly at startup; leave it unset on a real host.

The systemd units

The agent ships two units. The .deb postinst installs both but enables nothing — you turn on the agent role explicitly:

sudo systemctl enable --now \
  etminan-agent-ima-policy.service etminan-agent.service

etminan-agent-ima-policy.service

A Type=oneshot, RemainAfterExit=yes unit that runs etminan-agent write-ima-policy once, early at boot, before the daemon starts. IMA accepts exactly one successful policy write per boot, so the policy must be written in one shot rather than incrementally. It is idempotent: if a policy is already active this boot, that is treated as success.

etminan-agent.service

The daemon itself, running as a narrowly-privileged, non-root user:

[Service]
Type=simple
User=etminan-agent
Group=tss
SupplementaryGroups=tss
WorkingDirectory=/var/lib/etminan-agent
EnvironmentFile=-/etc/etminan-agent/agent.env
ExecStart=/usr/bin/etminan-agent
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_DAC_READ_SEARCH
CapabilityBoundingSet=CAP_DAC_READ_SEARCH
NoNewPrivileges=true

Two privilege details matter:

  • Group=tss grants access to /dev/tpmrm0.
  • AmbientCapabilities=CAP_DAC_READ_SEARCH is the narrowest capability that lets the non-root agent read the IMA measurement log, which the kernel keeps root:root 0440 by design — no group membership can grant read access to it. This same grant is what unprivileged fanotify write detection runs under; no CAP_SYS_ADMIN is ever added.

Key file locations

Path What it holds
/etc/etminan-agent/agent.env The environment file (this chapter).
/etc/etminan-agent/profiles.conf Named watched-path profiles.
/var/lib/etminan-agent/tls/cert.pem · key.pem This agent's mTLS identity (key.pem is 0600).
/var/lib/etminan-agent/assigned_profile A verifier-pushed profile selection.
/sys/kernel/security/ima/ascii_runtime_measurements_sha256 The IMA log the agent reads (kernel-owned).
/sys/kernel/security/ima/policy The one-shot policy write node (kernel-owned).
/dev/tpmrm0 The resource-managed TPM device.

Verifying the configuration

The agent has no separate self-check subcommand. Before wiring the agent into the verifier's rotation, start the service and read its startup output — it emits a distinct warning for anything misconfigured (watched paths that don't resolve, a missing verifier fingerprint, a fanotify fallback, a TCTI override):

sudo systemctl restart etminan-agent.service
journalctl -u etminan-agent.service -b

Confirm the essentials by hand as well:

  • /dev/tpmrm0 exists and is openable by the etminan-agent user (group tss);
  • IMA is enabled and measuring — wc -l /sys/kernel/security/ima/ascii_runtime_measurements_sha256 is non-zero;
  • CAP_DAC_READ_SEARCH is granted — systemctl show etminan-agent.service -p AmbientCapabilities.

Next steps