Mutual TLS¶
Every agent↔verifier connection is mutual TLS, mandatory, pinned by certificate fingerprint, with no plaintext fallback and no CA to manage — this chapter walks through generating and placing the keypairs and the trust ceremony that ties the two sides together.
Neither binary does anything useful without a keypair: both refuse to start until one exists. The transport is a closed two-party channel between one verifier and each of its enrolled agents — analogous to SSH host-key pinning, not a public CA-validated HTTPS service.
Why pinning, not a CA
This extends Etminan's existing trust-on-first-use pattern — the TPM AK fingerprint pinned at enrollment and confirmed out-of-band by a human — to the transport layer, rather than standing up a private CA. Both directions are authenticated: the agent (TLS server) authenticates the verifier (TLS client) too, because a verifier-to-agent config push (assigning a profile) has no other way to prove a command came from the legitimate verifier.
The trust model¶
flowchart TB
subgraph agentbox["Agent host"]
acert["cert.pem / key.pem<br/>/var/lib/etminan-agent/tls"]
avar["agent.env:<br/>ETMINAN_VERIFIER_CERT_FINGERPRINT"]
end
subgraph verifierbox["Verifier device"]
vcert["cert.pem / key.pem<br/>/var/lib/etminan-verifier/tls"]
vstate["state.json:<br/>tls_cert_fingerprint per host"]
end
acert -- "agent fingerprint,<br/>pinned via TOFU at enroll" --> vstate
vcert -- "verifier fingerprint,<br/>pinned manually in env" --> avar
- The verifier pins each agent's certificate by trust-on-first-use during
enroll, storing the captured fingerprint instate.jsonalongside the AK fingerprint. Every connection after that is pinned. - The agent pins the verifier's certificate statically, via
ETMINAN_VERIFIER_CERT_FINGERPRINTin itsagent.env.
A mismatch on either side aborts the handshake with a fingerprint-mismatch error
— it never falls back to CA validation. The stack is rustls with the ring
crypto provider and self-signed rcgen keypairs; the whole implementation lives
in the common crate and is exposed only through opaque type aliases, so
neither agent nor verifier depends on rustls/rcgen directly.
Always TLS-wrapped, compiler-enforced
Every real message call site goes through the sealed-trait
write_message_tls / read_message_tls in the common crate. The
TlsTransport trait is sealed so only the crate's own TLS stream types can
implement it — nothing outside can pass a bare TcpStream through, so
"always TLS-wrapped" is enforced by the compiler, not by convention.
Step by step¶
1. Generate the agent's keypair¶
On each monitored host, once:
This writes /var/lib/etminan-agent/tls/{cert,key}.pem (or $ETMINAN_TLS_DIR
if set) and prints the certificate's SHA-256 fingerprint. key.pem is created
pre-restricted at mode 0600 — it is never written world-readable and then
chmod'd. There is no auto-generation on first use; this is a deliberate,
auditable step.
2. Generate the verifier's keypair¶
On the verifier device, once:
This prints the verifier's fingerprint. Copy it into
ETMINAN_VERIFIER_CERT_FINGERPRINT in every agent host's agent.env this
verifier will talk to:
# /etc/etminan-agent/agent.env
ETMINAN_VERIFIER_CERT_FINGERPRINT=<sha256 hex from etminan-verifier keygen-tls>
A missing fingerprint fails loudly, not open
Accepting any client certificate is not the silent fallback for a
missing fingerprint. An agent started without
ETMINAN_VERIFIER_CERT_FINGERPRINT refuses to start unless you also set
ETMINAN_ALLOW_PERMISSIVE_TLS=1. That flag is the deliberate first-boot
enrollment window — see below.
3. The first-boot enrollment window¶
Before you know the verifier's fingerprint (the very first enrollment), the agent must accept a client certificate from any caller — still real mTLS, just not pinned. Opt in explicitly:
# /etc/etminan-agent/agent.env — first boot only
ETMINAN_ALLOW_PERMISSIVE_TLS=1
# leave ETMINAN_VERIFIER_CERT_FINGERPRINT unset for now
In this mode the agent serves only Quote (for enrollment), never advances
the IMA-log cursor, and refuses SetProfile and PackageLookup. Once you have
enrolled and confirmed the fingerprint, set
ETMINAN_VERIFIER_CERT_FINGERPRINT and remove the flag.
4. Pin the agent's certificate at enrollment¶
The agent's own certificate is pinned the other direction via trust-on-first-use.
etminan-verifier op enroll captures the agent's certificate fingerprint on that
first connection and stores it in state.json alongside the AK fingerprint:
Confirm both the TLS fingerprint and the AK fingerprint out-of-band — e.g. read them off the host's console — before trusting the enrollment, exactly as you would an SSH host key on first connect. Every connection after that is pinned. See Enrolling a host for the full enrollment ceremony, including the EK residency proof.
The mTLS-wrapped call¶
sequenceDiagram
autonumber
participant V as etminan-verifier (TLS client)
participant A as etminan-agent (TLS server)
V->>A: TCP connect (bounded by a 10s connect timeout)
V->>A: ClientHello + client certificate
A->>A: verify client cert fingerprint == pinned verifier fingerprint
A->>V: ServerHello + server certificate
V->>V: verify server cert fingerprint == pinned agent fingerprint
Note over V,A: mutual authentication complete — both fingerprints matched
V->>A: write_message_tls(Quote(nonce))
A->>V: read_message_tls -> Quote request
A->>V: write_message_tls(QuoteResponse: signed quote + IMA log delta)
V->>V: replay log, recompute PCR 10, match the TPM signature
Either fingerprint check failing aborts the exchange before any application data flows. The handshake is driven lazily on the first read/write, so a rejection surfaces there rather than at connection setup.
Silence and slow-loris are bounded
Each read/write has a 30-second timeout; the TCP connect has its own 10-second bound; and a whole-connection watchdog force-closes a connection that trickles bytes just under every timeout, so one unreachable or slow-loris host can never wedge the verifier's whole-fleet pass. A genuinely slow-but-live exchange (a big sweep, a slow TPM quote) is never cut.
Rotating certificates¶
The two directions of pinning rotate differently, on purpose.
Rotating an agent's certificate¶
Regenerate the keypair on the host, then re-pin it from the verifier — this is not a full re-enroll; the AK trust and approved baseline are untouched:
# on the agent host
etminan-agent keygen-tls
# on the verifier
etminan-verifier op rotate-tls web-01 \
--reason "rotated agent TLS keypair"
op rotate-tls TOFU-captures the new certificate (the new fingerprint isn't known
yet), but continues an already-trusted host: the connecting host's AK
fingerprint must still match what's on record (the same physical TPM — a
rotated transport cert must never sneak a different host past AK validation),
and the PCR 10 replay continues from the stored cumulative value. Only the
pinned TLS fingerprint changes, signed and recorded in the audit log.
Rotating the verifier's certificate¶
This is a deliberately manual procedure — there is no dual-fingerprint transition window:
Then update ETMINAN_VERIFIER_CERT_FINGERPRINT in every agent's agent.env
and restart each agent — in that order.
Order matters
Restarting an agent before its env is updated locks that agent out until it is fixed. Update the env value first, then restart. There is no accept-old-or-new transition window by design.
Verifying the setup¶
There is no self-check subcommand; exercise the transport instead. Each
keygen-tls prints the fingerprint you pinned, and a real attestation over the
pinned channel is the end-to-end confirmation that both directions match:
If a host reads as unreachable while the agent is actually up, the likeliest
cause is a TLS fingerprint mismatch — the pinned verifier refusing the
handshake. Check whether the agent's keypair was regenerated without a matching
op rotate-tls, confirm the new certificate is legitimate out-of-band, then run
op rotate-tls. See Troubleshooting.
Key file locations¶
| Path | What it holds |
|---|---|
/var/lib/etminan-agent/tls/cert.pem · key.pem |
Agent's mTLS identity (key.pem 0600). |
/var/lib/etminan-verifier/tls/cert.pem · key.pem |
Verifier's mTLS identity. |
ETMINAN_VERIFIER_CERT_FINGERPRINT (in agent.env) |
Verifier fingerprint the agent pins. |
state.json → tls_cert_fingerprint_sha256_hex |
Agent fingerprint the verifier pins per host. |
Next steps¶
- Configure the two roles: Agent configuration · Verifier configuration.
- Perform the enrollment that pins the agent cert: Enrolling a host.
- Understand the calls this transport carries: The attestation loop.