refactor(sops): encrypted files carry the .sops.yaml suffix #320

Merged
dominik.polakovics merged 1 commit from afk/319 into main 2026-08-10 17:19:11 +02:00

Closes #319

The nvim sops rule matched */secrets.yaml and update-secrets-keys discovered its work with find -name "secrets.yaml". Both encode the same guess — that an encrypted file is one called secrets.yaml — and it was wrong in both directions. Ansible's per-server secrets got no editor handling and, worse, no key rotation. And because sops.lua ships in a Neovim config used in every repo, a plaintext secrets.yaml in an unrelated project could not be saved at all: the hook is a BufWriteCmd, so it replaces the write, and with no creation rule matching there was nothing to write.

The filename is now the contract: a file is sops-encrypted iff its name ends in .sops.yaml.

What changed

  • Rename — 17 secrets.yamlsecrets.sops.yaml via git mv. 19 nix sopsFile/defaultSopsFile references and ansible's base_loki_secrets_file retargeted.
  • sops.lua — pattern is *.sops.yaml, with a basename guard skipping the .sops.yaml config itself. The guard now runs in all four autocmds; previously only BufWriteCmd called is_secrets_file(), so BufReadPre/BufReadPost/BufLeave relied on the pattern alone and would still have fired on the config.
  • .sops.yaml — every rule tightened from [^/]+\.yaml$ to [^/]+\.sops\.yaml$.
  • update-secrets-keys — convention-based discovery (find -name "*.sops.yaml" ! -name ".sops.yaml", .git pruned), so ansible secrets enter rotation and future encrypted files need no script edit.
  • ADR-0025 plus the operational docs. Historical ADRs 0002/0005/0021 left as written.

Two things beyond the issue's list

  • scripts/pre-commit:16 — its shared regex contains secrets\.yaml$, the trigger for the all-host dry-build when the root secrets file changes. After the rename it would silently never match again. Retargeted.
  • Live bug fixed as a side effect — at main, the root rule ^[^/]+\.yaml$ matched the sops config file .sops.yaml itself. ^[^/]+\.sops\.yaml$ structurally cannot, since [^/]+ needs at least one character before the literal suffix. The exclusion is now a property of the regex rather than luck.

I also updated the operational docs the issue didn't enumerate (AGENTS.md, README.md, .claude/*.md, .roo/rules/rules.md, ansible/README.md, EMAIL_SETUP.md, RESTORATION.md, land-pr/SKILL.md) — each contains a command or path list that the rename makes wrong. Historical records stay stale; instructions must not.

Verification

sops and nix are both absent from the agent sandbox, so the pre-commit dry-build could not run (/nix does not exist; test-configuration and ansible-check bail on their missing-toolchain checks before evaluating anything) and ./scripts/update-secrets-keys could not be executed. This needs a dry-build on a machine with Nix before merging. What I could establish instead:

  • No re-encryption. All 17 renamed blobs are byte-identical to their HEAD counterparts — verified by comparing git rev-parse HEAD:<old> against the staged object id, not by reading the diff. .sops.yaml's diff is confined to path_regex lines; zero other lines changed.
  • No recipient change. I simulated sops' first-matching-rule semantics over HEAD vs. the worktree for all 18 encrypted files. Every one resolves to the same recipient set. Rule count and key groups unchanged.
  • .sops.yaml no longer self-matches — confirmed empirically against both the old and new rule sets.
  • All 19 sopsFile/defaultSopsFile paths resolve to files that exist, including the symlink-crossing ../utils/modules/victoriametrics/secrets.sops.yaml in fwmetrics.nix. No secrets.yaml remains on disk.
  • sops.lua — no Lua interpreter available either, so no real parse. Block-keyword balance is 0 at HEAD and 0 in the worktree with depth never going negative, and each of the 4 added guard blocks is self-closing.

Not addressed, deliberately

Three encrypted files match no creation rule — hosts/web-arm/modules/{bitwarden,nextcloud}/secrets.sops.yaml and utils/modules/plausible/secrets.sops.yaml. This predates the PR and is unchanged by it (they matched none before either), but it means update-secrets-keys will fail on those three. The first two look dead: both modules take their secrets from web-arm's defaultSopsFile and nothing references the files. Filed separately as #320, along with the stale retired-host rules the issue scoped out.

Two notes: README.md:19 still references hosts/cloonar.com/, a host directory that no longer exists — left alone as unrelated. And .claude/skills/README.md is git-ignored in this worktree, so its edit is local-only and not in this diff.

