rustls_spiffe/lib.rs
1// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
2
3//! Dynamic [`rustls`] TLS configuration backed by the [SPIFFE Workload API][spiffe]
4//! and [`rustls-config-stream`]. Effortlessly hot-swap new X509-SVIDs and Trust
5//! Bundles as they rotate, without restarting your service.
6//!
7//! Provides [`SpiffeClientConfigStream`] and [`SpiffeServerConfigStream`] for
8//! use with [`ClientConfigProvider`] and [`ServerConfigProvider`].
9//!
10//! ## Installation
11//!
12//! ```sh
13//! cargo add rustls-spiffe
14//! ```
15//!
16//! The providers connect to the Workload API at the address in the
17//! `SPIFFE_ENDPOINT_SOCKET` environment variable (e.g.
18//! `unix:///tmp/spire-agent/public/api.sock`).
19//!
20//! ## Example
21//!
22//! An mTLS server that only accepts clients presenting an X509-SVID from the
23//! `example.org` trust domain and greets them by SPIFFE ID, plus a client that
24//! connects to it. In a real deployment these would be separate workloads; they
25//! share a process here so the example runs as-is.
26//!
27//! ```
28//! use rustls_spiffe::{
29//! ClientConfigProvider, ServerConfigProvider, SpiffeClientConfigStream,
30//! SpiffeServerConfigStream, extract_leaf_cert, extract_spiffe_id,
31//! };
32//! use tokio::{
33//! io::{AsyncReadExt, AsyncWriteExt},
34//! net::{TcpListener, TcpStream},
35//! };
36//! use tokio_rustls::{TlsAcceptor, TlsConnector};
37//!
38//! type Error = Box<dyn std::error::Error + Send + Sync>;
39//!
40//! #[tokio::main]
41//! async fn main() -> Result<(), Error> {
42//! let listener = TcpListener::bind("127.0.0.1:8443").await?;
43//! tokio::spawn(serve(listener));
44//!
45//! let greeting = greet().await?;
46//! print!("{greeting}");
47//! # assert!(greeting.starts_with("hello, spiffe://example.org/"), "{greeting:?}");
48//! Ok(())
49//! }
50//!
51//! /// Accept clients from `example.org` and greet them by SPIFFE ID.
52//! async fn serve(listener: TcpListener) -> Result<(), Error> {
53//! let builder = SpiffeServerConfigStream::builder(vec!["example.org".try_into()?]);
54//!
55//! // Waits for the first SVID, then keeps the config current in the background.
56//! let provider = ServerConfigProvider::start(builder).await?;
57//!
58//! loop {
59//! let (tcp, _) = listener.accept().await?;
60//!
61//! // Fetch the config per connection so every handshake uses the latest
62//! // SVID and trust bundle.
63//! let acceptor = TlsAcceptor::from(provider.get_config());
64//!
65//! tokio::spawn(async move {
66//! let Ok(mut tls) = acceptor.accept(tcp).await else {
67//! return;
68//! };
69//! if let Some(id) = extract_spiffe_id(extract_leaf_cert(&tls)) {
70//! let _ = tls.write_all(format!("hello, {id}\n").as_bytes()).await;
71//! }
72//! let _ = tls.shutdown().await;
73//! });
74//! }
75//! }
76//!
77//! /// Connect to a server from `example.org` and return its greeting.
78//! async fn greet() -> Result<String, Error> {
79//! let builder = SpiffeClientConfigStream::builder(vec!["example.org".try_into()?]);
80//! let provider = ClientConfigProvider::start(builder).await?;
81//!
82//! // As on the server, fetch the config for each new connection.
83//! let connector = TlsConnector::from(provider.get_config());
84//! let tcp = TcpStream::connect("localhost:8443").await?;
85//! let mut tls = connector.connect("localhost".try_into()?, tcp).await?;
86//!
87//! let mut greeting = String::new();
88//! tls.read_to_string(&mut greeting).await?;
89//! Ok(greeting)
90//! }
91//! ```
92//!
93//! The server verifies that client certificates chain to a configured trust
94//! domain, but does not restrict *which* workloads in that trust domain may
95//! connect. Use [`extract_spiffe_id`] to authorize individual SPIFFE IDs.
96//!
97//! The client performs standard hostname verification, so the server's SVID
98//! must carry a DNS SAN matching the name you connect to (with SPIRE, register
99//! the server entry with `-dns localhost`, for example).
100//!
101//! ## Rotation and Health
102//!
103//! Both providers subscribe to the Workload API and atomically swap in a new
104//! config whenever SPIRE rotates the SVID or a trust bundle changes. Existing
105//! connections keep the config they were established with.
106//!
107//! If the Workload API stream fails, the provider keeps serving the last good
108//! config while it reconnects with exponential backoff. `stream_healthy()`
109//! reports `false` until the stream is re-established, which is useful for
110//! readiness checks.
111//!
112//! ## Trust Domains and Federation
113//!
114//! The list passed to `builder` selects which trust bundles are used as roots.
115//! To accept peers from a federated trust domain (whose bundle SPIRE delivers
116//! when the registration entry federates with it), include it alongside your
117//! own:
118//!
119//! ```
120//! # use rustls_spiffe::SpiffeServerConfigStream;
121//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
122//! let builder = SpiffeServerConfigStream::builder(vec![
123//! "example.org".try_into()?,
124//! "partner.example.com".try_into()?,
125//! ]);
126//! # Ok(())
127//! # }
128//! ```
129//!
130//! ## Feature Flags
131//!
132//! All features are enabled by default via the `full` feature.
133//!
134//! | Feature | Description |
135//! |------------------|-----------------------------------------------------------------------------|
136//! | `config-stream` | [`SpiffeClientConfigStream`], [`SpiffeServerConfigStream`], and the providers |
137//! | `svid-extractor` | [`extract_leaf_cert`] and [`extract_spiffe_id`] for `tokio-rustls` servers |
138//! | `tracing` | Logs workload identities, bundle loads, and stream reconnects via `tracing` |
139//!
140//! ## Crypto Provider
141//!
142//! This crate enables `rustls`'s `aws-lc-rs` backend. If something else in your
143//! dependency tree enables another backend (e.g. `ring`), install a process
144//! default before starting a provider:
145//!
146//! ```
147//! // Errs only if a default is already installed, which is safe to ignore.
148//! let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
149//! ```
150//!
151//! [`rustls`]: https://docs.rs/rustls
152//! [spiffe]: https://spiffe.io/docs/latest/spiffe-about/overview/
153//! [`rustls-config-stream`]: https://docs.rs/rustls-config-stream
154//! [`SpiffeClientConfigStream`]: https://docs.rs/rustls-spiffe/latest/rustls_spiffe/struct.SpiffeClientConfigStream.html
155//! [`SpiffeServerConfigStream`]: https://docs.rs/rustls-spiffe/latest/rustls_spiffe/struct.SpiffeServerConfigStream.html
156//! [`ClientConfigProvider`]: https://docs.rs/rustls-config-stream/latest/rustls_config_stream/struct.ClientConfigProvider.html
157//! [`ServerConfigProvider`]: https://docs.rs/rustls-config-stream/latest/rustls_config_stream/struct.ServerConfigProvider.html
158//! [`extract_leaf_cert`]: https://docs.rs/rustls-spiffe/latest/rustls_spiffe/fn.extract_leaf_cert.html
159//! [`extract_spiffe_id`]: https://docs.rs/rustls-spiffe/latest/rustls_spiffe/fn.extract_spiffe_id.html
160
161#![cfg_attr(docsrs, feature(doc_cfg))]
162#![forbid(rust_2018_idioms)]
163#![forbid(missing_docs, unsafe_code)]
164#![deny(
165 clippy::all,
166 clippy::pedantic,
167 clippy::unwrap_used,
168 clippy::expect_used,
169 clippy::nursery,
170 clippy::dbg_macro,
171 clippy::todo
172)]
173
174#[cfg(feature = "config-stream")]
175mod client_stream;
176#[cfg(feature = "config-stream")]
177mod server_stream;
178#[cfg(feature = "svid-extractor")]
179mod svid_extractor;
180
181#[cfg(feature = "config-stream")]
182#[cfg_attr(docsrs, doc(cfg(feature = "config-stream")))]
183pub use client_stream::{ClientConfigProvider, SpiffeClientConfigStream};
184#[cfg(feature = "config-stream")]
185#[cfg_attr(docsrs, doc(cfg(feature = "config-stream")))]
186pub use server_stream::{ServerConfigProvider, SpiffeServerConfigStream};
187
188mod trust_domain_store;
189pub(crate) use trust_domain_store::TrustDomainStore;
190
191#[cfg(feature = "svid-extractor")]
192#[cfg_attr(docsrs, doc(cfg(feature = "svid-extractor")))]
193pub use svid_extractor::{extract_leaf_cert, extract_spiffe_id};