core.net.weft.spiffe
Per-workload identity in the SPIFFE / SPIRE model. X.509-SVID for mTLS, JWT-SVID for HTTP-auth. Rotation through a SPIRE agent (default TTL 1 hour, refresh at half-TTL). Verum 2026 datacenter default: mTLS everywhere with workload-identity-as-principal.
Source: core/net/weft/spiffe.vr.
Principal — the authenticated identity context
context Principal {
fn spiffe_id(&self) -> &SpiffeId;
fn expires_at(&self) -> Instant;
fn claims(&self) -> &Map<Text, Text>;
}
Principal is a context (DI value), not a request field. The
SpiffeAuthLayer validates the incoming SVID and provides
Principal so downstream handlers receive it as a typed context.
SpiffeAuthLayer<P> — the gate
public type SpiffeAuthLayer<P: TrustBundleProvider> is { /* ... */ };
implement<P: TrustBundleProvider + Send + Sync + 'static> SpiffeAuthLayer<P> {
public fn new(provider: P) -> SpiffeAuthLayer<P>
public fn jwt(secret: Bytes) -> SpiffeAuthLayer<JwtProvider>
public fn mtls(trust_bundle: TrustBundle) -> SpiffeAuthLayer<X509Provider>
}
Behaviour:
- For mTLS termination: validates the client certificate chain against the SPIRE trust bundle, extracts the SPIFFE URI SAN.
- For JWT: validates the
Authorization: Bearertoken signature, extracts the SPIFFE ID claim. - On success:
provide Principal = ...for the request scope. - On failure: returns an
AuthRejection—MissingCredential,InvalidCredential(Text),ExpiredorUntrustedSource(core/net/weft/spiffe.vr).WeftErrorhas noForbiddenvariant; its roster isBadRequest,RouteNotFound,MethodNotAllowed,ExtractionRejected,Timeout,Overloaded,UpstreamIoandInternal.
Usage
// `layer` is `ServiceBuilder`'s; the router is the service it wraps.
let router = Router.new()
.route("/admin", Method.Get, admin_handler);
let app = ServiceBuilder.new(router)
.layer(SpiffeAuthLayer.new(trust_bundle))
.build();
async fn admin_handler(
Ctx(p): Ctx<Principal>,
) -> Result<Response, ApiError> {
if !p.spiffe_id().matches("spiffe://prod/*/sa/admin") {
return Err(ApiError.Forbidden);
}
// ...
}
TrustBundleProvider — the trust source
public type TrustBundleProvider is protocol {
fn x509_bundle(&self) -> Maybe<X509Bundle>;
fn jwt_bundle(&self) -> Maybe<JwtBundle>;
fn reload(&self) -> Result<(), Text>;
}
The default implementation polls the SPIRE Workload API
(unix:///run/spire/sockets/agent.sock) for the current trust
bundle and SVID. Cache plus rotation:
- Fetches at startup, populates the cache.
- Sets a timer for
expiry / 2. - On timer: re-fetches; on success, atomically swaps the cache and
notifies the
rotation_signalchannel. - On fetch failure: keeps the current bundle, retries with exponential backoff, alerts if the bundle is within 10 minutes of expiry.
Subscribers to rotation_signal (the connection pool, in particular)
re-key their TLS sessions on rotation rather than letting them
silently expire mid-session.
SpiffeClientTransport — outgoing mTLS
SpiffeClientTransport is not declared in core/. To inspect the source
for this transport, run rg SpiffeClientTransport core/.
Neither does a constructor for HttpClient, which is a PROTOCOL left for
you to implement. One command shows both halves of that:
grep -nE 'type HttpClient|implement HttpClient' core/net/http.vr
# 722:/// implement HttpClient for SimpleHttpClient { <- a doc comment
# 728:public type HttpClient is protocol {
The only implement in the file is inside ///, so it compiles to
nothing.
What core/net/weft/spiffe.vr ships is the INBOUND side: Principal,
TrustBundleProvider, AuthMode, SpiffeAuthLayer, SpiffeAuthHandler
and AuthRejection. The shape below is what an outgoing transport would
look like once a client exists.
For service-to-service calls, SpiffeClientTransport uses the
local SVID as a client certificate:
let transport = SpiffeClientTransport.new(provider);
let upstream = HttpClient.new(transport);
let resp = upstream.get("https://billing.internal/api/...").send().await?;
The transport handles SVID rotation transparently — when the
underlying Principal rotates, the transport rebuilds its TLS
config and the next connection picks up the new SVID.
HBONE-aware mode for service-mesh ambient deployments
When the request arrives wrapped in an HBONE tunnel (Istio ambient data-plane), the layer detects the wrapper signature and:
- Decapsulates the inner HTTP CONNECT plus mTLS.
- Validates the inner SVID against the trust bundle.
- Provides the inner SVID as
Principal, so the application sees the originating workload identity, not the ztunnel mesh identity.
This makes Weft a drop-in waypoint target without sidecar configuration changes.
SPIFFE ID matching
The SpiffeId.matches(pattern) accepts glob patterns:
| Pattern | Matches |
|---|---|
spiffe://prod/ns/billing/sa/svc | exact |
spiffe://prod/ns/billing/sa/* | any service in billing namespace |
spiffe://prod/ns/*/sa/admin | any admin in any namespace |
spiffe://*/ns/billing/sa/svc | billing service in any trust domain |
Use exact matching for production gates; use globs for development or for catch-alls in policy.
Rotation telemetry
On rotation, the layer emits metrics:
weft.spiffe.rotation.success{trust_domain="prod"} 1
weft.spiffe.rotation.duration_ms{trust_domain="prod"} 142
weft.spiffe.rotation.bundle_size_bytes{trust_domain="prod"} 8192
Failed rotations:
weft.spiffe.rotation.failure{trust_domain="prod", reason="connect-refused"} 1
The near-expiry alarm fires when the active SVID is within 10
minutes of expiry without successful rotation:
weft.spiffe.expiry_warning{trust_domain="prod", remaining_seconds=540} 1
Capability narrowing through Principal
Compile-time capability narrowing does not exist. There is no
Principal.with_role, no context transformer of any kind in core/,
and Principal is a plain RECORD rather than a context — nothing
declares context Principal, so it cannot be transformed by
context-system operators.
public type Principal is {
spiffe_id: SpiffeId,
expires_at: Instant,
/// Claims surfaced from JWT-SVID or cert extensions.
/// Key case-preserved; first wins on duplicates.
claims: Map<Text, Text>,
};
The check is a RUNTIME one today, written in the handler:
async fn admin_handler(p: &Principal) -> Response {
match p.claims().get("role") {
Maybe.Some(r) if r == "admin" => { /* ... */ }
_ => return Response.new(StatusCode.new(403)),
}
}
Principal offers spiffe_id(), expires_at(), claims() and
matches(pattern) — the last does SPIFFE-ID pattern matching, which is
the identity check the library does provide. Enforce the role in a layer
if you want it applied uniformly; the compiler will not enforce it for
you.
Status
- Implementation: complete (X.509 + JWT validation, TrustBundleProvider, rotation signal, HBONE-aware mode).
- Conformance:
spiffe_basictest passing. - Phase: 6 closed.
- Out of scope: Federation across multiple trust domains is a Phase 7 follow-up.