Skip to content

examples/mtls-identity: demonstrate cert-SAN identity with axum + a helper for parity with standalone Server #49

@iainmcgin

Description

@iainmcgin

PR #31 added PeerCerts and PeerAddr to Context::extensions, automatically populated by the standalone Server from the rustls accept loop. Handlers can read peer identity (cert SANs, remote addr) without caring about transport - on the standalone Server.

For axum users, this is currently a do-it-yourself path: axum::serve(listener, app) assumes a plain TcpListener, so a custom rustls accept loop is needed to extract peer_certificates() and inject it into request extensions per-connection.

This issue tracks two related pieces of work.

Part 1: examples/mtls-identity/ example

A new example crate demonstrating mTLS + cert-SAN-based identity extraction in an axum app. Mirrors the existing examples/middleware/ shape but swaps bearer-token auth for mTLS:

  • Server uses an axum router behind a rustls TLS terminator that requires client certificates (WebPkiClientVerifier)
  • Per-connection layer captures PeerCerts and PeerAddr, injects them into request extensions
  • Handler reads identity by parsing the leaf cert's SANs from ctx.extensions.get::<PeerCerts>() rather than from a bearer token
  • Test certs generated at runtime via rcgen (no PEM files in the repo)
  • Integration test exercises authorized + permission-denied paths

The example would show how to bridge axum's listener model with connectrpc's peer-identity convention, so handlers stay transport-agnostic.

Part 2: Helper API for axum + connectrpc identity parity

Today, the standalone Server::with_tls magically populates PeerCerts/PeerAddr for handlers. Axum users have to write the rustls accept loop and tower-layer wiring themselves. The same handler code that reads ctx.extensions.get::<PeerCerts>() won't work on axum without that custom wiring.

Proposal: ship a thin helper from connectrpc that does the rustls accept loop + extension injection for axum users. Sketch:

// In connectrpc::axum (behind the `axum` + `server-tls` features)
pub async fn serve_tls(
    listener: tokio::net::TcpListener,
    app: axum::Router,
    tls_config: Arc<rustls::ServerConfig>,
) -> Result<(), BoxError> {
    // Custom accept loop:
    //   1. TCP accept
    //   2. tokio_rustls handshake
    //   3. Extract peer_certificates() + peer_addr()
    //   4. Wrap each connection in a hyper service that
    //      injects PeerCerts/PeerAddr into req.extensions before
    //      handing off to the axum service
}

With this helper:

// Plaintext - existing axum convention
axum::serve(listener, app).await?;

// TLS with peer-identity passthrough - drop-in replacement
connectrpc::axum::serve_tls(listener, app, tls_config).await?;

The same handler code reads ctx.extensions.get::<PeerCerts>() regardless of whether the server was built with the standalone Server or axum + this helper. Hosting choice and identity extraction are now orthogonal.

Alternatives considered

  • Tower layer: a layer can't extract peer certs from the rustls connection unless something earlier in the stack already exposed them - the layer runs at the HTTP-request level, not the TCP/TLS-connection level. So the helper has to be at the listener/accept-loop level, not a layer.
  • Document the manual pattern in the example only: doesn't scale - every axum + mTLS user reimplements the same accept loop and per-connection injection layer.
  • Push the work to axum-server integration: adds a transitive dependency for axum users; the accept loop is small enough to ship in connectrpc directly.

Suggested order

Land Part 2 (the helper) first so the example in Part 1 can use the public API rather than reimplementing it inline. Both can ship in the same release.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions