Skip to main content

spiffe_rustls/
server.rs

1use crate::authorizer::Authorizer;
2use crate::error::Result;
3use crate::policy::TrustDomainPolicy;
4use crate::resolve::MaterialWatcher;
5use crate::verifier::SpiffeClientCertVerifier;
6use rustls::server::ResolvesServerCert;
7use rustls::ServerConfig;
8use spiffe::X509Source;
9use std::sync::Arc;
10
11/// Function type for customizing a `ServerConfig`.
12type ServerConfigCustomizer = Box<dyn FnOnce(&mut ServerConfig) + Send>;
13
14/// Builds a [`rustls::ServerConfig`] backed by a live SPIFFE `X509Source`.
15///
16/// The resulting server configuration:
17///
18/// * presents the current SPIFFE X.509 SVID as the server certificate
19/// * requires and validates client certificates (mTLS)
20/// * authorizes the client by SPIFFE ID (URI SAN)
21///
22/// ## Trust Domain Selection
23///
24/// The builder uses the bundle set from `X509Source`, which may contain bundles
25/// for multiple trust domains (when SPIFFE federation is configured). The verifier
26/// automatically selects the correct bundle based on the peer's SPIFFE ID—no
27/// manual configuration is required. You can optionally restrict which trust
28/// domains are accepted using [`Self::trust_domain_policy`].
29///
30/// The default policy is [`TrustDomainPolicy::AnyInBundleSet`], which accepts any
31/// trust domain present in the source bundle set. For non-federated deployments,
32/// prefer [`TrustDomainPolicy::LocalOnly`] to restrict verification to the local
33/// trust domain.
34///
35/// ## Authorization
36///
37/// Client authorization is performed by invoking the provided [`Authorizer`] with
38/// the client's SPIFFE ID extracted from the certificate's URI SAN.
39///
40/// The default authorizer is [`crate::authorizer::any`]. It accepts any authenticated
41/// SPIFFE ID from any trust domain accepted by the configured trust-domain policy.
42/// By default, this means every trust domain in the source bundle set.
43///
44/// <div class="warning">
45///
46/// **Security:** with the default configuration, authentication validates the
47/// client certificate against the trust bundle for the peer's SPIFFE ID trust
48/// domain. Authorization uses [`crate::authorizer::any`] together with
49/// [`TrustDomainPolicy::AnyInBundleSet`], so any client whose certificate
50/// validates and whose SPIFFE ID is accepted by that policy is allowed.
51/// Production deployments should normally configure a specific [`Authorizer`]
52/// (via [`Self::authorize`]) and an appropriate [`TrustDomainPolicy`] (via
53/// [`Self::trust_domain_policy`]); [`TrustDomainPolicy::LocalOnly`] is suitable
54/// when federation is not required.
55///
56/// </div>
57///
58/// # Examples
59///
60/// ```no_run
61/// use spiffe::{TrustDomain, X509Source};
62/// use spiffe_rustls::{authorizer, mtls_server, LocalOnly};
63///
64/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
65/// let source = X509Source::new().await?;
66///
67/// // Pass string literals directly - trust_domains() will convert them
68/// let allowed_trust_domains = ["example.org"];
69///
70/// let local_trust_domain: TrustDomain = "example.org".try_into()?;
71///
72/// let server_config = mtls_server(source)
73///     .authorize(authorizer::trust_domains(allowed_trust_domains)?)
74///     .trust_domain_policy(LocalOnly(local_trust_domain))
75///     .build()?;
76/// # Ok(())
77/// # }
78/// ```
79pub struct ServerConfigBuilder {
80    source: Arc<X509Source>,
81    authorizer: Arc<dyn Authorizer>,
82    trust_domain_policy: TrustDomainPolicy,
83    alpn_protocols: Vec<Vec<u8>>,
84    config_customizer: Option<ServerConfigCustomizer>,
85}
86
87impl std::fmt::Debug for ServerConfigBuilder {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("ServerConfigBuilder")
90            .field("source", &"<Arc<X509Source>>")
91            .field("authorizer", &"<Arc<dyn Authorizer>>")
92            .field("trust_domain_policy", &self.trust_domain_policy)
93            .field("alpn_protocols", &self.alpn_protocols)
94            .field("config_customizer", &self.config_customizer.is_some())
95            .finish()
96    }
97}
98
99impl ServerConfigBuilder {
100    /// Creates a new builder from an `X509Source`.
101    ///
102    /// Defaults:
103    /// - Authorization: [`crate::authorizer::any`], which accepts any authenticated
104    ///   SPIFFE ID from any trust domain accepted by the configured trust-domain policy.
105    ///   By default, this means every trust domain in the source bundle set.
106    /// - Trust domain policy: [`TrustDomainPolicy::AnyInBundleSet`], which accepts any
107    ///   trust domain present in the source bundle set
108    /// - ALPN protocols: empty (no ALPN)
109    ///
110    /// With the default configuration, authentication validates the peer
111    /// certificate against the configured trust bundle, and authorization uses
112    /// [`crate::authorizer::any`] with [`TrustDomainPolicy::AnyInBundleSet`].
113    /// Production deployments should normally configure a specific authorizer via
114    /// [`Self::authorize`] and an appropriate trust domain policy via
115    /// [`Self::trust_domain_policy`]; [`TrustDomainPolicy::LocalOnly`] is suitable
116    /// when federation is not required.
117    pub fn new(source: X509Source) -> Self {
118        Self {
119            source: Arc::new(source),
120            authorizer: Arc::new(crate::authorizer::any()),
121            trust_domain_policy: TrustDomainPolicy::default(),
122            alpn_protocols: Vec::new(),
123            config_customizer: None,
124        }
125    }
126
127    /// Sets the authorization policy for client SPIFFE IDs.
128    ///
129    /// Accepts any type that implements `Authorizer`, including closures.
130    ///
131    /// # Examples
132    ///
133    /// ```no_run
134    /// use spiffe_rustls::{authorizer, mtls_server};
135    ///
136    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
137    /// let source = spiffe::X509Source::new().await?;
138    ///
139    /// // Pass string literals directly
140    /// let config = mtls_server(source.clone())
141    ///     .authorize(authorizer::trust_domains([
142    ///         "example.org",
143    ///     ])?)
144    ///     .build()?;
145    ///
146    /// // Using a closure
147    /// let config = mtls_server(source.clone())
148    ///     .authorize(|id: &spiffe::SpiffeId| id.path().starts_with("/api/"))
149    ///     .build()?;
150    ///
151    /// // Using the Any authorizer (default)
152    /// let config = mtls_server(source)
153    ///     .authorize(authorizer::any())
154    ///     .build()?;
155    /// # Ok(())
156    /// # }
157    /// ```
158    #[must_use]
159    pub fn authorize<A: Authorizer>(mut self, authorizer: A) -> Self {
160        self.authorizer = Arc::new(authorizer);
161        self
162    }
163
164    /// Sets the trust domain policy.
165    ///
166    /// Defaults to [`TrustDomainPolicy::AnyInBundleSet`], which accepts any trust
167    /// domain present in the source bundle set. For non-federated deployments,
168    /// prefer [`TrustDomainPolicy::LocalOnly`] to restrict verification to the local
169    /// trust domain.
170    #[must_use]
171    pub fn trust_domain_policy(mut self, policy: TrustDomainPolicy) -> Self {
172        self.trust_domain_policy = policy;
173        self
174    }
175
176    /// Sets the ALPN (Application-Layer Protocol Negotiation) protocols.
177    ///
178    /// The protocols are advertised during the TLS handshake. Common values:
179    /// - `b"h2"` for HTTP/2 (required for gRPC)
180    /// - `b"http/1.1"` for HTTP/1.1
181    ///
182    /// Protocols should be specified in order of preference (most preferred first).
183    ///
184    /// # Examples
185    ///
186    /// ```no_run
187    /// use spiffe_rustls::mtls_server;
188    ///
189    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
190    /// let source = spiffe::X509Source::new().await?;
191    /// let config = mtls_server(source)
192    ///     .with_alpn_protocols([b"h2"])
193    ///     .build()?;
194    /// # Ok(())
195    /// # }
196    /// ```
197    #[must_use]
198    pub fn with_alpn_protocols<I, P>(mut self, protocols: I) -> Self
199    where
200        I: IntoIterator<Item = P>,
201        P: AsRef<[u8]>,
202    {
203        self.alpn_protocols = protocols.into_iter().map(|p| p.as_ref().to_vec()).collect();
204        self
205    }
206
207    /// Applies a customizer function to the `ServerConfig` after it's built.
208    ///
209    /// This is an **advanced** API for configuration not directly exposed by the builder.
210    /// The customizer is called **last**, after all other builder settings (including
211    /// ALPN) have been applied, and gives direct access to the underlying `rustls`
212    /// configuration.
213    ///
214    /// **Warning:** Replacing the verifier or resolver disables SPIFFE authentication.
215    /// Do not use this hook for that purpose; build a custom `rustls` configuration instead.
216    ///
217    /// # Examples
218    ///
219    /// ```no_run
220    /// use spiffe_rustls::mtls_server;
221    ///
222    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
223    /// let source = spiffe::X509Source::new().await?;
224    /// let config = mtls_server(source)
225    ///     .with_config_customizer(|cfg| {
226    ///         // Example: adjust cipher suite preferences
227    ///     })
228    ///     .build()?;
229    /// # Ok(())
230    /// # }
231    /// ```
232    #[must_use]
233    pub fn with_config_customizer<F>(mut self, customizer: F) -> Self
234    where
235        F: FnOnce(&mut ServerConfig) + Send + 'static,
236    {
237        self.config_customizer = Some(Box::new(customizer));
238        self
239    }
240
241    /// Builds the `rustls::ServerConfig`.
242    ///
243    /// TLS session resumption (stateful session caching and TLS 1.3 session
244    /// tickets) is **disabled by default**. rustls does not re-invoke the
245    /// client certificate verifier on a resumed handshake, so resumption would
246    /// let a session outlive its SVID's (deliberately short) expiry, and
247    /// bypass a bundle or authorizer change (e.g. defederation, a tightened
248    /// allow list) until the cached session/ticket ages out. SPIRE's own TLS
249    /// server endpoint disables session tickets for the same reason. If you
250    /// understand this trade-off and want resumption for its performance
251    /// benefit, re-enable it via [`Self::with_config_customizer`] by setting
252    /// `cfg.session_storage` and `cfg.send_tls13_tickets`.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if:
257    ///
258    /// * the SPIFFE `X509Source` does not currently have an SVID,
259    /// * rustls crypto providers are not installed,
260    /// * or the material watcher cannot be initialized.
261    pub fn build(self) -> Result<ServerConfig> {
262        crate::crypto::ensure_crypto_provider_installed();
263
264        let watcher = MaterialWatcher::spawn(self.source)?;
265
266        let resolver: Arc<dyn ResolvesServerCert> =
267            Arc::new(resolve_server::SpiffeServerCertResolver {
268                watcher: watcher.clone(),
269            });
270
271        let verifier = Arc::new(SpiffeClientCertVerifier::new(
272            Arc::new(watcher),
273            self.authorizer,
274            self.trust_domain_policy,
275        ));
276
277        let mut cfg = ServerConfig::builder()
278            .with_client_cert_verifier(verifier)
279            .with_cert_resolver(resolver);
280
281        cfg.alpn_protocols = self.alpn_protocols;
282
283        // Disable session resumption by default: rustls does not re-run the
284        // client cert verifier on a resumed handshake, so a resumed session
285        // would silently bypass SVID expiry and bundle/authorizer changes.
286        // See the `build` doc for rationale and how to opt back in.
287        // `session_storage` covers TLS 1.2 and stateful TLS 1.3 resumption;
288        // `send_tls13_tickets = 0` stops issuing TLS 1.3 tickets so clients
289        // cannot resume statelessly either.
290        cfg.session_storage = Arc::new(rustls::server::NoServerSessionStorage {});
291        cfg.send_tls13_tickets = 0;
292
293        // Apply customizer last
294        if let Some(customizer) = self.config_customizer {
295            customizer(&mut cfg);
296        }
297
298        Ok(cfg)
299    }
300}
301
302mod resolve_server {
303    use crate::resolve::MaterialWatcher;
304    use rustls::server::ResolvesServerCert;
305    use rustls::sign::CertifiedKey;
306    use std::sync::Arc;
307
308    #[derive(Clone, Debug)]
309    pub(crate) struct SpiffeServerCertResolver {
310        pub watcher: MaterialWatcher,
311    }
312
313    impl ResolvesServerCert for SpiffeServerCertResolver {
314        fn resolve(
315            &self,
316            _client_hello: rustls::server::ClientHello<'_>,
317        ) -> Option<Arc<CertifiedKey>> {
318            Some(Arc::clone(&self.watcher.current().certified_key))
319        }
320    }
321}