axum-server-mtls 0.1.2

mTLS peer certificate extraction for axum-server. Wraps RustlsAcceptor to inject client certificates into request extensions.
Documentation

axum-server-mtls

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.

Quick Start

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(),
    }
}

How It Works

  1. MtlsAcceptor wraps RustlsAcceptor and implements axum_server::accept::Accept.
  2. After the TLS handshake, it reads ServerConnection::peer_certificates().
  3. It wraps the connection's service so that every request carries a PeerCertificates value in its extensions.
  4. Handlers extract it via Extension<PeerCertificates>.

Enabling Client Certificate Verification

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)).

PeerCertificates API

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

Compatibility

Dependency Version
axum-server 0.7.x
rustls 0.23.x
tokio-rustls 0.26.x
axum 0.8.x (for Extension extractor)

What This Crate Does NOT Do

  • TLS verification — that's Rustls' job. Configure WebPkiClientVerifier on your ServerConfig.
  • 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.

License

Licensed under either of Apache License, Version 2.0 or MIT License at your option.