Feature Specification: Component spire
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/components/hosting-provider/spire/default.nix, core/components/hosting-provider/spire/spire.nix, core/components/hosting-provider/spire/retire-name.sh, core/effects/tf/incus/host/retire-ek.sh, core/resources/spiffe/default.nix, core/resources/spiffe/spire-agent.nix, core/setups/wiring/spire-roster-test.nix, core/checks/default.nix, core/checks/nix-unit-tests.nix).
User Scenarios & Testing (mandatory)
User Story 1 - Give each host a SPIFFE identity from its TPM (Priority: P1)
Nodes of a deployment need a workload identity to log in to OpenBao. The
spire component runs the SPIRE server for the trust domain fediversity.
The server attests agents with the TPM NodeAttestor in EK-hash allow-list
mode. For each known host, spire-register allow-lists the EK hash and
creates the entry spiffe://fediversity/host/<name>. The agents on the
consumer nodes come from the spiffe
resource. The component declares provides = [ "spiffe" ], so the agents
of the group attest to this node.
Why this priority: without an entry, the agent gets no SVID, and every
OpenBao cert login from that node fails (see openbao).
Independent Test: integrations-spire-openbao and
integrations-spire-incus. The second test attests an incus-deployed node
through its incus vTPM (see incus).
Acceptance Scenarios:
- Given
consumerHostHashes = { host-a = "<hash>"; }, When the node boots, Then tmpfiles seeds/var/lib/spire-server/host-ek-hashes/host-a, andspire-registercreates thehost/host-aentry. - Given a deploy that drops a
<name>file with an EK hash into/var/lib/spire-server/host-ek-hashes, Whenspire-registerruns, Then it writes the allow-list marker into/var/lib/spire-server/ek-hashesand creates the entry with parentspire/agent/tpm/<hash>and selectorunix:uid:0. - Given a host whose EK hash changes, When
spire-registerruns, Then it re-points the entry to the new parent and removes the old marker. - Given two drop-files with the same EK hash, When
spire-registerruns, Then it keeps the newest name and retires the others.
User Story 2 - Retire identities of removed hosts (Priority: P2)
A host that leaves the deployment must lose its identity. Otherwise anything
that holds its EK keeps the identity. spire-register retires each drop-dir
name that is absent from hostRoster and otherDeploymentRoster. A
deployment that removes its own instances calls spire-retire-name on
destroy, through retire-ek.sh.
Why this priority: a stale identity is a security gap, but it does not stop a live deployment.
Independent Test: integrations-spire-incus retires the old name of a
renamed node and a node absent from hostRoster.
nix-unit-setups-spire-roster and nix-unit-setups-spire-registration test
the rosters that the setups render.
Acceptance Scenarios:
- Given a non-empty
hostRoster, When a drop-dir name is in neither roster, Thenspire-registerretires it. - Given
otherDeploymentRoster = [ "app" ], When the drop-dir holdsabc123-app, Thenspire-registerkeeps it. - Given both rosters empty, When
spire-registerruns, Then it retires no name. - Given two names with the same EK hash, When
spire-retire-nameremoves one, Then the allow-list marker stays for the other.
User Story 3 - Publish the trust bundle (Priority: P3)
Agents and other consumers need the CA bundle of the server. The
spire-bundle-export unit writes the bundle every 15 minutes. The deploy reads
it from /var/lib/spire-server/ca-bundle.pem and gives it to each agent (see
the spiffe resource). The component does
not serve the bundle on the network.
Why this priority: an agent needs the bundle to attest, and keeps it current over its own stream after that.
Independent Test: integrations-cold-start. A node that deploys before
its SPIRE server exists still finishes activation.
Acceptance Scenarios:
- Given a running server, When
spire-bundle-exportruns, Then it writes/var/lib/spire-server/ca-bundle.pem. - Given a host that the deploy registers, When
register-ek.shruns, Then it reads the bundle on this node and writes it to thetrust_bundle_pathof the agent on that host.
User Story 4 - Recover after the server loses its state (Priority: P2)
An operator recovers the fleet after the server loses its state
(/var/lib/spire/server, which holds the datastore and keys.json, or
/var/lib/spire-server). There is no backup to restore (see Assumptions). The
server starts with a new key, a new CA and an empty datastore, and each agent
and OpenBao still trust the old CA.
Recovery steps, on the server node unless a step says otherwise:
- Start the server, or redeploy its node. tmpfiles makes the state
directories and the drop-files of
consumerHostHashesagain. - Run
systemctl start spire-bundle-export.service, so that/var/lib/spire-server/ca-bundle.pemholds the new CA now and not after the next timer run (at most 15 minutes). - Run
systemctl restart spire-register.servicewhen the drop-files in/var/lib/spire-server/host-ek-hashesare still there but the datastore is not.register-ek.shdoes not restartspire-registerfor a drop-file that holds the same hash, so without this step the entries stay absent until the next change of a drop-file. - Apply each deployment that registers hosts with this server. This includes
each deployment that owns the hosts of
otherDeploymentRoster. The apply writes the new bundle to thetrust_bundle_pathof each agent (terraform_data.trust_bundle). On the hypervisor host, thehost-register-ektimer does this within 5 minutes. As an alternative to an apply, copy/var/lib/spire-server/ca-bundle.pemto/var/lib/spire-agent/trust-bundle.pemon each node, with mode 0444, as a temporary file that you move into place.
No step on the agent nodes is necessary after the bundle arrives. A running
agent fails its next sync with the server (an unknown authority), and SPIRE
stops the agent, because rebootstrap_mode is never. systemd starts it
again (Restart=on-failure), spire-agent-trust-bundle removes the cached
SVID that does not chain to the new bundle, and the agent attests again. Until
the bundle arrives, the agent stops and starts in a loop.
OpenBao follows without a step: openbao-spire-ca-bundle runs when the
bundle of its own agent changes, waits for that agent to attest to the new CA
(see openbao), installs the bundle and re-seeds the cert
auth roles. The listener needs no reload. Until then, each cert login fails
and each node keeps its current token until that token expires.
Why this priority: until the recovery is complete, no node can log in to OpenBao, so no node gets a new secret or a new token.
Independent Test: the subtest "The fleet recovers after the SPIRE server
loses its state" of integrations-spire-openbao deletes the state of the
server, does steps 1 to 4, and makes sure that the running agent gets an SVID
from the new CA with no step on its node, and that a cert login succeeds
after OpenBao has the new bundle. integrations-openbao-spire-ca-bundle tests
the wait of openbao-spire-ca-bundle.
Acceptance Scenarios:
- Given a server with a new CA, and an agent that runs and has the new bundle at its
trust_bundle_path, When the agent syncs with the server, Then it stops, starts again, and holds an SVID that chains to the new bundle, with no step on its node. - Given OpenBao with the old bundle, When a node logs in with an SVID from the new CA, Then the login fails and the node keeps its current token.
- Given OpenBao with the new bundle and re-seeded
certauth roles, When a node logs in, Then it gets a new token, and the credential daemon reads secrets with it.
Edge Cases
- The server is not up yet:
spire-registerwaits in a health-check loop. On failure it restarts after 30 seconds, with no start limit (StartLimitIntervalSec=0). spire-serverfails:spire-registeronlywantsit, so it does not stop with it.- A retire runs twice:
spire-retire-nameis idempotent. - A name with an operator namespace prefix is gone but matches
otherDeploymentRoster: the name stays until its owning deployment retires it. - The server loses its state: see User Story 4 for the recovery steps.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
components.spirewithname = "SPIRE",package = pkgs.spire, and animplementationthat importsspire.nix. - FR-002: The server MUST use the trust domain
fediversity, listen on port 8081, use the SQLite datastore and the disk key manager. - FR-003: The server MUST attest agents with
NodeAttestor.tpmandhash_path/var/lib/spire-server/ek-hashes. - FR-004: The drop-dir
/var/lib/spire-server/host-ek-hashesMUST have mode 0700, and the allow-list dir MUST have mode 0755. - FR-005:
consumerHostHashesMUST seed one drop-file for each host through tmpfiles. - FR-006:
spire-registerMUST be a oneshot withRemainAfterExit,Restart=on-failureand a 30 second delay. - FR-007:
spire-registerMUST, in order, wait for server health, prune by roster, retire duplicate-EK names, and reconcile entries. - FR-008: Each entry MUST be
spiffe://fediversity/host/<name>with parentspire/agent/tpm/<hash>and selectorunix:uid:0. - FR-009: The prune MUST match
hostRosterexactly andotherDeploymentRosteras the entry or*-<entry>, and MUST be off when both are empty. - FR-010:
spire-retire-name <name> [reason]MUST delete the entry and the drop-file, and remove the marker only when no other drop-file holds that hash. - FR-011:
spire-retire-nameMUST be onenvironment.systemPackages. - FR-012:
spire-bundle-exportMUST run from a timer every 15 minutes. - FR-013: The component MUST NOT serve the bundle on the network. The deploy reads it over
incus execorssh. - FR-014: The component MUST declare
provides = [ "spiffe" ], so the setups route thespiffeserver address of the group to this node.
Key Entities
- Drop-file:
/var/lib/spire-server/host-ek-hashes/<name>, which holds the EK hash of one host. - Allow-list marker: a file in
/var/lib/spire-server/ek-hashesthat names an EK hash the TPM attestor accepts. - Host entry:
spiffe://fediversity/host/<name>, the SPIFFE ID of one host. - Host roster (
hostRoster): the host names this deployment provisions. - Other-deployment roster (
otherDeploymentRoster): name stems that other deployments register under a namespace prefix. - Trust bundle:
/var/lib/spire-server/ca-bundle.pem, which holds the current CA and the prepared next CA.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
integrations-spire-openbaoandintegrations-spire-incuspass. - SC-002:
integrations-cold-startpasses. - SC-003:
nix-unit-setups-spire-rosterandnix-unit-setups-spire-registrationpass.
Assumptions
- The deploy writes drop-files for hosts that it provisions at deploy time, through
register-ek.sh. - The setups render
hostRosterandotherDeploymentRosterfrom the full group roster (see setups and wiring). - Consumer nodes run a SPIRE agent from the
spifferesource and have a TPM 2.0. - OpenBao trusts the bundle of this server for cert logins. It gets the bundle from its own agent (see
openbao). - The server has no offsite backup. Its state is the datastore and
keys.json, and the deploy makes all of the datastore again. A copy ofkeys.jsonis a copy of the CA key, which can sign an SVID for each host, and thus get each secret that a host can read in OpenBao. All restic repositories use the same password, thus a backup would give this key to each node that can read a backup bucket. The CA has a lifetime of 24 hours, thus a daily copy is near its end or expired when it is restored. The server node has no SPIRE agent, thus it cannot get the S3 key of a bucket from OpenBao KV.
Known Deviations
spire.nixhas a TODO: the datastore is SQLite and not PostgreSQL.- The
*-<entry>match inotherDeploymentRosterkeeps names that no deployment holds, until the owning deployment retires them.