Feature Specification: Resource generateFiles

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/resources/generateFiles/default.nix, core/resources/generateFiles/consumer.nix, core/resources/generateFiles/spire-login.nix, core/resources/generateFiles/test.nix, core/setups/wiring/vars-plumbing.nix, core/setups/wiring/resource-routing.nix, core/setups/hosts-common.nix, core/checks/default.nix), pull requests #1184 and #1238, and issues #527, #1205 and #1206.

User Scenarios & Testing (mandatory)

User Story 1 - Generate a secret once and read it on every node (Priority: P1)

A component needs a generated value, for example a password or a key pair. It states an instance with its files and a script. The vars provider runs the script once, stores the files in OpenBao KV, and renders them back on each node that reads them. A rebuild keeps the same value.

Why this priority: most components need secrets that nobody types in. Many of them are shared between nodes.

Independent Test: nix-unit-resources-generateFiles, and the NixOS checks contracts-generatefiles-vars and contracts-generatefiles-vars-openbao.

Acceptance Scenarios:

  1. Given an instance x with a file f, When the deployment is evaluated, Then the node wants contracts.generateFiles.want.x, and the file bridges to contracts.fileSecrets.want.vars.x_f.
  2. Given a component reads the result, When it reads contracts.fileSecrets.results.vars.x_f.path, Then it gets the path of the rendered file.
  3. Given no node has a request, When the deployment is evaluated, Then the resource adds no agent and no want.
  4. Given a script, When the generator runs, Then it writes each file to $out/<file> and reads each dependency from $in/<dependency>/.

User Story 2 - Log a node in to OpenBao with its SPIRE identity (Priority: P1)

A node must authenticate to OpenBao before it can read or write KV. The setup adds spire-login.nix through extraModules. The login service fetches the X.509 SVID of the node from the SPIRE agent and logs in with the cert method. It writes the token and publishes the SVID pair at stable paths. The openbao-agent default instance logs in by itself with cert auto-auth, with that SVID pair and as the same role. upload-vars-openbao, systemd-creds-openbao, the openbao-pki issuing units and the netbox, windmill and valkey scripts use the token.

Why this priority: without a login, no secret renders on the node.

Independent Test: nix-unit-resources-generateFiles checks that the agent uses cert auto-auth with the SVID pair, the role name and the CA, that its vault block has no client certificate, that it starts after the login and that the login does not restart it, that a node without the SVID pair uses token_file, and that the agent restarts past its start limit. integrations-spire-openbao checks that the agent renders a secret with no token file, and that a re-login does not restart the agent.

Acceptance Scenarios:

  1. Given the SPIRE agent is healthy, When vars-openbao-login runs, Then it runs bao login -method=cert name=<loginName> and writes the token atomically to /run/vars-openbao/token.
  2. Given the SPIRE agent stays unhealthy past spireWaitTimeout, When the deadline comes, Then the login exits 3, and systemd does not restart it.
  3. Given a login completes, When systemd-creds-openbao is on, Then the login reloads or restarts it. The login does not restart openbao-agent-default.
  4. Given a new SVID pair, When the agent logs in again at the end of its token TTL, Then it reads the new pair from disk.
  5. Given the login timer, When 30 minutes pass after boot or after the last run, Then the login runs again.

User Story 3 - Grow an instance without new values for the old files (Priority: P2)

An instance gets a new file in a later version of a component. An atomic instance refuses a partial state. An instance with independent = true generates only the missing files and keeps the others.

Why this priority: a new file must not rotate a secret that is in use.

Independent Test: contracts-generatefiles-vars-independent.

Acceptance Scenarios:

  1. Given an independent instance with one stored file, When a second file is added, Then the generator writes only the second file.
  2. Given an atomic instance with a partial state, When the generator runs, Then it refuses to continue.

User Story 4 - Grant a node write or read access to vars it does not generate (Priority: P3)

Some applications write their own vars, or only read the vars of another node. They state writtenInstances or readInstances. The setup grants the matching OpenBao paths and adds no generator.

Why this priority: the grants must follow the requests, not a roster kept by hand.

