mTLS client certificate extraction for axum-server.
axum-server does not expose peer certificates after the TLS handshake
(issue #162).
This crate fills that gap by wrapping RustlsAcceptor with a custom Accept
implementation that extracts the client certificate chain and injects it into
every HTTP request as an extension.
use axum::{extract::Extension, routing::get, Router};
use axum_server_mtls::{MtlsAcceptor, PeerCertificates};
use axum_server::tls_rustls::{RustlsAcceptor, RustlsConfig};
#[tokio::main]
async fn main() {
// Build your RustlsConfig with client cert verification enabled.
// See rustls docs for WebPkiClientVerifier setup.
let rustls_config = RustlsConfig::from_pem_file("cert.pem", "key.pem")
.await
.unwrap();
let app = Router::new().route("/", get(handler));
// Wrap the RustlsAcceptor with MtlsAcceptor
let acceptor = MtlsAcceptor::new(RustlsAcceptor::new(rustls_config));
let addr: std::net::SocketAddr = "0.0.0.0:3000".parse().unwrap();
axum_server::bind(addr)
.acceptor(acceptor)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler(Extension(certs): Extension<PeerCertificates>) -> String {
match certs.leaf_cn() {
Some(cn) => format!("Hello, {cn}!"),
None => "No client certificate presented.".into(),
}
}MtlsAcceptorwrapsRustlsAcceptorand implementsaxum_server::accept::Accept.- After the TLS handshake, it reads
ServerConnection::peer_certificates(). - It wraps the connection's service so that every request carries a
PeerCertificatesvalue in its extensions. - Handlers extract it via
Extension<PeerCertificates>.
For clients to present certificates, the Rustls ServerConfig must be built with
a client cert verifier. MtlsAcceptor only extracts certificates that Rustls
has already verified — it does not perform verification itself.
use rustls::server::WebPkiClientVerifier;
use rustls::RootCertStore;
use std::sync::Arc;
// Load your client CA certificates
let mut roots = RootCertStore::empty();
// roots.add(...) your client CA certs
let verifier = WebPkiClientVerifier::builder(Arc::new(roots))
.allow_unauthenticated() // optional: allow clients without certs too
.build()
.unwrap();
let config = rustls::ServerConfig::builder()
.with_client_cert_verifier(verifier)
.with_single_cert(server_certs, server_key)
.unwrap();Then pass this config to RustlsConfig::from_config(Arc::new(config)).
| Method | Returns | Description |
|---|---|---|
is_present() |
bool |
Client presented at least one certificate |
is_empty() |
bool |
No client certificate presented |
chain() |
&[CertificateDer] |
Full DER-encoded cert chain, leaf first |
leaf() |
Option<&CertificateDer> |
The client's own certificate |
leaf_cn() |
Option<String> |
Common Name from the leaf cert's subject |
leaf_sans() |
Vec<String> |
Subject Alternative Names (DNS, email, IP) |
leaf_serial_hex() |
Option<String> |
Serial number as hex string |
leaf_not_after_unix() |
Option<i64> |
Expiry as UNIX timestamp |
| Dependency | Version |
|---|---|
| axum-server | 0.7.x |
| rustls | 0.23.x |
| tokio-rustls | 0.26.x |
| axum | 0.8.x (for Extension extractor) |
- TLS verification — that's Rustls' job. Configure
WebPkiClientVerifieron yourServerConfig. - Identity mapping — mapping CN/SANs to users/roles is application logic.
- Certificate revocation — use Rustls' CRL/OCSP support in the verifier.
- Certificate management — generating, storing, or rotating certs is out of scope.
Licensed under either of Apache License, Version 2.0 or MIT License at your option.