Closes #319 The nvim sops rule matched `*/secrets.yaml` and `update-secrets-keys` discovered its work with `find -name "secrets.yaml"`. Both encode the same guess — that an encrypted file is one called `secrets.yaml` — and it was wrong in both directions. Ansible's per-server secrets got no editor handling and, worse, no key rotation. And because `sops.lua` ships in a Neovim config used in *every* repo, a plaintext `secrets.yaml` in an unrelated project could not be saved at all: the hook is a `BufWriteCmd`, so it *replaces* the write, and with no creation rule matching there was nothing to write. The filename is now the contract: **a file is sops-encrypted iff its name ends in `.sops.yaml`.** ## What changed - **Rename** — 17 `secrets.yaml` → `secrets.sops.yaml` via `git mv`. 19 nix `sopsFile`/`defaultSopsFile` references and ansible's `base_loki_secrets_file` retargeted. - **`sops.lua`** — pattern is `*.sops.yaml`, with a basename guard skipping the `.sops.yaml` config itself. The guard now runs in **all four** autocmds; previously only `BufWriteCmd` called `is_secrets_file()`, so `BufReadPre`/`BufReadPost`/`BufLeave` relied on the pattern alone and would still have fired on the config. - **`.sops.yaml`** — every rule tightened from `[^/]+\.yaml$` to `[^/]+\.sops\.yaml$`. - **`update-secrets-keys`** — convention-based discovery (`find -name "*.sops.yaml" ! -name ".sops.yaml"`, `.git` pruned), so ansible secrets enter rotation and future encrypted files need no script edit. - **ADR-0025** plus the operational docs. Historical ADRs 0002/0005/0021 left as written. ## Two things beyond the issue's list - **`scripts/pre-commit:16`** — its `shared` regex contains `secrets\.yaml$`, the trigger for the all-host dry-build when the root secrets file changes. After the rename it would silently never match again. Retargeted. - **Live bug fixed as a side effect** — at `main`, the root rule `^[^/]+\.yaml$` matched the sops config file `.sops.yaml` **itself**. `^[^/]+\.sops\.yaml$` structurally cannot, since `[^/]+` needs at least one character before the literal suffix. The exclusion is now a property of the regex rather than luck. I also updated the operational docs the issue didn't enumerate (`AGENTS.md`, `README.md`, `.claude/*.md`, `.roo/rules/rules.md`, `ansible/README.md`, `EMAIL_SETUP.md`, `RESTORATION.md`, `land-pr/SKILL.md`) — each contains a command or path list that the rename makes wrong. Historical records stay stale; instructions must not. ## Verification `sops` and `nix` are both absent from the agent sandbox, so **the pre-commit dry-build could not run** (`/nix` does not exist; `test-configuration` and `ansible-check` bail on their missing-toolchain checks before evaluating anything) and `./scripts/update-secrets-keys` could not be executed. **This needs a dry-build on a machine with Nix before merging.** What I could establish instead: - **No re-encryption.** All 17 renamed blobs are byte-identical to their `HEAD` counterparts — verified by comparing `git rev-parse HEAD:<old>` against the staged object id, not by reading the diff. `.sops.yaml`'s diff is confined to `path_regex` lines; zero other lines changed. - **No recipient change.** I simulated sops' first-matching-rule semantics over `HEAD` vs. the worktree for all 18 encrypted files. Every one resolves to the same recipient set. Rule count and key groups unchanged. - **`.sops.yaml` no longer self-matches** — confirmed empirically against both the old and new rule sets. - **All 19 `sopsFile`/`defaultSopsFile` paths resolve** to files that exist, including the symlink-crossing `../utils/modules/victoriametrics/secrets.sops.yaml` in `fwmetrics.nix`. No `secrets.yaml` remains on disk. - **`sops.lua`** — no Lua interpreter available either, so no real parse. Block-keyword balance is 0 at `HEAD` and 0 in the worktree with depth never going negative, and each of the 4 added guard blocks is self-closing. ## Not addressed, deliberately Three encrypted files match **no** creation rule — `hosts/web-arm/modules/{bitwarden,nextcloud}/secrets.sops.yaml` and `utils/modules/plausible/secrets.sops.yaml`. This predates the PR and is unchanged by it (they matched none before either), but it means `update-secrets-keys` will fail on those three. The first two look dead: both modules take their secrets from web-arm's `defaultSopsFile` and nothing references the files. Filed separately as #320, along with the stale retired-host rules the issue scoped out. Two notes: `README.md:19` still references `hosts/cloonar.com/`, a host directory that no longer exists — left alone as unrelated. And `.claude/skills/README.md` is git-ignored in this worktree, so its edit is local-only and not in this diff.
The nvim sops rule matched */secrets.yaml and update-secrets-keys found its
work with find -name secrets.yaml. That guess was wrong in both directions:
ansible/inventory/host_vars/*.sops.yaml got no editor handling and no key
rotation, and — because sops.lua ships in a Neovim config used in every repo —
a plaintext secrets.yaml in an unrelated project could not be saved at all,
since the BufWriteCmd hook replaces the write and had no creation rule to match.

The filename is now the contract: a file is sops-encrypted iff its name ends
in .sops.yaml.

- git mv the 17 secrets.yaml files to secrets.sops.yaml; retarget 19 nix
  sopsFile/defaultSopsFile references and ansible's base_loki_secrets_file
- sops.lua matches *.sops.yaml, with a basename guard excluding the .sops.yaml
  config itself, now applied in all four autocmds rather than only BufWriteCmd
- tighten every creation rule to [^/]+\.sops\.yaml$; this also stops the root
  rule matching .sops.yaml, which it previously did
- update-secrets-keys discovers by convention, bringing ansible secrets into
  key rotation
- scripts/pre-commit's shared-path regex follows the root secrets rename
- ADR-0025 records the contract; historical ADRs 0002/0005/0021 left as written

No file is re-encrypted: all 17 renamed blobs are byte-identical to HEAD and
every one resolves to the same recipient set under sops' first-match semantics.
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Validated statically. No build gate vouched: pr checks returned state: none (this repo's only dry-build gate is the local pre-commit hook, and the deploy action does not build), and neither nix nor sops was available in the landing session. The dry-build and ./scripts/update-secrets-keys therefore did not run for this PR. Merged on the maintainer's explicit go-ahead after their own review of the diff.

Verified independently:

  • Recipient sets unchanged — simulated sops first-match semantics over all 18 encrypted files against both rule sets: 0 mismatches.
  • .sops.yaml no longer self-matches — matched rule[0] ^[^/]+\.yaml$ on main, matches nothing on head.
  • No re-encryption — all 17 renamed blobs byte-identical by git object id.
  • All 19 sopsFile/defaultSopsFile refs resolve, including the symlink-crossing hosts/fw/modules/fwmetrics.nix:13.
  • All four sops.lua autocmds guarded (:51, :67, :151, :276).
  • Conventions: Conventional Commits title, valid Closes #319, scope matches the issue. Merges cleanly (merge-base is current main).

CONCERNS (non-blocking, carried):

  • ./scripts/update-secrets-keys will exit 1 on three files matching no creation rule (hosts/web-arm/modules/{bitwarden,nextcloud}/secrets.sops.yaml, utils/modules/plausible/secrets.sops.yaml). Confirmed pre-existing on main, not a regression; tracked in #321.
  • PR body cites the follow-up issue as #320 (this PR); the actual follow-up is #321.
[autoland] verdict: pass > *This was generated by AI while landing a PR.* Validated statically. **No build gate vouched**: `pr checks` returned `state: none` (this repo's only dry-build gate is the local pre-commit hook, and the deploy action does not build), and neither `nix` nor `sops` was available in the landing session. The dry-build and `./scripts/update-secrets-keys` therefore **did not run** for this PR. Merged on the maintainer's explicit go-ahead after their own review of the diff. Verified independently: - **Recipient sets unchanged** — simulated sops first-match semantics over all 18 encrypted files against both rule sets: 0 mismatches. - **`.sops.yaml` no longer self-matches** — matched rule[0] `^[^/]+\.yaml$` on main, matches nothing on head. - **No re-encryption** — all 17 renamed blobs byte-identical by git object id. - **All 19 `sopsFile`/`defaultSopsFile` refs resolve**, including the symlink-crossing `hosts/fw/modules/fwmetrics.nix:13`. - **All four `sops.lua` autocmds guarded** (:51, :67, :151, :276). - Conventions: Conventional Commits title, valid `Closes #319`, scope matches the issue. Merges cleanly (merge-base is current main). CONCERNS (non-blocking, carried): - `./scripts/update-secrets-keys` will **exit 1** on three files matching no creation rule (`hosts/web-arm/modules/{bitwarden,nextcloud}/secrets.sops.yaml`, `utils/modules/plausible/secrets.sops.yaml`). Confirmed pre-existing on main, not a regression; tracked in #321. - PR body cites the follow-up issue as #320 (this PR); the actual follow-up is **#321**.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!320
No description provided.