Status
Accepted on 2026-03-08 by Jan Ivar Beddari.
Context
Infrastructure configuration managed through OpenTofu requires secrets such as API tokens. These secrets must be stored in version control safely and decryptable only by authorized parties.
The solution must work for a single operator initially, scale to a team later, avoid vendor lock-in, and allow secrets to be replaced individually without decrypting unrelated material.
Decision Drivers
- Secrets must be safe to store in version control.
- Operators should not need to decrypt unrelated secrets when rotating one value.
- Private key material should remain hardware-bound rather than stored on disk.
- The solution should scale from one operator to a team without forcing an immediate external KMS dependency.
Considered Options
SOPS with hardware-bound age recipients and one secret per file
Encrypt each secret independently and decrypt inline during OpenTofu operations using hardware-backed recipients.
Shared encrypted secret bundle
Store multiple secrets together in one encrypted file or one broader secret set.
Immediate centralized secret backend requirement
Require an external centralized secrets system from the first phase.
Decision
Dataverket infrastructure secrets use SOPS for secret files and OpenTofu native encryption for state. Both rely on hardware-bound key sources based on age-plugin-se and or age-plugin-yubikey.
Encryption layers
| What | Encrypted by | Key source |
|---|---|---|
| Secrets such as tokens and credentials | SOPS | age-plugin-se / age-plugin-yubikey |
| OpenTofu state | OpenTofu native state encryption | age-plugin-se / age-plugin-yubikey |
Secret file strategy
Each secret is a separate SOPS-encrypted file under infra/secrets/.
infra/secrets/
├── codeberg-token.enc.yaml
├── mirror-token.enc.yaml
└── ...One secret per file means:
- rotating a secret only requires re-encrypting that one file
- no unrelated secrets need to be decrypted to replace one value
- version control diffs stay narrow and understandable
SOPS integration
The nobbs/sops OpenTofu provider decrypts secrets inline during tofu plan and tofu apply. No wrapper scripts or environment variables are required.
Hardware-bound keys
age-plugin-seand orage-plugin-yubikeyact as SOPS recipients- private key material never exists on disk
- each operator’s public key is listed in
.sops.yaml - hardware-token choice remains an operator-side concern
Planned phase 2
An OpenBao transit key may be added later as an additional SOPS key source so that new team members can authenticate to OpenBao instead of managing individual age recipients only. Existing age keys remain as offline and emergency fallback.
Consequences
Positive
- Secrets and state are safe to commit because they remain encrypted at rest.
- Individual secret rotation becomes straightforward because files are separated.
- The approach avoids an immediate dependency on a cloud KMS.
Negative
- Team membership changes still require recipient management in
.sops.yamluntil a centralized transit backend is added. - The security model depends on operators maintaining working hardware-bound decryption paths.
Neutral
- Migration to OpenBao later is additive rather than a full replacement.
- Operator onboarding can evolve from “add your public key” to “get OpenBao access” without discarding the initial model.
Decision Outcome
Accepted. Infrastructure secrets and OpenTofu state will use encrypted-at-rest workflows based on SOPS, OpenTofu native state encryption, and hardware-bound age recipients.
Related Decisions
- ../../../bootstrap/docs/decisions/016-secrets-and-certificate-lifecycle.md defines the broader platform posture for daily secrets and certificate lifecycle.
Links
- No external links are required for this ADR.
More Information
- The future OpenBao transit phase is an explicit extension path, not a prerequisite for the initial design.
Audit
- 2026-03-08: ADR created and accepted.