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-spiffeThe 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.
| Feature | Description |
|---|---|
config-stream | SpiffeClientConfigStream, SpiffeServerConfigStream, and the providers |
svid-extractor | extract_leaf_cert and extract_spiffe_id for tokio-rustls servers |
tracing | Logs 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§
- Client
Config Provider config-stream - Holds the current
ClientConfigand refreshes it from an async stream. - Server
Config Provider config-stream - Holds the current
ServerConfigand refreshes it from an async stream. - Spiffe
Client Config Stream config-stream - A stream that yields updated
rustls::ClientConfigvalues derived from the SPIFFE Workload API X509-SVID and Trust Bundles. - Spiffe
Server Config Stream config-stream - A stream that yields updated
rustls::ServerConfigvalues derived from the SPIFFE Workload API X509-SVID and Trust Bundles.
Functions§
- extract_
leaf_ cert svid-extractor - Extract the leaf
CertificateDerfrom aTlsStream - extract_
spiffe_ id svid-extractor - Extract a
SpiffeIdfrom aCertificateDerif the certificate is a valid X509-SVID