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}