32. Vendor Data JWT Attestation (SPIRE Integration Plan, Phase 2)
Date: 2026-09-02
Status
Proposed
Reference
Phase 2 of doc/plans/spire-integration.md. Depends on Phase 1 (SPIRE
DevStack plugin, merged) for the per-host nova-compute SPIFFE identity
this endpoint authenticates its caller with, and on a minimal slice of
Phase 3 (the internal SPIFFE mTLS interface, already wired to the REST
router) for the interface this endpoint lives on.
1. Context
Nova injects a keystone-rs-signed JWT into a VM’s vendor_data.json at
boot. A (future, Phase 5) custom SPIRE node attestor plugin running inside
the VM reads that JWT and presents it to the SPIRE server, which verifies
the signature and mints a project/instance-scoped SPIFFE SVID for the VM.
This ADR covers the keystone-rs side of that hand-off: the endpoint that
signs the JWT, and the key it is signed with.
2. Decision
POST /v4/vendordata(internal SPIFFE mTLS interface, port 8444) signs a short-lived (60-600s, default 300s) attestation JWT scoped to aproject_id/instance_idpair, requested by Nova-compute on thevendor_data_urlboot hook.GET /v4/spiffe/{domain_id}/jwkspublishes the public half of the signing key, unauthenticated, for the (Phase 5) SPIRE server plugin to fetch and cache.- Attestation key isolation: the signing key is a second, independent
per-domain asymmetric key
(
openstack_keystone_core::spiffe_key), never the OAuth2 access-token signing key (openstack_keystone_core::oauth2_key, ADR 0026 §3) and never published on the OAuth2 JWKS endpoint. An earlier draft considered publishing both keys in one JWKS set, distinguished by ausemetadata tag; that was rejected because it would make a JWK metadata field (per RFC 7517, meant to distinguish signature vs. encryption keys, not application purpose) the actual security boundary between “can forge API tokens” and “can mint VM identities” – a validator that mishandlesusewould silently accept the wrong key. Endpoint separation removes that failure mode: the SPIRE server plugin only ever fetches/v4/spiffe/{domain}/jwksand can never be tricked into trusting the OAuth2 key. - Ownership verification (Fix 1): before signing, and only when
[vendordata] verify_placementistrue(the default), the handler cross-checks the caller’s claimedproject_id/instance_idagainst Nova’s own record for that instance, using keystone-rs’s own admin-scoped Nova credentials. A mismatch is403; a Nova-side error or timeout is503(fail closed – never a signed JWT on an unverifiable claim). Without this check, any compute host’s SPIFFE SVID could request a signed attestation for an instance it does not run, since the SVID alone doesn’t bind the caller to a specific host/instance pairing.
3. Key design decisions
audis fixed tospiffe-attestation, preventing reuse of an attestation JWT for any other purpose.jtiis a freshly generated random identifier per issued JWT, neverinstance_id– a legitimate re-attestation (VM rebuild) must produce a differentjtifrom the original boot so the (Phase 5) SPIRE server plugin’s anti-replay cache can tell fresh re-attestation apart from true replay.ttl_secondsis capped at[oauth2] access_token_lifetime_minutesas a safety bound, regardless of what the request asks for.- The endpoint lives on the internal SPIFFE mTLS interface because
Nova-compute authenticates with its per-host SPIFFE SVID
(
spiffe://{trust_domain}/service/nova-compute/host/{hostname}, Phase- – there is no other credential Nova-compute could plausibly present.
4. Known gaps and deviations from the plan doc (Phase 2 scope)
This phase intentionally trades some fidelity to the plan doc’s original design for a smaller, self-contained first implementation. Each item below is a deliberate, flagged simplification – not an oversight – and a natural next increment:
- Attestation key storage is filesystem-backed, not Raft-backed.
[oauth2]’s signing key (ADR 0026 §3) is Raft + FjallDB replicated for HA; the attestation key reuses the same genericAsymmetricKeyRepository/KeyMaterialshape fromkey-repository, but throughFilesystemAsymmetricKeySource(the same source the Phase 0 JWS token provider already uses in single-node deployments) rather than a new Raft-backed backend. A single-node keystone-rs deployment works unchanged; multi-node HA needs the same Raft generalization[oauth2]already has, tracked as follow-up work, not part of this ADR. - No emergency rotation or JTI revocation for the attestation key.
[oauth2]’s signing key has a two-phase emergency rotation and JTI revocation list (ADR 0026 §3, §11); the attestation key’s provider trait (SpiffeKeyApi) is deliberately smaller –ensure_domain_keys,jwks,active_signing_keyonly. Attestation JWTs are already short-lived (max 600s) and carry no long-lived session state to revoke, so the operational need is smaller than for OAuth2 access tokens; if it grows, the same rotation machinery[oauth2]uses can be generalized to a second purpose. - Nova ownership verification mints its own token per call (resolved:
originally a static, operator-provisioned admin token).
[vendordata] nova_auth_scope(project or system) andnova_auth_roles(role names) configure the scope and effective roles of a token keystone-rs mints for itself on every ownership check, using the same “virtual identity with pre-populated roles” mechanism the SPIFFE/mapping engine (ADR 0020) uses for externally authenticated identities – keystone-rs still has no real user account or role assignment for this identity, so the configured role names are set directly as the token’s effective roles rather than resolved from an assignment lookup (each name must resolve to an existing role, or the check fails closed with503). This removes the long-lived static credential; nothing left to rotate manually. - The compute host is derived directly from the raw
SpiffeIdrequest extension, not through Phase 3’s generic SPIFFE claim-flattening (spiffe.hostincrates/core/src/api/auth.rs) and the ADR 0020 mapping engine. This endpoint parsesspiffe://{trust_domain}/service/nova-compute/host/{hostname}itself, with the same strict path-segment validation the plan doc specifies (reject trailing slashes, extra segments, and any URI that doesn’t match exactly) – but that claim is not yet available to OPA policy or any other handler. Generalizing it into the mapping engine’s claim flattening remains Phase 3 proper.
5. Security Considerations
See doc/plans/spire-integration.md’s “Security Considerations” table for
the full threat model this phase participates in
(“Compute-host impersonation / unauthorized attestation minting”,
“Attestation/OAuth2 key blast-radius coupling”). The devstack join-token
registration path from Phase 1 does not itself prove per-host identity the
way x509pop would in production – this endpoint’s ownership check
narrows, but does not eliminate, that gap in a devstack deployment.
6. Consequences
Positive
- A key compromise no longer grants both API-token forgery and VM identity minting (separate keys, separate endpoints).
- A compromised or malicious compute host cannot mint attestations for
instances it does not run, when
verify_placementis enabled. - Reuses the existing generic asymmetric key-repository machinery rather than inventing a new key storage format.
Negative / Risks
- The filesystem-backed attestation key does not survive a single-node failure the way the Raft-backed OAuth2 key does; multi-node HA for this key is not yet implemented (see §4).
nova_auth_rolesis configured with broad (cross-project) read access viaos-extended-server-attributesin mind; misconfiguring it with an over-broad role name grants every self-minted Nova-calling token that same access, so treat the role list with the same operational care as a policy file, not as an inert config value.