Carve out consumed files so a dev machine stops carrying the whole secrets repo #21
Labels
No labels
blocked
bug
decision
duplicate
enhancement
help wanted
invalid
landed?
question
ready-to-merge
stale
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/archetypes#21
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reduce what a dev machine carries from its deployment's secrets repo to the files that machine actually uses, instead of the whole repo. Two modules interpolate the secrets flake and append a path, and because a flake input resolves to a directory, that pulls in every encrypted file the repo holds — including ciphertexts belonging to other machines, which the machine cannot decrypt and has no reason to store.
Primary goals:
age.secretsentry matches on a path suffix and rejects a carved path, so it changes with the carve rather than being weakened to let one through.Current state and specifics
modules/agent-hooks.nix:14,16,18,20set fourhome.filesources by interpolatinggitPolicySourceand appending/git/<name>.modules/github-credentials.nix:32does the same with"${secrets}/${consumer.secret}". In both cases the resulting string names a file inside the input's store directory, so the derivation depends on the directory. Both modules are composed only bymkDevVm(flake.nix:401,flake.nix:434); privacy VMs and the hypervisor import neither.Measured on this repo's own public template at
b8b2859, which is the smallest case that exists — one dev machine, six ciphertexts:nixosConfigurations.allod-dev.config.system.build.toplevelhas 867 requisites, and the secrets source root/nix/store/<hash>-sourceis one of them.home-manager-files, the symlink tree holding the four policy files. The root'ssecrets/directory travels with it — every.agefile in the repo, pluskeys/,secrets.nix, and the rest.This is exposure, not access. Which machine can decrypt which ciphertext is governed by the recipient graph in
secrets.nix, and that is unaffected and correct: the machines cannot open what they are carrying, apart from credentials that are deliberately shared. The defect is that they carry it at all.The framework already refuses this shape one layer up.
flake.nix:328-377asserts thatmkDevVm's two token arguments are a Nix path or null, precisely because the directory-interpolating spelling leaks a whole repository;modules/host-user-credentials.nix:57-69mirrors it for the hypervisor's own credential fields. That assertion reads the builder's arguments, so it cannot see either site here — a composed module settingage.secrets.<name>.fileafter the merge is past it, and ahome.filesource was never in it. Its comment atflake.nix:312-319records both leaks as measured and open and names this issue; landing this makes that comment false, so it is part of the change.Suggested default, verified to work:
This yields a content-addressed store path holding the single file, and it behaves the same whether the argument is a literal path or a string carrying the input's context, so no context-discarding helper is needed. Measured against the same input as above:
"${secrets}/git/protected-branches"/nix/store/irdys9…-source/git/protected-branches/nix/store/irdys9…-source— the whole repobuiltins.path { path = "${secrets}/git/protected-branches"; name = "protected-branches"; }/nix/store/1rciq5…-protected-branchesThe carved value is a string, not a Nix path —
builtins.isPathisfalseon it — which is why the builder-argument assertion above would refuse it and why nothing here routes through that assertion.Two references that look similar are not in scope and should stay as they are:
flake.nix:1108,:1244and:1250interpolate a source inside a check derivation, which is built on demand and never lands on a machine. A check that inspects the repo is supposed to see the repo.An alternative worth weighing before implementing: have the identity template export the carved paths and have these modules consume them. That moves the choice to the repo that owns the files, but it costs a new export in the template plus a matching change in every fork's data repo, for the same result. The default above is preferred because it is confined to the consuming modules.
The GitHub target matcher changes with the carve
credential-profilesbinds each registered credential target to theage.secretsentry a machine actually generates, and it does so differently for the two registries:targetMatchesGeneratedAgeSecret(Forgejo)flake.nix:745-761/<secret_path>suffix or a-<basename>suffixgithubTargetMatchesGeneratedAgeSecret(GitHub)flake.nix:783-796/<consumer.secret>suffix onlyA carved file is
/nix/store/<hash>-<basename>.age, which has no/secrets/<name>.agesuffix, so the GitHub matcher fails on exactly the credential this change carves. Do not copy the Forgejo matcher's basename fallback: a suffix proves only the label, so an unrelated carved file with the same basename would pass. Instead construct the expected source independently and require exact equality:There is one GitHub construction site and no fork consumes the old spelling, so replace the suffix condition rather than keeping a compatibility branch. The rest of that matcher — destination, owner, group, mode — is already exact and stays.
githubCredentialTargetsreaches the module as an argument ofmkDevVm(flake.nix:275), defaulted from the registry, so the module's own signature is{ secrets, machineName, targets }and a check can apply it directly with synthetic data.Scope
In scope: the two modules above, the GitHub matcher, the stale comment at
flake.nix:312-319, and a check that pins the property so the reference cannot creep back.Out of scope:
flake.nix:1108,:1244and:1250.modules/agent-forgejo-token.nixand the token arguments generally. Those values originate as./secrets + "/<name>.age", a Nix path, which already stages the single file.modules/agent-hooks.nix:11and theallod-toolsinput. The tools input carries no ciphertext, so removing its root is derivation-churn work with a different justification; do it as its own change with its own closure claim.mkDevVmshould also accept abuiltins.pathvalue handed in from a deployment is allod/archetypes#70, which is the same construction seen from the outside; neither issue blocks the other, and nothing proposed there reaches these two sites.Validation
home.filesources equal independently carved paths, and a synthetic non-empty GitHub target produces the exact expected file, destination, owner, group and mode. Sabotage witnesses: an un-carved policy source, a same-basename carved file with different bytes, an empty target list, and a target whose credential has no unique agenix consumer.credential-profilesstill binds every declared target to a generated secret. The public template declares no GitHub targets, so its passing is not evidence for the GitHub matcher — the synthetic fixture above is.Revised the body against
b8b2859, after the secrets rearchitecture arc. What changed and why:allod-devtoplevel has 867 requisites and the secrets source root is one of them; exactly one closure path references it,home-manager-files. So the four policy files alone hold the root in, and the GitHub site adds nothing in the template because no GitHub target is declared there.modules/github-credentials.nix:29is now:32. The fouragent-hooks.nixlines are unchanged. The out-of-scope check derivations wereflake.nix:717,723and are nowflake.nix:1108,:1244and:1250— the third arrived with the credential-store URL parity check.credential-profiles"already accepts a carved single-file path … so this should pass unchanged". That is true of the Forgejo matcher and false of the GitHub one, which is the matcher this change actually touches:githubTargetMatchesGeneratedAgeSecret(flake.nix:783-796) tests a/<consumer.secret>suffix only, and a carved file has no such suffix. The body now states the matcher change and says not to copy the Forgejo basename fallback, which would pass an unrelated file of the same name.mkDevVmnow asserts its token arguments are a Nix path or null (flake.nix:328,:370-377), andmodules/host-user-credentials.nix:57-69mirrors it for the hypervisor. The assertion's comment atflake.nix:312-319names this issue and calls both leaks open; that comment is now in scope, since landing this makes it false. This also retires the old closing note about a deployment's hypervisor interpolating its own source.builtins.pathconstruction seen from outside the builder — it decides whether a deployment may hand one in as a token argument. Recorded as out of scope in both directions.allod/strategydev-plans/archetypes-consumed-file-carve-out.mdpredates the current rule that a plan is written for R4 work only; this is R3. Its live decisions — the carve construction, the exact-equality matcher, and what the new check must witness — are in the body now, so the issue is the single spec.Implemented in allod/archetypes#87, on current master. allod/archetypes#24, which implemented this issue in July from a base 86 commits behind, is closed in its favour; #87 carries its carves and its retention sweep and closes the two findings its review pass left open.
The R3 dev plan in
allod/strategyis archived by allod/strategy#58, so this body is the single spec.What remains before any machine takes it is the human gate the Validation section names: rebuild a disposable dev VM first, not the machine you work from, and confirm agenix decrypts the declared credential to its unchanged destination and home-manager installs all four policy files.