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:

  1. Given a component with "spiffe".spiffe.enable = true, When its node evaluates, Then services.spire.agent is enabled with the tpm node attestor and the systemd and unix workload attestors.
  2. Given a node with no spiffe request, for example the spire server node, When it evaluates, Then consumerSide is empty and the node has no agent.
  3. Given an enabled agent, When the node evaluates, Then spire-tpm-plugin is installed and tmpfiles give /dev/tpm0 and /dev/tpmrm0 to the tss group.

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:

  1. Given a group with a node that provides spiffe, When the routing evaluates, Then serverAddress is the address of that node.
  2. Given a group without a node that provides spiffe, When the routing evaluates, Then serverAddress is the FQDN of the spiffe provider of the hosting-provider group, else spire.<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:

  1. Given a bundle on the spire node that holds at least one valid CA, When register-ek.sh runs, Then it writes the bundle to the node through a temporary file and mv, with mode 0444, and restarts spire-agent when the agent is not active.
  2. 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.sh runs, Then it exits with an error and the node keeps the bundle it has.
  3. Given the same bundle on the node and on the spire node, When register-ek.sh runs, Then it changes nothing on the node.
  4. Given no bundle on disk, When the oneshot runs, Then it logs no trust bundle at <path> and exits 0, so activation completes.
  5. 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.
  • TimeoutStartSec of the oneshot is 60. 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-ek timer 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.sh selects the SVID for host/<name>, else one *-<name> match for a namespaced deployment.

Requirements (mandatory)

Functional Requirements

  • FR-001: The request MUST accept enable (default false).
  • FR-002: The policy MUST accept routing, a deferred module (default { }).
  • FR-003: apply MUST return consumerSide with one module when any request has enable = true, and an empty list otherwise.
  • FR-004: The module MUST import spire-agent.nix and the policy routing, and MUST set fediversity.spire-agent.enable = true.
  • FR-005: spire-agent.nix MUST accept serverAddress, serverPort (default 8081), healthWaitTimeout (default 300) and trustDomain (default fediversity).
  • FR-006: The agent MUST use the memory key manager, the tpm node attestor and the systemd and unix workload attestors.
  • FR-007: spire-agent-trust-bundle MUST run before spire-agent.service and MUST NOT get the bundle from the network.
  • FR-010: register-ek.sh MUST 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.sh MUST be the one SVID selection rule for generateFiles/spire-login.nix and ssl/openbao-pki-login.nix.
  • FR-009: core/setups/hosts-common.nix MUST add the consumerSide of the process output to the imports of 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-routing and nix-unit-resources-spiffe pass.
  • SC-002: nix-unit-setups-operator-config passes.
  • SC-003: integrations-spire-openbao, integrations-spire-incus, integrations-cold-start and integrations-register-ek-evidence pass.

Assumptions

  • The host has a TPM 2.0 device, or a vTPM on incus.
  • spire-bundle-export on the spire node writes the CA bundle to /var/lib/spire-server/ca-bundle.pem.
  • The node identity is registered at the server. The spire component does this, not this resource.

Known Deviations

None.