Skip to content

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 in state.json alongside the AK fingerprint. Every connection after that is pinned.
  • The agent pins the verifier's certificate statically, via ETMINAN_VERIFIER_CERT_FINGERPRINT in its agent.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:

etminan-agent keygen-tls

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:

etminan-verifier keygen-tls

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:

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

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:

# on the verifier
etminan-verifier keygen-tls

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:

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

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.jsontls_cert_fingerprint_sha256_hex Agent fingerprint the verifier pins per host.

Next steps