Skip to main content

Module tls

Module tls 

Source
Available on crate feature tls only.
Expand description

TLS termination, and the client certificate that goes with it.

This crate used to terminate no TLS at all, on the grounds that a second TLS stack doubles the CVE surface of a program whose job is holding other people’s secrets and that every target deployment already has a terminator. The first half of that is still true and is why TLS is opt-in twice — a Cargo feature and a [server.tls] block — but the second half was never a rule about deployments, only about the ones that had been looked at. A server on a machine with no ingress, and a deployment that wants the config server’s own socket to demand a client certificate, are both real, and neither is served by an answer that lives in somebody else’s process.

§What a client certificate is here

A second gate, not a second identity. A caller that presents a certificate signed by the configured CA gets a TCP connection and nothing else: it is still nobody until it presents a bearer token, and the token is still what names it in the audit log and what its grants hang off.

The two rejected alternatives are worth stating, because both are defensible and only one of these three can be in the code:

  • A certificate instead of a token. That makes the certificate a way to bypass the token, which is the opposite of what a second factor is for, and it moves authorisation onto a subject name — a string issued by whoever holds the CA key, which is frequently not whoever maintains this server’s roster.
  • A certificate that names a client (subject → principal). One identity, two spellings, and a second roster to keep in step with the first. Worse, it makes the CA an authorisation authority: anybody who can get a certificate with CN=billing-pod out of it reads billing, and CAs are asked for certificates by processes that have never heard of this server’s grants.

So the certificate says this connection came from a machine the deployment provisioned, and the token says this caller may read billing. Two independent facts, both required, neither able to stand in for the other. Nothing in the router changes because of TLS, and that is the property to preserve: a request that reached a handler is authorised exactly as it was before.

§Posture

Not invented here. The protocol versions are rustls’s with_safe_default_protocol_versions and the cipher suites and key exchange groups are the ring provider’s defaults, in the provider’s own preference order. This module chooses no suite, disables no version and reorders nothing — the whole reason to use rustls is that these decisions are made by people who track them, and a hand-picked list here would be this crate’s opinion frozen at the day it was written.

The one thing it does set is ALPN: http/1.1, and only that, because axum is compiled here with http1 alone. A client that negotiated h2 against a server that cannot speak it is a connection that fails after the handshake instead of during it.

§Revocation, and why there is none

A certificate that chains to client_ca is good until it expires. This module configures no CRL, and [server.tls] crl is a startup refusal (Refusal::RevocationUnsupported) rather than a key that is read — because a decorative revocation check is worse than an acknowledged absence, and this one would be decorative.

rustls has the machinery, and it is about twenty lines: ClientCertVerifierBuilder::with_crls, a revocation-check depth and an unknown-status policy. What sank it is not the code but the freshness, and both halves were measured rather than assumed (the measurement is tests/tls.rs::the_measurement_behind_refusing_revocation_still_holds, which fails if either default moves):

  • By default a stale CRL is used, silently. rustls’s ExpirationPolicy::Ignore means a list whose nextUpdate passed in 2020 still verifies a handshake in 2026 with no error, no warning and nothing in any log. So the naive build is a server that reports it checks revocation and, from whenever the file stopped being refreshed, does not. Worse, it tests green: the obvious test — revoke a certificate, assert the handshake fails — passes against a six-year-old list, because revocation itself keeps working. Only freshness rots, and nothing observes it.
  • The switch that fixes that breaks everything else. enforce_revocation_expiration refuses a stale list — and refuses every clean, unrevoked client along with it, for as long as it is stale. That makes the CA’s publishing cadence a liveness dependency of every service’s configuration, in the one program a fleet cannot fetch configuration without.

The obvious escape is to re-read the file on the same watcher the sections use, and it does not work: a watcher fires on a write, and the failure to catch is the absence of one. No filesystem event says “this should have been rewritten an hour ago”. Catching that needs a clock — a periodic wake-up — which is the polling loop this crate does not have and whose absence is a stated property of it. An HTTP distribution point is a fetch loop, and OCSP is a second protocol and a third dependency.

What settles it is that the certificate is a gate, not an identity. A stolen certificate on its own buys a TCP connection and a 401; reading anything needs the bearer token. So a CRL here would revoke the credential that does not authorise, on a schedule this server cannot verify, while the credential that does authorise is a line in a file the operator already controls — deleted and restarted in seconds, with no CA, no cadence and no new way to fail. Issue short-lived certificates; revoke the token.

Structs§

Tls
A loaded TLS configuration: a certificate chain, a private key, and either a client-certificate verifier or the absence of one.

Enums§

TlsError
Why TLS did not start.