spiffe-rustls 0.4.0

SPIFFE/SPIRE integration for rustls
Documentation

spiffe-rustls

Crates.io Docs.rs MSRV

spiffe-rustls integrates rustls with SPIFFE/SPIRE using the spiffe crate’s X509Source (SPIFFE Workload API).

It provides builders for rustls::ClientConfig and rustls::ServerConfig backed by a live X509Source. When the SPIRE agent rotates SVIDs or trust bundles, new TLS handshakes automatically use the updated material, without restarting the application.

The crate focuses on TLS-level authentication and authorization via SPIFFE IDs, while delegating all cryptography and TLS mechanics to rustls.


Key Features

  • Federation support — Automatically handles multiple trust domains when SPIFFE federation is configured
  • Typed authorization — Strongly-typed Authorizer trait for SPIFFE ID–based access control
  • Live updates — Material rotates automatically when SPIRE updates SVIDs or bundles
  • Production-ready — Zero unsafe code, conservative parsing, graceful degradation

Quick Start

1. Create an X509Source

The source is configured via SPIFFE_ENDPOINT_SOCKET:

let source = spiffe::X509Source::new().await?;

2. Build a rustls client configuration

use spiffe_rustls::{authorizer, mtls_client};

let source = spiffe::X509Source::new().await?;

// Pass string literals directly — exact() will convert them
let client_cfg = mtls_client(source)
    .authorize(authorizer::exact([
        "spiffe://example.org/myservice",
        "spiffe://example.org/myservice2",
    ])?)
    .build()?;

The resulting ClientConfig can be used directly with rustls, or integrated into tokio-rustls, tonic-rustls, or similar libraries.


Federation

When SPIFFE federation is configured, the Workload API delivers trust bundles for multiple trust domains. spiffe-rustls automatically handles this:

  1. Extracts the SPIFFE ID from the peer certificate
  2. Derives the trust domain from that SPIFFE ID
  3. Selects the correct root certificate bundle from the bundle set
  4. Verifies the certificate chain using the selected bundle

No federation-specific configuration is required. Federation works automatically whenever the Workload API provides bundles for multiple trust domains.


Trust Domain Policy

You may optionally restrict which trust domains are accepted using [TrustDomainPolicy]. This is a defense-in-depth mechanism—the primary trust model comes from the bundle set delivered by the Workload API.

use spiffe_rustls::{AllowList, AnyInBundleSet, LocalOnly, TrustDomainPolicy};
use std::collections::BTreeSet;

// Choose exactly one policy variant:

// Default: use all bundles from the Workload API
let policy = AnyInBundleSet;

// Restrict to specific trust domains
let mut allowed = BTreeSet::new();
allowed.insert("broker.example".try_into()?);
allowed.insert("stockmarket.example".try_into()?);
let policy = AllowList(allowed);

// Only trust a single trust domain
let policy = LocalOnly("example.org".try_into()?);

// Full path variant (equivalent)
let policy = TrustDomainPolicy::AnyInBundleSet;

Authorization

Authorization is performed after cryptographic verification succeeds.

The crate provides a strongly-typed [Authorizer] trait for SPIFFE ID–based authorization.

Using the Authorizer trait

use spiffe_rustls::{Authorizer, authorizer};
use spiffe::SpiffeId;
use std::sync::Arc;

// Accept any SPIFFE ID (authentication only)
let auth: Arc<dyn Authorizer> = Arc::new(authorizer::any());

// Accept only exact SPIFFE IDs
let auth = authorizer::exact([
    "spiffe://example.org/payment",
    "spiffe://example.org/checkout",
])?;

// Accept any SPIFFE ID from specific trust domains
let auth = authorizer::trust_domains([
    "broker.example",
    "stockmarket.example",
])?;

// Custom authorization logic
let auth: Arc<dyn Authorizer> = Arc::new(|peer: &SpiffeId| {
    peer.path().starts_with("/payment/")
});

API Overview

Builders

  • ClientConfigBuilder — builds rustls::ClientConfig
  • ServerConfigBuilder — builds rustls::ServerConfig