Independent Test: no check covers writtenInstances and readInstances alone.

Acceptance Scenarios:

  1. Given a request with writtenInstances = [ "x" ], When the rosters are built, Then varsProviderRoster grants the node write on vars/secret/x/.
  2. Given a request with readInstances = [ "x" ], When the rosters are built, Then varsReaderRoster grants the node read on x.

Edge Cases

  • The OpenBao address is https:// and caCertFile is set: the agent gets ca_cert. Otherwise it gets none.
  • clientCertFile and clientKeyFile are not set, as in a test with a static token: the agent uses token_file auto-auth with tokenFile.
  • The SVID pair is not published yet when the agent starts: the login fails and the agent tries again with a max_backoff of 5s. The agent does not exit.
  • The SVID of a login expires before the token: the token renewals fail with a backoff, and near the end of the token TTL the agent logs in again with the pair on disk.
  • A node with no request still needs the agent, for example for the OIDC client secret. The setup adds consumerModule, which has the plumbing and no request.
  • The resource imports consumer.nix by path, so two imports of it merge into one. Each import also sets services.vars-openbao.{address,tokenFile,caCertFile}. All nodes of a deployment use the same values, so the definitions agree.
  • The generator runs with PATH set to runtimeInputs only. A script that uses a program outside runtimeInputs fails at run time with exit 127.

Requirements (mandatory)

Functional Requirements

  • FR-001: The request MUST accept writtenInstances, readInstances and instances.
  • FR-002: An instance MUST accept files, script, runtimeInputs, dependencies and independent (default false).
  • FR-003: A file MUST accept secret (default true), owner (default root), group (default root) and mode (default 0400).
  • FR-004: The policy MUST accept routing, address (default http://127.0.0.1:8200), tokenFile (default /run/keys/vars-openbao-token), caCertFile (default empty) and extraModules, and MUST expose the read-only consumerModule.
  • FR-005: process MUST merge the instances of all apps by instance name.
  • FR-006: apply MUST emit nothing for an empty set of instances.
  • FR-007: The consumer module MUST default generateFiles to the vars provider, varsBackend to openbao and envSecrets to openbao-agent, all with mkDefault.
  • FR-008: When clientCertFile and clientKeyFile are set, the openbao-agent default instance MUST use cert auto-auth with that pair, name set to loginName, reload = true and the ca_cert of the vault block. Otherwise it MUST use token_file auto-auth with tokenFile. Both MUST use a max_backoff of 5s. The vault block MUST NOT name a client certificate. The agent MUST start after network-online.target.
  • FR-009: vars-openbao-login MUST restart on failure every 10 seconds with no start limit, except after exit 3.
  • FR-010: openbao-agent-default MUST restart every 10 seconds with no start limit, because a missing vault.ca_cert stops it.
  • FR-011: upload-vars-openbao and systemd-creds-openbao MUST start after vars-openbao-login.
  • FR-012: The login MUST read the SVID from the socket /run/spire/agent/public/api.sock.

Key Entities

  • Instance: a named set of files with one generator script. It maps to the KV paths vars/secret/<instance>/<file> and vars/public/<instance>/<file>.
  • vars provider: the provider in nix/contracts that runs generators and bridges files to fileSecrets.
  • Consumer module: consumer.nix, the OpenBao agent and the systemd-creds-openbao service of a node.
  • SPIRE login: spire-login.nix. On a deployed node it publishes the SVID pair that the agent logs in with, and writes the token for the units that call OpenBao directly.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix-unit-resources-generateFiles passes.
  • SC-002: contracts-generatefiles-vars, contracts-generatefiles-vars-independent and contracts-generatefiles-vars-openbao pass.
  • SC-003: integrations-apex-key-mint passes: a generator runs under exactly its runtimeInputs.

Assumptions

  • The setup sets address to the mTLS listener of OpenBao on port 8200, not to nginx, because nginx removes the client certificate.
  • The setup sets loginName to certAuthLabel of the node and svidName to the node name. The roster rules are in setups and wiring.
  • postgresql, redis and garage render their credentials through the openbao-agent default instance of this resource.