Same class of problem as the deployedTargets/README fixes: hand-maintained prose that drifted from reality and nobody was obligated to update. - CLAUDE.md: "18 hosts" was a stale hardcoded count (actually 20); reworded to not need updating as hosts are added. Also added the missing tailscale-exit-node build type to a list that had it everywhere else in the file except one bullet. - AGENTS.md: same missing tailscale-exit-node build type. - docs/auto-installer.md: the hand-enumerated lxc-* list was missing lxc-tailscale-exit-node. - flake-target-refactor-spec.md: added a "Status: implemented" note so this completed historical spec (referenced elsewhere purely for rationale) can't be mistaken for an open plan with unresolved Open Questions. - remove-sensetive-info-refactor.md: the "Definition of done" checklist was entirely unchecked despite most of the work being done. Checked off what's actually done (sops-nix migration, history scrub just performed, the pre-commit gitleaks hook), and left rotation of the GitHub PAT found in history explicitly flagged as the one still-open item -- an operator action against GitHub, not something this repo can attest to itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
Spec: Remove Sensitive Information from NixOS Flake
Goal
Every secret currently readable in plaintext anywhere in this repo (working tree and git history) gets removed, replaced with sops-nix-managed encrypted references, and rotated. When this is done, the repo should be safe to make public without exposing anything about the systems it configures.
Treat this as three sequential milestones. Do not start git history rewriting (Milestone 3) until Milestones 1 and 2 are fully verified and the flake still builds. This should be its own branch (refactor/secrets) until fully verified, then merged.
Milestone 1 — Audit
Before touching anything, produce a complete inventory. Do not guess at scope — grep the whole tree and the whole history.
-
Run a secret scanner across the working tree and full history. Use both, since they catch different things:
gitleaks detect --source . -v --log-opts="--all"(scans history too)trufflehog git file://. --since-commit=$(git rev-list --max-parents=0 HEAD) --only-verified=falseIf neither is installed, add them via a temporarynix-shell -p gitleaks trufflehog— don't install anything globally on the host.
-
Manually grep for the categories below, since scanners miss config-specific patterns:
hashedPassword,password,initialPassword,initialHashedPasswordin anyusers.users.*blockage.secrets,sops.secrets(if any partial secrets work already exists — check for it)- PSK /
preSharedKey,privateKeyFileinline values (vs. file references) for WireGuard authKey,apiToken,api_key,token =,secret =in service modules (Tailscale, Cloudflare, backup tools, etc.)- SSH private key material: search for
BEGIN OPENSSH PRIVATE KEY/BEGIN RSA PRIVATE KEYliterals - TLS cert/key pairs committed under e.g.
secrets/,certs/,pki/ - Real name, personal email, home address, or anything in comments/hostnames that maps a machine to your physical identity or network layout (e.g. hostnames like
wayne-desktop, static LAN IPs, ISP-identifying info) .envfiles,secrets.nix,secrets.yaml, or any file that looks like it was meant to be gitignored but wasn't
-
Produce
secrets-inventory.md(temporary, delete before finishing) listing: file path, line, secret type, and which host/service it belongs to. This becomes the checklist for Milestone 2 — every row must be either migrated to sops or deleted, with nothing left unaccounted for.
Milestone 2 — Migrate to sops-nix
2.1 Set up sops-nix
- Add the flake input:
sops-nix.url = "github:Mic92/sops-nix"; sops-nix.inputs.nixpkgs.follows = "nixpkgs"; - Import
sops-nix.nixosModules.sopsinto each host's module list (or into a sharedcommon.nixif all hosts use it). - Generate an age keypair per host (not one shared key for everything — a compromised host shouldn't decrypt every other host's secrets):
Print the public key (
nix-shell -p age --run "age-keygen -o /var/lib/sops-nix/key.txt"age-keygen -y) for each host — you'll need it for.sops.yaml. - Also generate one age key for yourself (your admin workstation) so you can edit secrets without needing to SSH into a host: store it at
~/.config/sops/age/keys.txt, back it up somewhere outside this repo (password manager, offline). If this key is lost, every secret encrypted with it is unrecoverable — losing the age key is equivalent to losing the secrets. - Create
.sops.yamlat the repo root defining creation rules: which age public keys can decrypt which secrets files, keyed by path regex, so e.g.secrets/hostA.yamlis decryptable by your admin key + hostA's key,secrets/hostB.yamlby your admin key + hostB's key.
2.2 Migrate each secret category from the inventory
For each row in secrets-inventory.md:
- Password hashes: generate hash with
mkpasswd -m sha-512(orbcryptif your setup wants that), store undersops.secrets."<name>/hashedPassword", reference viausers.users.<name>.hashedPasswordFile = config.sops.secrets."<name>/hashedPassword".path;. Do not put the plaintext password anywhere, only the hash, and only the hash goes into the encrypted sops file. - API tokens / auth keys: move the raw value into the per-host sops YAML, reference in the module via
config.sops.secrets."<service>/token".path— most NixOS service modules that take a token also accept a*Filevariant (e.g.environmentFile,tokenFile); use that instead of passing the value directly. - Private keys / certs: move the PEM/key content wholesale into a sops secret, output as a file with appropriate
sops.secrets.<name>.path,owner,mode,restartUnitsso the depending service (sshd, wireguard, nginx) reloads when the secret changes. - Personal/identifying info: this doesn't belong in sops (it's not "secret," it's just information you don't want public). Replace real names/emails with placeholders or move to a small untracked
local.nixthat's.gitignore'd and imported conditionally, with a documented template (local.nix.example) committed instead.
2.3 Verify before moving on
nixos-rebuild dry-build --flake .#<host>succeeds for every host.sudo nixos-rebuild switch --flake .#<host>on at least one real machine (or a VM) confirms secrets decrypt and services start.- Confirm decrypted secrets land under
/run/secrets/(not the Nix store — anything placed in/nix/storeis world-readable by design, so sops-nix's runtime-only placement is the whole point; double check no module accidentally pulls a secret path into a store-built config file). - Re-run the grep/scanner sweep from Milestone 1 against the working tree only (not history yet) — it should now come back clean.
Milestone 3 — Scrub git history
Do this only after Milestone 2 is merged to your main branch and confirmed working, since it rewrites every commit SHA from the point of the earliest offending commit onward.
This is destructive and irreversible on your local clone. Back up first:
cp -r /path/to/nixos-repo /path/to/nixos-repo-backup-$(date +%F)
- Install
git-filter-repo(not the oldergit filter-branch/ BFG — filter-repo is the currently maintained, faster, safer tool):nix-shell -p git-filter-repo - Use the
secrets-inventory.mdlist to build a list of literal strings/paths to strip. Two approaches, use both:- Path-based: if whole files were secret (e.g.
secrets.nix, a.env, a private key file), remove them entirely from history:git filter-repo --path secrets.nix --path .env --invert-paths - Value-based: for secrets embedded inline in files you're keeping (not deleting the whole file), use
--replace-textwith a file listing each literal secret string to replace with***REMOVED***:git filter-repo --replace-text expressions.txt
- Path-based: if whole files were secret (e.g.
- After filtering, verify: run the Milestone 1 scanners again against full history (
--log-opts="--all"). They must come back clean. - Force-push the rewritten history:
git push origin --force --all git push origin --force --tags - Every other clone of this repo (other machines, WSL instances, CI) must be deleted and re-cloned fresh — a
git pullagainst rewritten history will not work cleanly and risks resurrecting the old commits. Don't try to reconcile old clones; throw them away and re-clone. - If this repo has ever been pushed to a public host (GitHub, etc.) or a fork/mirror exists, treat every secret that was ever in history as permanently compromised regardless of the rewrite — caches, forks, and Wayback-style archives can retain old commits indefinitely. History scrubbing prevents future exposure via
git clone; it does not undo past exposure.
Milestone 4 — Rotate everything
Because the secrets were exposed in history (even briefly, even in a private repo), the migration is not complete until every credential in the inventory has been rotated, not just re-encrypted. Re-encrypting an already-leaked value protects it going forward but doesn't undo the leak.
For each row in the original inventory:
- Password hashes → change the actual account password, regenerate the hash, update the sops file.
- API tokens/auth keys → revoke the old token in the issuing service's dashboard (Cloudflare, Tailscale, backup provider, etc.) and generate a new one.
- SSH/WireGuard private keys → generate new keypairs, update the corresponding public key wherever it's trusted (authorized_keys, peer configs, etc.), retire the old ones.
- TLS certs → reissue if the private key was exposed.
Keep secrets-inventory.md open during this step and check off each row as rotated. Delete the file only once every row is checked off — it should not be committed.
Ongoing prevention
Add a pre-commit hook (or a nix flake check step) running gitleaks protect --staged so a secret can't be committed again by accident. Document in the repo README (briefly) that new secrets go through sops <file> to edit, never as plaintext in a tracked file.
Definition of done
Status as of 2026-07-20: Milestones 1–3 are done — sops-nix is fully
wired (.sops.yaml, secrets/*.yaml, referenced via hashedPasswordFile/
*File/sops.secrets.*.path throughout), and history has been scrubbed
with git-filter-repo + force-push (this removed a GitHub fine-grained PAT
that had been committed in plaintext in flake.nix/common/home.nix
between 2025-07-16 and 2026-02-09, later migrated to sops but never scrubbed
from history until now). Milestone 4 is not confirmed — whether that PAT
(or any other historically-plaintext credential) was actually rotated, not
just re-encrypted, isn't something this repo can attest to; that's an
operator action against the issuing service (GitHub, etc.), not a repo
change. Do that before considering this fully closed.
- Milestone 1 inventory complete and reviewed
- All hosts have per-host age keys; admin key backed up outside the repo
- Every inventoried secret migrated to sops-nix, referenced via
*File/sops.secrets.*.path, nothing plaintext in the working tree nixos-rebuild dry-buildand at least one realswitchverified per host- Working-tree scanner sweep clean
- History rewritten with
git-filter-repo, force-pushed, full-history scanner sweep clean - All other clones deleted and re-cloned from the rewritten history — every clone that existed before 2026-07-20's rewrite (any other machine, WSL instance, or CI checkout) needs this
- Every credential in the original inventory rotated (not just re-encrypted) — the GitHub PAT found in history specifically still needs this
- Pre-commit secret scanning hook added (
.githooks/pre-commit,gitleaks protect --staged) secrets-inventory.mddeleted from the working directory (never committed)