Feature Specification: Resource secrets
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code of core/resources/secrets/ and of the forgejo and forgejo-runner components at 093e68b6f, pull requests #1184, #1206, #1238 and #1255.
User Scenarios & Testing (mandatory)
The secrets resource puts secrets that are encrypted in the repository on a node. It wraps the fileSecrets contract and routes its own secrets to the agenix provider. A consumer reads the file location from the contract result. Secrets that the deployment generates use the generateFiles resource, which reaches the same contract without this resource. The shared resource rules are in the resource family spec.
User Story 1 - Provide a stored secret to an application (Priority: P1)
A component needs a secret that a person creates, for example a database password or a registration token. The component requests the secret by name, with its owner, group, mode and encrypted source file. The resource has no default source, because it holds the secrets of no deployment. The policy wants the secret on the fileSecrets contract and routes it to agenix. agenix decrypts the source on the node at activation.
Why this priority: the forgejo and forgejo-runner components cannot start without their stored secrets.
Independent Test: unit tests of the result location, the route to agenix, the flow of owner, group and mode, and the source file; a VM test that checks owner, group, mode and content of the decrypted file on a running node.
Acceptance Scenarios:
- Given a component that requests a secret, When the node evaluates, Then the result location is unique for the pair of application and secret name.
- Given a request with an owner and a mode, When the node evaluates, Then the decrypted file has that owner, the group of that owner, and that mode.
- Given a request with a source file, When the node evaluates, Then
agenixdecrypts that file.
User Story 2 - Leave other secret providers alone (Priority: P1)
A node can also consume fileSecrets from another provider, for example vars through generateFiles. This resource routes each of its secrets on its own, as a default. It sets no node-wide default provider. When no component requests a secret, the policy gives nothing and does not load agenix.
Why this priority: a node-wide default would take over the routing of other fileSecrets consumers on the node.
Independent Test: unit tests that the node has no node-wide default provider, and that two applications get separate keys.
Acceptance Scenarios:
- Given a node with
agenixsecrets, When it evaluates, Then thefileSecretscontract of the node has no default provider. - Given two applications, When they request secrets, Then each secret has its own key per application and name.
- Given no request, When the policy runs, Then it gives an empty module.
User Story 3 - Encrypt a secret to the machines that consume it (Priority: P2)
A person encrypts the secret files with the agenix tool. The recipients of each file are all contributor keys, the cd key, and the host keys of the machines that consume it. The repository derives the consumers: for each machine of each in-tree group, it evaluates the component that the machine runs, with the configuration that the group gives it (or enabled when the group gives none). When the requests of that component include this secret, the machine is a consumer, and its host key, found by the network name of the machine, is a recipient. The host keys of a secret are in a stable order. The OpenPGP key of the security contact and its passphrase have no consumer, and go only to the contributor keys.
Why this priority: the secret files are encrypted to a fixed recipient set. A recipient list that a person writes by hand can give a secret to a machine that does not consume it, or keep it from a machine that does.
Independent Test: unit tests that the derived recipients match the recipients that the files are encrypted to, that each file has recipients, that a machine without a consumer loses its host key, that a machine with a consumer gets it, that a disabled component has no recipients, and that a consumer without a host key fails.
Acceptance Scenarios:
- Given the in-tree groups, When the recipients evaluate, Then the
forgejosecrets go to the host key of theforgejomachine, and the runner token goes to the host keys of theforgejo-runnermachines, each with the contributor keys and thecdkey. - Given a runner machine that runs another component, When the recipients evaluate, Then the runner token does not go to that machine.
- Given a group that disables
forgejo, When the recipients evaluate, Then no secret offorgejogoes to a machine of that group. - Given a consumer machine with no host key, When the recipients evaluate, Then the evaluation fails and names the machine.
User Story 4 - Roll out a rotated secret (Priority: P3)
A person encrypts a secret file again with a new value. The result location does not change, so no unit changes. A consumer that must see the new value makes its units restart when the ciphertext changes. forgejo-runner does this for each runner unit.
Why this priority: without such a trigger, a rotated secret reaches the consumer only at its next restart.
Independent Test: a unit test that a new ciphertext changes every runner unit.
Acceptance Scenarios:
- Given a new runner token file, When the runner node evaluates, Then every runner unit restarts on the change of the ciphertext.
Edge Cases
- The group of a secret defaults to its owner. The mode defaults to readable by the owner only.
- The policy keeps only the requests of this resource and drops applications with no secrets.
- The routing of the policy can send one secret to another provider, because the route per secret is a default.
- A node can decrypt a secret only when its host key is a recipient.
- A change to the roster or the components of a group can change the recipients of a secret. The files keep the recipients that they were encrypted to until a person rekeys them, and the recipient test fails until then.
Requirements (mandatory)
Functional Requirements
- FR-001: The request MUST accept a set of named secrets, each with an owner, a group, a mode and a source file.
- FR-002: The policy MUST accept a routing module, which is empty by default.
- FR-003: The policy MUST group the requests by application, then by secret name, and keep the application key.
- FR-004: The policy MUST give an empty module when no secret is requested.
- FR-005: Otherwise, the policy MUST import its routing, give the grouped secrets to the node as data, want each secret on the
fileSecretscontract with its owner, group and mode, and route each secret toagenixas a default. - FR-006: The
agenixprovider MUST decrypt each secret to a location that is unique per application and name, with the requested owner, group and mode, and MUST give that location as the result. - FR-007: The setup MUST give this policy to every node, for every deployment method.
- FR-008: Each secret MUST be encrypted to all contributor keys, the
cdkey, and the host keys of its consumers. - FR-009: The repository MUST derive the consumers of a secret from the requests of the components that the machines of the in-tree groups run, and MUST find each host key by the network name of the machine. The derivation MUST fail when a consumer has no host key.
Key Entities
fileSecretscontract: the request is the owner, the group and the mode; the result is the file location.agenixprovider: per application and name, the source file and the name of the decrypted file.- Recipient map: the recipients of each secret file, read by the
agenixtool. - Consumer map: the host keys per secret file. It evaluates without a package set, so the
agenixtool can read it with a plain evaluation.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: the unit tests of the resource pass.
- SC-002: the VM test of a decrypted secret passes.
- SC-003: the unit tests of the recipients pass.
Assumptions
- Each consuming node has an SSH host key that is a recipient of its secrets.
- A person edits and encrypts the secret files with the
agenixtool.
Known Deviations
None.