← Week 3: Integration and Application

Day 18: mTLS with SPIFFE SVIDs — Replacing Static Certs

Phase 4 · August 22, 2026

← Week 3: Integration and Application

Agenda (2–3 hours)

  • Read (30 min): SPIFFE Workload API spec §mTLS section; spiffe-rs examples
  • Build (120 min): mTLS server and client using live SVID from Workload API
  • Test (30 min): Two-way identity verification; watch SVID rotation
← Week 3: Integration and Application

mTLS with Static Certs vs. SVIDs

Static certs (Phase 2, toy-pki):

// At startup — load from disk, never updated
let server_config = ServerConfig::builder()
    .with_client_cert_verifier(verifier)
    .with_single_cert(cert_chain, private_key)?;

SVIDs (today) — the key difference is hot rotation:

// Use X509Source which auto-updates when SPIRE rotates the SVID
let source = Arc::new(X509Source::default().await?);

// Build a rustls config that reads from the source at handshake time
// (not at startup — the cert can change while the server is running)
let server_config = build_server_config_from_source(&source)?;

The identity is the same X.509 cert → mTLS handshake works identically.
The difference is lifecycle: SVIDs rotate without server restart.

← Week 3: Integration and Application

Building a TLS Config from an SVID Source

The spiffe crate provides a rustls integration feature:

[dependencies]
spiffe = { version = "0.6", features = ["rustls"] }
use spiffe::workload_api::x509_source::X509Source;
use spiffe::bundle::BundleSource;

// Server config: presents our SVID, validates peer SVIDs against trust bundle
pub async fn build_mtls_server_config(
    source: Arc<X509Source>,
    allowed_spiffe_ids: Vec<SpiffeId>,
) -> Result<Arc<ServerConfig>> {
    // The spiffe crate's rustls integration handles cert selection + peer validation
    // (check docs.rs/spiffe for the exact API — may vary by version)
    
    // Pattern if not using the integration feature:
    // Implement CertResolver + ClientCertVerifier using the source
    todo!("see Day 18 notes on the resolver pattern")
}
← Week 3: Integration and Application

Custom CertResolver for Hot Rotation

If the spiffe crate's rustls integration doesn't cover your use case:

use rustls::server::{ClientHello, ResolvesServerCert};
use rustls::sign::CertifiedKey;
use std::sync::Arc;

struct SvidCertResolver {
    source: Arc<X509Source>,
}

impl ResolvesServerCert for SvidCertResolver {
    fn resolve(&self, _client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
        // Called at every TLS handshake — always returns the current SVID
        let svid = self.source.get_x509_svid().ok()?;
        
        let cert_chain = svid.cert_chain().iter()
            .map(|der| rustls::Certificate(der.clone()))
            .collect();
        let key = rustls::PrivateKey(svid.private_key().as_ref().to_vec());
        let signing_key = rustls::crypto::ring::sign::any_supported_type(&key).ok()?;
        
        Some(Arc::new(CertifiedKey::new(cert_chain, signing_key)))
    }
}

Because resolve() is called per-handshake, the server automatically uses
the newest SVID after rotation — no restart needed.

← Week 3: Integration and Application

Custom ClientCertVerifier for SPIFFE Authorization

use rustls::server::{ClientCertVerified, ClientCertVerifier};
use rustls::DistinguishedName;

struct SpiffeVerifier {
    validator: Arc<SvidValidator>,
    allowed_ids: Vec<SpiffeId>,
}

impl ClientCertVerifier for SpiffeVerifier {
    fn verify_client_cert(
        &self,
        end_entity: &rustls::Certificate,
        intermediates: &[rustls::Certificate],
        _now: std::time::SystemTime,
    ) -> Result<ClientCertVerified, rustls::Error> {
        let spiffe_id = self.validator.validate(&end_entity.0)
            .map_err(|e| rustls::Error::General(format!("SVID invalid: {e:?}")))?;
        
        let parsed_id: SpiffeId = spiffe_id.parse()
            .map_err(|e| rustls::Error::General(format!("{e}")))?;
        
        if !self.allowed_ids.contains(&parsed_id) {
            return Err(rustls::Error::General(
                format!("unauthorized SPIFFE ID: {spiffe_id}")
            ));
        }
        
        Ok(ClientCertVerified::assertion())
    }
    // ... other required methods
}
← Week 3: Integration and Application

The mTLS Flow with SVIDs

Client (spiffe://example.org/service-a) connects to
Server (spiffe://example.org/service-b)

TLS Handshake:
1. Client → ClientHello
2. Server → ServerHello + Certificate (SVID for service-b)
3. Server → CertificateRequest (requesting client cert)
4. Client → Certificate (SVID for service-a) + CertificateVerify
5. Server verifies client SVID:
   a. Chain valid against trust bundle? ✓
   b. Validity period current? ✓
   c. SPIFFE ID = spiffe://example.org/service-a ✓
   d. service-a is in the allowed list? ✓
6. Handshake complete — both sides verified.

Step 5d is the authorization check that goes beyond TLS's normal cert validation.
This is what SPIFFE adds: workload-level authorization, not just "valid cert."

← Week 3: Integration and Application

Challenge Assignment

Build a complete mTLS pair in spiffe-demo using live SVIDs:

  1. spiffe-demo tls-server — starts a TLS server on localhost:7443

    • Uses its SVID as the server cert (via SvidCertResolver)
    • Validates client SVIDs (via SpiffeVerifier)
    • Prints the client's SPIFFE ID after successful handshake
    • Responds with "hello from service-b"
  2. spiffe-demo tls-client — connects to localhost:7443

    • Uses its SVID as the client cert
    • Validates server cert as an X.509-SVID
    • Prints the server's SPIFFE ID after successful handshake
    • Prints the response
  3. Run both simultaneously (different UIDs for different SVIDs)

  4. Simulate rotation: after a successful connection, wait for the SVID to rotate
    (use a 60-second TTL), then reconnect — does it use the new cert?

← Week 3: Integration and Application

Resources

  • spiffe crate rustls feature: docs.rs/spiffe — look for rustls integration
  • rustls ResolvesServerCert: docs.rs/rustls
  • rustls ClientCertVerifier: docs.rs/rustls
  • Phase 2, Day 17 (your toy-pki mTLS code) — the same rustls patterns apply