Each builder:

  • retains an Arc<X509Source> internally
  • always uses the latest SVIDs and trust bundles
  • authorizes peers by SPIFFE ID (URI SAN)

Authorization helpers

  • [Authorizer] — trait for SPIFFE ID–based authorization
  • authorizer::any()
  • authorizer::exact()
  • authorizer::trust_domains()

Trust Domain Policy types

  • [TrustDomainPolicy]
  • AnyInBundleSet — use all bundles from the Workload API (default)
  • AllowList — restrict to specific trust domains
  • LocalOnly — trust exactly one trust domain

Policy variants are re-exported at the crate root for convenience.


Client Configuration

ClientConfigBuilder

Builds a rustls::ClientConfig that:

  • presents the current SPIFFE X.509 SVID
  • validates the server certificate chain using Workload API bundles
  • selects the correct trust domain automatically
  • authorizes the server by SPIFFE ID
use spiffe_rustls::{authorizer, mtls_client, AllowList};
use std::collections::BTreeSet;

let source = spiffe::X509Source::new().await?;

let allowed_server_ids = [
    "spiffe://example.org/myservice",
    "spiffe://example.org/myservice2",
];

let mut allowed_trust_domains = BTreeSet::new();
allowed_trust_domains.insert("example.org".try_into()?);

let client_cfg = mtls_client(source)
    .authorize(authorizer::exact(allowed_server_ids)?)
    .trust_domain_policy(AllowList(allowed_trust_domains))
    .build()?;

Server Configuration

ServerConfigBuilder

Builds a rustls::ServerConfig that:

  • presents the current SPIFFE X.509 SVID
  • requires and validates client certificates (mTLS)
  • selects the correct trust domain automatically
  • authorizes the client by SPIFFE ID
use spiffe::{TrustDomain, X509Source};
use spiffe_rustls::{authorizer, mtls_server, LocalOnly};

let source = X509Source::new().await?;

let allowed_trust_domains = ["example.org"];
let local_trust_domain: TrustDomain = "example.org".try_into()?;

let server_cfg = mtls_server(source)
    .authorize(authorizer::trust_domains(allowed_trust_domains)?)
    .trust_domain_policy(LocalOnly(local_trust_domain))
    .build()?;

Features

Most features are additive and opt-in. Crypto provider features are mutually exclusive—exactly one must be enabled.

Crypto providers

[features]
default = ["ring"]
ring = ["rustls/ring"]
aws-lc-rs = ["rustls/aws_lc_rs"]
  • Default: ring
  • Optional: aws-lc-rs

Example (AWS-LC):

cargo add spiffe-rustls --no-default-features --features aws-lc-rs

Provider choice affects only cryptographic primitives; SPIFFE semantics and API behavior are identical across providers.


Observability

Observability is optional and controlled via features:

  • logging — emit events via the log crate
  • tracing — emit events via the tracing crate

Both features are disabled by default.

Precedence

  1. tracing (if enabled)
  2. logging (only if tracing is disabled)
  3. no-op (if neither is enabled)

Example:

spiffe-rustls = { version = "0.4", features = ["tracing"] }

Examples

Prerequisites

All examples require:

  • a running SPIRE agent
  • a valid Workload API socket (SPIFFE_ENDPOINT_SOCKET)
  • local DNS resolution for example.org

For local testing, add to /etc/hosts:

127.0.0.1 example.org

Raw TLS (tokio-rustls)

cargo run --example mtls_tcp_server
cargo run --example mtls_tcp_client

gRPC (tonic-rustls)

gRPC examples live in a separate crate (spiffe-rustls-grpc-examples) to avoid pulling gRPC/protobuf dependencies into the library.

cargo run -p spiffe-rustls-grpc-examples --bin grpc_server_mtls
cargo run -p spiffe-rustls-grpc-examples --bin grpc_client_mtls

Security Considerations

  • Certificates must contain exactly one SPIFFE ID URI SAN
  • Trust bundles come exclusively from the Workload API
  • Trust domain selection is automatic and deterministic
  • Authorization runs after cryptographic verification
  • Material updates are atomic; new handshakes use fresh material

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.