Skip to main content

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};