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-podout of it readsbilling, 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::Ignoremeans a list whosenextUpdatepassed 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_expirationrefuses 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.