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:
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:
ETMINAN_WATCHED_PATHS— a direct comma-separated list inlined in this host'sagent.env. Wins over everything. Use it for a one-off host that fits no shared profile.
-
A verifier-pushed assigned profile — a profile name centrally assigned with
op assign-profile, persisted locally inETMINAN_ASSIGNED_PROFILE_PATH. Overridden byETMINAN_WATCHED_PATHS, but takes precedence overETMINAN_PROFILE. Only the selected name is ever written or sent over the wire — the path list itself always stays in the agent's ownprofiles.conf. -
ETMINAN_PROFILE=<name>— looks<name>up in the profiles file so many hosts can share one reusable named path set.
profiles.conf is a hand-rolled [name] + one-path-per-line format (no
TOML/YAML); # comments and blank lines are ignored:
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 theCAP_DAC_READ_SEARCHalready 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:
- Re-derives its Attestation Key (AK) from a fixed, deterministic creation
template via
TPM2_CreatePrimary— no persistent key storage is needed. - Requests a
TPM2_Quoteover PCR 10 in the SHA-256 bank, with the nonce as the quote'squalifyingData. - 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:
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=tssgrants access to/dev/tpmrm0.AmbientCapabilities=CAP_DAC_READ_SEARCHis the narrowest capability that lets the non-root agent read the IMA measurement log, which the kernel keepsroot:root 0440by design — no group membership can grant read access to it. This same grant is what unprivileged fanotify write detection runs under; noCAP_SYS_ADMINis 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):
Confirm the essentials by hand as well:
/dev/tpmrm0exists and is openable by theetminan-agentuser (grouptss);- IMA is enabled and measuring —
wc -l /sys/kernel/security/ima/ascii_runtime_measurements_sha256is non-zero; CAP_DAC_READ_SEARCHis granted —systemctl show etminan-agent.service -p AmbientCapabilities.
Next steps¶
- Stand up the mandatory transport: Mutual TLS.
- Configure the other half of the deployment: Verifier configuration.
- Bring this host under attestation: Enrolling a host.