3 26 ADR 006 Scoped Secret Files
Nimmo edited this page 2026-09-13 13:14:49 +01:00

ADR-006: Scope encrypted secret files to services, hosts, and real consumer sets

  • Status: Accepted
  • Date: 2026-07-29
  • Scope: SOPS file boundaries, age recipients, host identity, recovery access, and runtime secret delivery
  • Supersedes: Monolithic secrets/secrets.yaml
  • Superseded by: None

Context

The original SOPS layout placed many unrelated credentials in one secrets/secrets.yaml and granted every managed host access to the encrypted file. Encryption protected the repository copy, but a host able to decrypt one required value could also decrypt credentials for unrelated hosts and services.

SOPS recipient policy applies to a file. Keeping unrelated YAML keys in one encrypted document therefore prevents meaningful least-privilege recipient scope. Conversely, creating arbitrary tiny files without regard to actual consumer sets would add rotation and discovery work without reducing access.

Forgejo issue #80 migrated the monolith into service and host files, added consumer-specific .sops.yaml rules, and retained administrator recovery identities. Later service work refined the same boundary: Karakeep's Vega-only runtime environment was separated from MCP credentials needed by AI clients, and Trilium's Vega-only OIDC credentials were separated from client ETAPI credentials.

The current tree still contains a broad fallback rule and a shared rule for several legacy split files. It also grants Vega and Lyra access to nebula-ca.yaml despite an inline statement that servers are excluded. These are transitional implementation exceptions to the accepted decision, not a reason to describe recipient scoping as complete.

Decision

Store repository-managed secrets in SOPS-encrypted files whose boundaries match a real service, host identity, or shared consumer set.

  • A host-specific network identity or agent credential gets a host-specific file, such as secrets/nebula/<host>.yaml or secrets/beszel/<host>.yaml.
  • A service used by one host gets a service file decryptable by that host and the designated recovery administrators.
  • A credential genuinely consumed by several hosts may share one file whose recipients are exactly those consumers. Shared Backrest and notification credentials are examples; “shared” must not mean “the whole fleet by default.”
  • Credentials with different privilege or consumer sets must be separated even when they relate to the same application. Server runtime/OIDC credentials and client API/MCP credentials are separate boundaries.
  • Every scoped file retains deliberate administrative recovery recipients. The two YubiKeys provide routine and backup editing access, while the provisioning identity provides an off-host bootstrap/recovery path. Hosts are consumers, not the only editors.
  • A new host is added only to the rules and encrypted files it needs. Host retirement removes its recipient from every affected file and re-encrypts those files; deleting a public key from .sops.yaml alone does not revoke an existing encrypted stanza.

Host SSH Ed25519 keys remain the ordinary sops-nix decryption identities because they are unique per machine, root-protected, and available before /home is mounted. Private keys and plaintext secret values never enter Git or the Nix store.

At activation, sops-nix materialises plaintext under /run/secrets. Services consume files, protected environment files, SOPS templates, or systemd credentials as appropriate. Prefer a private systemd credential over a process environment or command-line value when the service supports it.

The broad catch-all creation rule is a migration safety net only. New files must receive an explicit rule before use; the catch-all is not an accepted long-term consumer scope.

Consequences

  • Compromise of one host does not inherently grant every repository-managed credential; exposure is bounded by the files for which that host is a recipient.
  • Secret inventories are easier to relate to service ownership and retirement, but .sops.yaml and module references require more deliberate maintenance.
  • Moving a key between files is a real migration: update consumers, creation rules, encrypted data, validation, and deployment evidence together.
  • A newly created encrypted file must be staged before flake evaluation because untracked paths are invisible to the flake.
  • Recipient rotation must enumerate the affected tracked files and verify decryption with the new recovery path before removing the old one.
  • Shared administrator recipients increase recoverability and create a deliberate high-value trust boundary. Loss or compromise of one requires a scoped recipient rotation.
  • Runtime file ownership and delivery remain separate from encryption recipients. A host's ability to decrypt a file does not justify making every plaintext key readable by every local service or user.
  • The current fallback, legacy shared rules, and Nebula CA recipient mismatch remain visible policy debt under the Security Baseline.

Alternatives considered

Keep one encrypted monolithic file

Rejected. It is convenient to rotate as one object but makes the union of all recipients capable of decrypting the union of all secrets, greatly increasing the consequence of one host compromise.

Duplicate every shared credential into per-host files

Rejected. It creates multiple encrypted copies of the same value and makes coordinated rotation more error-prone without reducing privilege when all those hosts legitimately consume it.

Create one file for every scalar value

Rejected. File boundaries should represent privilege and consumer boundaries, not syntax. Closely related values with the same consumers and lifecycle are safer to rotate and understand together.

Use only host recipients, without administrator recovery identities

Rejected. A failed or retired host could become the sole route to required credentials. Off-host YubiKey and provisioning recipients preserve recovery and allow editing without broadening ordinary host consumption.

Put secrets directly in Nix or generated store files

Rejected. The Nix store is not a secret store and is broadly readable. Nix may refer to encrypted inputs and runtime paths, but plaintext values must be materialised outside the store.

Adopt a separate online secret manager for all values

Not selected. It could centralise revocation and auditing but would add an availability, bootstrap, authentication, and recovery dependency. SOPS plus host identities fits the declarative, Git-backed fleet while remaining usable during control-plane outages.

References

  • Required controls and current exceptions: Security Baseline
  • Current operating procedure: Secret Editing and Recipient Rotation; implementation patterns: Secrets Implementation Conventions.
  • Source-level implementation detail: secrets/README.md
  • Current source: .sops.yaml, modules/common/sops-host.nix, scoped files under secrets/, and service sops.secrets, sops.templates, and LoadCredential declarations
  • Migration decision and acceptance evidence: Forgejo issue #80 and pull request #122
  • Monolith split and removal: repository commits 1f227a0ca08d6b7e413f9125a66df56d06311d8f and f7251bc85e66e41d858ff774bfd18aac2271ab42
  • Recipient narrowing: repository commit 875ac08927aa55a8783c4657c87a6cf7a16e8877
  • Later privilege-boundary refinements: repository commits 54b6a6e1a2fcaecb001e050efc61e5c131a2e733 (Karakeep) and 958fb1fe81aa373d437ca8bb1b36434d2311927d (Trilium)