Skip to main content

Crate rustls_spiffe

Crate rustls_spiffe 

Source
Expand description

Dynamic rustls TLS configuration backed by the SPIFFE Workload API and rustls-config-stream. Effortlessly hot-swap new X509-SVIDs and Trust Bundles as they rotate, without restarting your service.

Provides SpiffeClientConfigStream and SpiffeServerConfigStream for use with ClientConfigProvider and ServerConfigProvider.

§Installation

cargo add rustls-spiffe

The providers connect to the Workload API at the address in the SPIFFE_ENDPOINT_SOCKET environment variable (e.g. unix:///tmp/spire-agent/public/api.sock).

§Example

An mTLS server that only accepts clients presenting an X509-SVID from the example.org trust domain and greets them by SPIFFE ID, plus a client that connects to it. In a real deployment these would be separate workloads; they share a process here so the example runs as-is.

use rustls_spiffe::{
    ClientConfigProvider, ServerConfigProvider, SpiffeClientConfigStream,
    SpiffeServerConfigStream, extract_leaf_cert, extract_spiffe_id,
};
use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::{TcpListener, TcpStream},
};
use tokio_rustls::{TlsAcceptor, TlsConnector};

type Error = Box<dyn std::error::Error + Send + Sync>;

#[tokio::main]
async fn main() -> Result<(), Error> {
    let listener = TcpListener::bind("127.0.0.1:8443").await?;
    tokio::spawn(serve(listener));

    let greeting = greet().await?;
    print!("{greeting}");
    Ok(())
}

/// Accept clients from `example.org` and greet them by SPIFFE ID.
async fn serve(listener: TcpListener) -> Result<(), Error> {
    let builder = SpiffeServerConfigStream::builder(vec!["example.org".try_into()?]);

    // Waits for the first SVID, then keeps the config current in the background.
    let provider = ServerConfigProvider::start(builder).await?;

    loop {
        let (tcp, _) = listener.accept().await?;

        // Fetch the config per connection so every handshake uses the latest
        // SVID and trust bundle.
        let acceptor = TlsAcceptor::from(provider.get_config());

        tokio::spawn(async move {
            let Ok(mut tls) = acceptor.accept(tcp).await else {
                return;
            };
            if let Some(id) = extract_spiffe_id(extract_leaf_cert(&tls)) {
                let _ = tls.write_all(format!("hello, {id}\n").as_bytes()).await;
            }
            let _ = tls.shutdown().await;
        });
    }
}

/// Connect to a server from `example.org` and return its greeting.
async fn greet() -> Result<String, Error> {
    let builder = SpiffeClientConfigStream::builder(vec!["example.org".try_into()?]);
    let provider = ClientConfigProvider::start(builder).await?;

    // As on the server, fetch the config for each new connection.
    let connector = TlsConnector::from(provider.get_config());
    let tcp = TcpStream::connect("localhost:8443").await?;
    let mut tls = connector.connect("localhost".try_into()?, tcp).await?;

    let mut greeting = String::new();
    tls.read_to_string(&mut greeting).await?;
    Ok(greeting)
}

The server verifies that client certificates chain to a configured trust domain, but does not restrict which workloads in that trust domain may connect. Use extract_spiffe_id to authorize individual SPIFFE IDs.

The client performs standard hostname verification, so the server’s SVID must carry a DNS SAN matching the name you connect to (with SPIRE, register the server entry with -dns localhost, for example).

§Rotation and Health

Both providers subscribe to the Workload API and atomically swap in a new config whenever SPIRE rotates the SVID or a trust bundle changes. Existing connections keep the config they were established with.

If the Workload API stream fails, the provider keeps serving the last good config while it reconnects with exponential backoff. stream_healthy() reports false until the stream is re-established, which is useful for readiness checks.

§Trust Domains and Federation

The list passed to builder selects which trust bundles are used as roots. To accept peers from a federated trust domain (whose bundle SPIRE delivers when the registration entry federates with it), include it alongside your own:

let builder = SpiffeServerConfigStream::builder(vec![
    "example.org".try_into()?,
    "partner.example.com".try_into()?,
]);

§Feature Flags

All features are enabled by default via the full feature.

FeatureDescription
config-streamSpiffeClientConfigStream, SpiffeServerConfigStream, and the providers
svid-extractorextract_leaf_cert and extract_spiffe_id for tokio-rustls servers
tracingLogs workload identities, bundle loads, and stream reconnects via tracing

§Crypto Provider

This crate enables rustls’s aws-lc-rs backend. If something else in your dependency tree enables another backend (e.g. ring), install a process default before starting a provider:

// Errs only if a default is already installed, which is safe to ignore.
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();

Structs§

ClientConfigProviderconfig-stream
Holds the current ClientConfig and refreshes it from an async stream.
ServerConfigProviderconfig-stream
Holds the current ServerConfig and refreshes it from an async stream.
SpiffeClientConfigStreamconfig-stream
A stream that yields updated rustls::ClientConfig values derived from the SPIFFE Workload API X509-SVID and Trust Bundles.
SpiffeServerConfigStreamconfig-stream
A stream that yields updated rustls::ServerConfig values derived from the SPIFFE Workload API X509-SVID and Trust Bundles.

Functions§

extract_leaf_certsvid-extractor
Extract the leaf CertificateDer from a TlsStream
extract_spiffe_idsvid-extractor
Extract a SpiffeId from a CertificateDer if the certificate is a valid X509-SVID