Feature Specification: Resource spiffe
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/resources/spiffe/default.nix, core/resources/spiffe/spire-agent.nix, core/resources/spiffe/fetch-svid.sh, core/setups/wiring/resource-routing.nix, core/setups/wiring/resource-routing-test.nix, core/setups/hosts-common.nix, the "spiffe".spiffe requests in core/components/), pull requests #1184, #1205, #1238 and #1405.
User Scenarios & Testing (mandatory)
The spiffe resource gives a node a SPIFFE workload identity. It puts a
spire-agent on the node that attests with the TPM of the host. Consumers
log in to OpenBao with the X.509 SVID of the agent. The resource wraps no
contract and has one implementation. The shared resource rules are in the
resource family spec.
User Story 1 - Give a node an identity (Priority: P1)
A component must log in to OpenBao, for example to read generated secrets
(see generateFiles) or to get a certificate from the
OpenBao PKI (see ssl). The component requests
"spiffe".spiffe.enable = true. apply returns a consumerSide module that
imports spire-agent.nix and the policy routing, and enables the agent.
The agent serves the Workload API on /run/spire/agent/public/api.sock.
Why this priority: without an identity, a node cannot read its secrets from OpenBao.
Independent Test: nix-unit-setups-operator-config checks that each
operator node runs the spire-agent that it logs in to OpenBao with.
nix-unit-resources-spiffe checks that an enabled request gives one agent
with the context of the policy routing, and that no request gives nothing.
integrations-spire-openbao tests the login end to end.
Acceptance Scenarios:
- Given a component with
"spiffe".spiffe.enable = true, When its node evaluates, Thenservices.spire.agentis enabled with thetpmnode attestor and thesystemdandunixworkload attestors. - Given a node with no
spifferequest, for example thespireserver node, When it evaluates, ThenconsumerSideis empty and the node has no agent. - Given an enabled agent, When the node evaluates, Then
spire-tpm-pluginis installed and tmpfiles give/dev/tpm0and/dev/tpmrm0to thetssgroup.
User Story 2 - Point the agent at the deployment's server (Priority: P1)
The agent must attest to the spire-server of its deployment.
core/setups/hosts-common.nix sets "spiffe".spiffe.routing with mkDefault
to the spiffe row of core/setups/wiring/resource-routing.nix. The row
sets serverAddress to spireServerHost. serverPort and trustDomain
keep their defaults from core/resources/spiffe/server-coordinates.nix,
which the server reads too. spireServerHost is the host of the node
that provides spiffe in the group. A group without such a node uses the
spiffe provider of the hosting-provider group, else spire.<apex>.
Why this priority: an agent that reaches no server gets no identity.
Independent Test: nix-unit-setups-resource-routing, test
test-secret-generation-and-spiffe-route-unconditionally.
Acceptance Scenarios:
- Given a group with a node that provides
spiffe, When the routing evaluates, ThenserverAddressis the address of that node. - Given a group without a node that provides
spiffe, When the routing evaluates, ThenserverAddressis the FQDN of thespiffeprovider of thehosting-providergroup, elsespire.<apex>.
User Story 3 - Give the agent its trust bundle over an authenticated channel (Priority: P2)
The SPIRE CA is minted at run time, so no build can contain it, and the server
rotates it about daily, so no value can be pinned. The deploy gives it to the
node. register-ek.sh (core/effects/tf/incus/host/register-ek.sh) reads
/var/lib/spire-server/ca-bundle.pem on the spire node and writes it to
/var/lib/spire-agent/trust-bundle.pem on the node, over incus exec and
ssh, which the deployer authenticates. It does this when it registers the
node, and again on each apply (terraform_data.trust_bundle, with
bundle_only). On the hypervisor host, the host-register-ek timer does it
every 5 minutes. The agent does not fetch the bundle from the network.
The oneshot spire-agent-trust-bundle runs before spire-agent.service. It
does not get the bundle and does not wait for it. It removes persisted agent
data that no longer chains to the current bundle. When the server gets a new
CA, a running agent fails its next sync and stops, and systemd starts it again
(Restart=on-failure). At that start this unit removes the old SVID, so the
agent attests again with the bundle that the deploy delivered (see
spire, User Story 4).
Why this priority: the agent cannot attest without the CA of the server, and a bundle from an unauthenticated source lets a peer on the path become the trust root of the node.
Independent Test: integrations-register-ek-evidence tests the delivery.
integrations-spire-incus tests that a deployed container has the server's
bundle. integrations-cold-start tests a node that activates before it has a
bundle.
Acceptance Scenarios:
- Given a bundle on the spire node that holds at least one valid CA, When
register-ek.shruns, Then it writes the bundle to the node through a temporary file andmv, with mode0444, and restartsspire-agentwhen the agent is not active. - Given a bundle on the spire node that is empty, holds a block that does not parse, or holds no CA that is valid now, When
register-ek.shruns, Then it exits with an error and the node keeps the bundle it has. - Given the same bundle on the node and on the spire node, When
register-ek.shruns, Then it changes nothing on the node. - Given no bundle on disk, When the oneshot runs, Then it logs
no trust bundle at <path>and exits0, so activation completes. - Given a persisted SVID that does not verify against the current bundle, When the oneshot runs, Then it removes
/var/lib/spire/agent/agent-data.json, so that the agent attests again.
Edge Cases
- The agent has no
StartLimitBurst. While no server exists, it restarts without limit, by design. - The oneshot has no
RemainAfterExit, so each agent restart runs it again. TimeoutStartSecof the oneshot is60. The oneshot does not wait, so this bounds only a tool that hangs.- A container that is offline across a CA rotation gets the current bundle on the next apply. The hypervisor host gets it from the
host-register-ektimer within 5 minutes. - A VM, or a node that the deploy reaches over
ssh, gets no EK registration and no bundle from the deploy. - An agent can hold more than one SVID.
fetch-svid.shselects the SVID forhost/<name>, else one*-<name>match for a namespaced deployment.
Requirements (mandatory)
Functional Requirements
- FR-001: The request MUST accept
enable(defaultfalse). - FR-002: The policy MUST accept
routing, a deferred module (default{ }). - FR-003:
applyMUST returnconsumerSidewith one module when any request hasenable = true, and an empty list otherwise. - FR-004: The module MUST import
spire-agent.nixand the policyrouting, and MUST setfediversity.spire-agent.enable = true. - FR-005:
spire-agent.nixMUST acceptserverAddress,serverPort(default8081),healthWaitTimeout(default300) andtrustDomain(defaultfediversity). - FR-006: The agent MUST use the
memorykey manager, thetpmnode attestor and thesystemdandunixworkload attestors. - FR-007:
spire-agent-trust-bundleMUST run beforespire-agent.serviceand MUST NOT get the bundle from the network. - FR-010:
register-ek.shMUST write the trust bundle to the node only when each block in it parses and at least one CA in it is valid now. - FR-008:
fetch-svid.shMUST be the one SVID selection rule forgenerateFiles/spire-login.nixandssl/openbao-pki-login.nix. - FR-009:
core/setups/hosts-common.nixMUST add theconsumerSideof theprocessoutput to theimportsof each node.
Key Entities
spire-agent: the SPIRE agent on the node, with its Workload API socket.- Trust bundle: the CA bundle of the
spire-server, which the deploy writes to the node. - SVID: the X.509 identity
spiffe://fediversity/host/<name>that the agent issues.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix-unit-setups-resource-routingandnix-unit-resources-spiffepass. - SC-002:
nix-unit-setups-operator-configpasses. - SC-003:
integrations-spire-openbao,integrations-spire-incus,integrations-cold-startandintegrations-register-ek-evidencepass.
Assumptions
- The host has a TPM 2.0 device, or a vTPM on incus.
spire-bundle-exporton the spire node writes the CA bundle to/var/lib/spire-server/ca-bundle.pem.- The node identity is registered at the server. The
spirecomponent does this, not this resource.
Known Deviations
None.