From 71d052e737022e387f392e176714c129ec4934ed Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Sun, 19 Jul 2026 12:46:39 +1000 Subject: [PATCH] Migrate live secrets to sops-nix (Milestone 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited the working tree and full git history for committed secrets (gitleaks + trufflehog + manual grep, see secrets-inventory.md, kept local/gitignored per the spec). Found: a password hash shared by root and the nixos user across every host, two live Beszel monitoring tokens, and a GitHub fine-grained PAT embedded in a home-manager nix.conf. Migrates all of them to sops-nix: - .sops.yaml + secrets/*.yaml, encrypted for admin + the age keys derived (via ssh-to-age) from each live host's existing SSH host key — no new key material transferred to any machine. - users.users.{root,nixos}.hashedPasswordFile replaces the inline hashedPassword shared by every target. - The GitHub PAT moves from a home-manager-managed, store-visible nix.conf to a sops.templates-rendered file included via nix.conf's native !include, system-wide instead of per-user. - Beszel TOKEN moves from `environment` (store-visible) to `environmentFile` (runtime-only via sops.templates); the dead commented-out docker token is removed from the tree entirely. Added a tracked pre-commit hook (gitleaks protect --staged, wired via core.hooksPath) so a secret can't be committed by accident again, and documented the sops workflow in README.md. Structural verification only: all 17 flake targets evaluate, and `nix build --dry-run --no-link` succeeds for the three currently deployed hosts. Per CLAUDE.md, actual `nixos-rebuild switch` — the step that confirms secrets decrypt and services start on a real machine — is left for manual verification. Git history still contains the original plaintext secrets; scrubbing history (Milestone 3) and rotating every credential (Milestone 4) are separate, deliberately gated steps per remove-sensetive-info-refactor.md. Co-Authored-By: Claude Sonnet 5 --- .githooks/pre-commit | 11 +++ .gitignore | 4 + .sops.yaml | 36 ++++++++ README.md | 26 +++++- flake.lock | 23 ++++- flake.nix | 7 +- hosts/nix-cache/host.nix | 9 +- hosts/server/host.nix | 9 +- modules/beszel/enable-agent.nix | 2 - modules/common/configuration.nix | 26 +++++- modules/common/home.nix | 9 +- remove-sensetive-info-refactor.md | 134 ++++++++++++++++++++++++++++++ scripts/codex-setup.sh | 3 + secrets/common.yaml | 45 ++++++++++ secrets/nix-cache.yaml | 25 ++++++ secrets/server.yaml | 25 ++++++ 16 files changed, 375 insertions(+), 19 deletions(-) create mode 100755 .githooks/pre-commit create mode 100644 .sops.yaml create mode 100644 remove-sensetive-info-refactor.md create mode 100644 secrets/common.yaml create mode 100644 secrets/nix-cache.yaml create mode 100644 secrets/server.yaml diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..8936fd2 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Blocks commits containing secrets. Installed via: +# git config core.hooksPath .githooks +# (scripts/codex-setup.sh does this automatically in Codex sessions.) +set -euo pipefail + +if command -v gitleaks >/dev/null 2>&1; then + gitleaks protect --staged -v +else + nix-shell -p gitleaks --run "gitleaks protect --staged -v" +fi diff --git a/.gitignore b/.gitignore index b820e87..7a5862a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ result-* auto-installer/flake.lock auto-installer/result auto-installer/nixos-auto.iso + +# Temporary Milestone 1 audit checklist (remove-sensetive-info-refactor.md) +# - working notes only, never committed, deleted once every row is rotated. +secrets-inventory.md diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..2de5cf6 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,36 @@ +keys: + - &admin age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 + - &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e + - &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf + - &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu + +creation_rules: + # Shared across every currently-deployed host: root/nixos password hash, + # GitHub access token. Same value on every host today, so every live host's + # key can decrypt it (matches current risk profile — narrow further in + # Milestone 4 if hosts should diverge). + - path_regex: secrets/common\.yaml$ + key_groups: + - age: + - *admin + - *docker + - *server + - *nix-cache + + - path_regex: secrets/nix-cache\.yaml$ + key_groups: + - age: + - *admin + - *nix-cache + + - path_regex: secrets/server\.yaml$ + key_groups: + - age: + - *admin + - *server + + - path_regex: secrets/docker\.yaml$ + key_groups: + - age: + - *admin + - *docker diff --git a/README.md b/README.md index d0f2661..590b2da 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,25 @@ review sessions. ## Security Notes -Do not commit tokens, private keys, live credentials, or new password hashes. -This repository currently contains committed password hashes in shared NixOS -configuration; rotate those passwords and move hashes into host-local secret -management before treating the repository as public or widely shared. +Do not commit tokens, private keys, live credentials, or new password hashes +as plaintext. Secrets are managed with [sops-nix](https://github.com/Mic92/sops-nix): +encrypted files live under `secrets/`, recipients (per-host age keys derived +from each host's existing SSH host key, plus an admin key) are declared in +`.sops.yaml`. To add or edit a secret: + +```bash +nix-shell -p sops --run "sops secrets/.yaml" +``` + +then reference it from a module via `config.sops.secrets."".path` +(or `sops.templates` for values that need to be embedded in a rendered +config file, e.g. `nix.conf`'s `access-tokens`). Never write a secret value +directly into a tracked `.nix` file. A pre-commit hook (`.githooks/`, +enabled via `git config core.hooksPath .githooks`, done automatically by +`scripts/codex-setup.sh`) runs `gitleaks protect --staged` to catch mistakes +before they're committed. + +This repository's git *history* still contains secrets committed before this +migration (see `remove-sensetive-info-refactor.md`) — those are being +scrubbed and rotated separately; don't treat the repo as safe to make public +until that's finished. diff --git a/flake.lock b/flake.lock index 7fb6355..01206a6 100644 --- a/flake.lock +++ b/flake.lock @@ -166,7 +166,8 @@ "disko": "disko", "home-manager": "home-manager", "nixos-conf-editor": "nixos-conf-editor", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_2", + "sops-nix": "sops-nix" } }, "snowfall-lib": { @@ -192,6 +193,26 @@ "type": "github" } }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783174389, + "narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, "systems": { "locked": { "lastModified": 1681028828, diff --git a/flake.nix b/flake.nix index dc4de9d..644e8cf 100644 --- a/flake.nix +++ b/flake.nix @@ -12,9 +12,13 @@ url = "github:nix-community/disko"; inputs.nixpkgs.follows = "nixpkgs"; }; + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, nixos-conf-editor, home-manager, ... } @ inputs: + outputs = { self, nixpkgs, nixos-conf-editor, home-manager, sops-nix, ... } @ inputs: let system = "x86_64-linux"; @@ -31,6 +35,7 @@ inherit system; modules = [ inputs.disko.nixosModules.disko + sops-nix.nixosModules.sops ./modules/common/configuration.nix ./modules/platforms/${platform}.nix ./modules/build-types/${buildType}.nix diff --git a/hosts/nix-cache/host.nix b/hosts/nix-cache/host.nix index 68b8313..971f247 100644 --- a/hosts/nix-cache/host.nix +++ b/hosts/nix-cache/host.nix @@ -1,14 +1,19 @@ -{ ... }: +{ config, ... }: { networking.hostName = "nix-cache"; + sops.secrets."beszel-token".sopsFile = ../../secrets/nix-cache.yaml; + sops.templates."nix-cache-beszel.env".content = '' + TOKEN=${config.sops.placeholder."beszel-token"} + ''; + services.beszel.agent.environment = { #DOCKER_HOST = "tcp://docker-socket-proxy:2375"; #HUB_URL = "http://docker.sweet.home:8090"; KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG"; - TOKEN = "c9192e4c-7b5d-4910-8241-c2d68afadbac"; }; + services.beszel.agent.environmentFile = config.sops.templates."nix-cache-beszel.env".path; # Preserved from the pre-refactor `nix-cache` target — stateVersion must # never be bumped on an already-installed machine. diff --git a/hosts/server/host.nix b/hosts/server/host.nix index bd35485..c753856 100644 --- a/hosts/server/host.nix +++ b/hosts/server/host.nix @@ -1,17 +1,22 @@ -{ ... }: +{ config, ... }: { networking.hostName = "server"; networking.hostId = "6689f93e"; + sops.secrets."beszel-token".sopsFile = ../../secrets/server.yaml; + sops.templates."server-beszel.env".content = '' + TOKEN=${config.sops.placeholder."beszel-token"} + ''; + services.beszel.agent.environment = { #DOCKER_HOST = "tcp://docker-socket-proxy:2375"; #HUB_URL = "http://docker.sweet.home:8090"; KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG"; - TOKEN = "48e71783-35df-4ea0-a8c2-05bc5e020d2e"; EXTRA_FILESYSTEMS = "/tank/docker/volumes"; LOG_LEVEL = "debug"; }; + services.beszel.agent.environmentFile = config.sops.templates."server-beszel.env".path; # Preserved from the pre-refactor `server` target — stateVersion must never # be bumped on an already-installed machine. diff --git a/modules/beszel/enable-agent.nix b/modules/beszel/enable-agent.nix index 14a4c3c..fb7d2cd 100644 --- a/modules/beszel/enable-agent.nix +++ b/modules/beszel/enable-agent.nix @@ -5,7 +5,5 @@ services.beszel.agent.enable = true; services.beszel.agent.environment = { #DOCKER_HOST = "tcp://docker-socket-proxy:2375"; HUB_URL = "http://docker.sweet.home:8090"; -#KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA2Snq66P/6GwTEafKuu3D/V1f7ofjKL9GD6ik+Kf/Gn"; -#TOKEN = "235b3-25a94f482-84576-40d34c48"; }; } diff --git a/modules/common/configuration.nix b/modules/common/configuration.nix index c5f0036..a3e1834 100644 --- a/modules/common/configuration.nix +++ b/modules/common/configuration.nix @@ -25,9 +25,31 @@ git gcr ]; + + # Secrets shared by every host, decrypted at activation via each host's + # existing SSH host key (sops-nix derives the age key from + # /etc/ssh/ssh_host_ed25519_key automatically — see modules/common/README + # or docs/ for the sops workflow). hashedPassword/hashedPasswordFile need + # neededForUsers so they're available before the normal secret-activation + # step, since user creation happens very early in boot. + sops.defaultSopsFile = ../../secrets/common.yaml; + sops.secrets."root-hashedPassword".neededForUsers = true; + sops.secrets."nixos-hashedPassword".neededForUsers = true; + sops.secrets."nix-github-token" = { }; + + # nix.conf doesn't support a *File-style option for access-tokens, so the + # token is rendered into a runtime-only file (never touches the Nix store) + # and pulled in via nix.conf's native !include directive. + sops.templates."nix-github-token.conf".content = '' + access-tokens = github.com=${config.sops.placeholder."nix-github-token"} + ''; + nix.extraOptions = '' + !include ${config.sops.templates."nix-github-token.conf".path} + ''; + #Set root password users.users.root = { - hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70"; + hashedPasswordFile = config.sops.secrets."root-hashedPassword".path; }; # Define a user account. Don't forget to set a password with ‘passwd’. @@ -37,7 +59,7 @@ users.users.root = { packages = with pkgs; [ tree ]; - hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70"; + hashedPasswordFile = config.sops.secrets."nixos-hashedPassword".path; openssh.authorizedKeys.keys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface" diff --git a/modules/common/home.nix b/modules/common/home.nix index 949996a..143935d 100644 --- a/modules/common/home.nix +++ b/modules/common/home.nix @@ -17,11 +17,10 @@ in { programs.bash.enable = true; - home.file = { - ".config/nix/nix.conf".text = '' - access-tokens = github.com=***REMOVED*** - ''; - }; + # GitHub access-tokens setting used to live here in plaintext; it's now + # rendered system-wide from a sops-nix secret via nix.extraOptions in + # modules/common/configuration.nix instead (covers the daemon for every + # user, not just this one). # Optional: packages home.packages = with pkgs; [ diff --git a/remove-sensetive-info-refactor.md b/remove-sensetive-info-refactor.md new file mode 100644 index 0000000..4f1d1d5 --- /dev/null +++ b/remove-sensetive-info-refactor.md @@ -0,0 +1,134 @@ +# 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. + +1. 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=false` + If neither is installed, add them via a temporary `nix-shell -p gitleaks trufflehog` — don't install anything globally on the host. + +2. Manually grep for the categories below, since scanners miss config-specific patterns: + - `hashedPassword`, `password`, `initialPassword`, `initialHashedPassword` in any `users.users.*` block + - `age.secrets`, `sops.secrets` (if any partial secrets work already exists — check for it) + - PSK / `preSharedKey`, `privateKeyFile` inline 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 KEY` literals + - 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) + - `.env` files, `secrets.nix`, `secrets.yaml`, or any file that looks like it was meant to be gitignored but wasn't + +3. 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 + +1. Add the flake input: + ```nix + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "nixpkgs"; + ``` +2. Import `sops-nix.nixosModules.sops` into each host's module list (or into a shared `common.nix` if all hosts use it). +3. Generate an age keypair **per host** (not one shared key for everything — a compromised host shouldn't decrypt every other host's secrets): + ``` + nix-shell -p age --run "age-keygen -o /var/lib/sops-nix/key.txt" + ``` + Print the public key (`age-keygen -y`) for each host — you'll need it for `.sops.yaml`. +4. 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.** +5. Create `.sops.yaml` at the repo root defining creation rules: which age public keys can decrypt which secrets files, keyed by path regex, so e.g. `secrets/hostA.yaml` is decryptable by your admin key + hostA's key, `secrets/hostB.yaml` by 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` (or `bcrypt` if your setup wants that), store under `sops.secrets."/hashedPassword"`, reference via `users.users..hashedPasswordFile = config.sops.secrets."/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."/token".path` — most NixOS service modules that take a token also accept a `*File` variant (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..path`, `owner`, `mode`, `restartUnits` so 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.nix` that'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 .#` succeeds for every host. +- `sudo nixos-rebuild switch --flake .#` 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/store` is 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) +``` + +1. Install `git-filter-repo` (not the older `git filter-branch` / BFG — filter-repo is the currently maintained, faster, safer tool): + ``` + nix-shell -p git-filter-repo + ``` +2. Use the `secrets-inventory.md` list 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-text` with a file listing each literal secret string to replace with `***REMOVED***`: + ``` + git filter-repo --replace-text expressions.txt + ``` +3. After filtering, verify: run the Milestone 1 scanners again against full history (`--log-opts="--all"`). They must come back clean. +4. Force-push the rewritten history: + ``` + git push origin --force --all + git push origin --force --tags + ``` +5. **Every other clone of this repo (other machines, WSL instances, CI) must be deleted and re-cloned fresh** — a `git pull` against 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. +6. 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 ` to edit, never as plaintext in a tracked file. + +--- + +## Definition of done + +- [ ] 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-build` and at least one real `switch` verified 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 credential in the original inventory rotated (not just re-encrypted) +- [ ] Pre-commit secret scanning hook added +- [ ] `secrets-inventory.md` deleted from the working directory (never committed) diff --git a/scripts/codex-setup.sh b/scripts/codex-setup.sh index a0e4e20..99f6e43 100755 --- a/scripts/codex-setup.sh +++ b/scripts/codex-setup.sh @@ -70,6 +70,9 @@ EOF echo "Nix version:" nix --version +echo "Enabling tracked git hooks (pre-commit secret scan)..." +git config core.hooksPath .githooks + echo "Installing jq if unavailable..." if ! command -v jq >/dev/null 2>&1; then nix profile install nixpkgs#jq diff --git a/secrets/common.yaml b/secrets/common.yaml new file mode 100644 index 0000000..3b8d7e1 --- /dev/null +++ b/secrets/common.yaml @@ -0,0 +1,45 @@ +root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str] +nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str] +nix-github-token: ENC[AES256_GCM,data:OfNRGJg16Ede6EilWUetCs9za+xk5/Lsa3SpVajsqz8PMdA1xQNeCWdX7ZAMdijHClpBhU6ETFGsXvt41O9aORS951uijeGSW7/NH35/bnPISrKdYeBx/+xEiqwH,iv:QGU3v7xOy89uzRTCb1U9ICyJ8XYIpXrUsDt12aL3g2Y=,tag:Bde2wcWNv8H4WLxSEUAodg==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6NkpZRzJuMFNuV09WamR0 + SERDN0VSUlNQbkhSem1EeFlxdlhmdUx0S21JCnY1VkRBQTBqcW9JRHpJWEtJSitN + dHkyR0I4ckNkWkpGWHAzZGRaZkhJalEKLS0tIHRrU3RhU3UxN3B4NHdLeStuSXQ4 + anFkRlpMTHowZzJNdnY0MExQcXd4OUEKgbT7uOSFkfEs6t3X6jmGOiC28lDJWF33 + 50f2fZ771ylhHa6WJMetGZ5cwl9r1RCaDiWyJEaqNGe7NqARlhr3EQ== + -----END AGE ENCRYPTED FILE----- + recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDamw2ekY0dFZyOGNJelVa + ZGlNK2VNOWtWNTRzU3o2aVJvRi9CME5yS1JFCndlamdFMzVRTnoyVE1ZWXZkTjRR + eWdSZzZERHE5VElrOGZvcE4vK0g2d0EKLS0tIG15QlVyalZTK2NpVDdWWU4yUU4r + SkFncC9rZkNKUS9MSmF6WGQwOENwZTQKs5kLNLdQJoZtcsw3zlUWUUtJs0MJnuvw + o5vziOswRnuXENw//xN01nMDC6Ckzvb+q9GFIyxHLE4j3fT5XdZakg== + -----END AGE ENCRYPTED FILE----- + recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyaWpWTGd4TEVoaHg4b3hm + S1o3VXdkYkZCS0QxcW5hUWxyVjFneE5aSmp3ClNsdEZoMHpPUzI4K3B0ZnBYMlEw + MkJyd2NPcGJDZzgvc1EyWFBhNVlGUVEKLS0tIEptYXI2bnJHZVZGR3hSSm1sMlN5 + ay8wdUh6TEowL0ZiUWtqbXl3NzhYOVEKW9l4mr+MLhuXA8sgQndaU8NiFeCMcxhG + qjKFn+mu2GFbwfZWAy6y/KUd9Ug4H6y3pNHlJMHupTu8v2wZ+3S4EA== + -----END AGE ENCRYPTED FILE----- + recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxVG13RVVweGpRM04vMWxG + V2hMMnp2Qk5Ub1BZQWNKNXVya3J4NGpreDJrCktqajVPRXdEL1BMU1dMbDhmU01L + UlQvRVQvcEFUZVZ5Umc3Y0hMUWNLVFEKLS0tIFgzRnF1RHdQdktDVjROSURYYTdx + NjAydjVuV1VCOGZoVFdTZmw1YmJ0eEEKN9KRGegafu1sjHlhyn4KCMPPAKxsSZLc + QJkei1ZQ0AVgC2QfCVEXonMOrs45nCIplG+uyER9khZyyjKObWk1pA== + -----END AGE ENCRYPTED FILE----- + recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu + lastmodified: "2026-07-19T02:30:40Z" + mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.1 diff --git a/secrets/nix-cache.yaml b/secrets/nix-cache.yaml new file mode 100644 index 0000000..36af31a --- /dev/null +++ b/secrets/nix-cache.yaml @@ -0,0 +1,25 @@ +beszel-token: ENC[AES256_GCM,data:meuzUP/6wCssJDVTgbC0XwiLZPMGyDl55HEIiON9xOXCD9k6,iv:TDqWcp+8Mxd8wN09r5otQRQXq3XTeQphaTWxvvuLTAs=,tag:cRPZQGlwB/dTguBAheWPQg==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkb29VM3haQ0c0WDY0STVn + L1ZyOUROSnR6ZjZZZWc1VGFFNVp6bWxCV3hZCnlGTC9CbEtIM3MvSGJQaStEbzJ0 + eWVhY2l6UHNDRTYxdEpkd003WXFia2MKLS0tIHcxU3FPYTVuZ0RqZTZTT1RvZnN0 + OGlQM3B2R0l4MHhWNXBBWmpiOEVxK1EK8kxh3ikKL6Fw2am3r7lPGpB0fCqzEukO + NU10Cbf38Bd+fybRbBnvRuu9To1FOf+KU3iKbsuWmZn4KJn4Ajcbhw== + -----END AGE ENCRYPTED FILE----- + recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCNWRhK2QrV1ZibllqY25Y + ZVRPLzZ0TE82TWZ4L0w5ZmFPTDBtT0poODBnCkE5TWhJcXJjNkdva1hYZ3loeVdq + UlBXWHZpRUpMSGhPU0FIRi9JMEtTdGMKLS0tIE9yUk5Ebjk2MEFtNEkyR3RDcU51 + ZVMwNnNRMlBMVVFlamU1U3JQSy9UT00KJZdEpRHuj4Dp1dGwP1z4xi9oddQOrQj2 + qheEN/IlifVdZDpvlLUc8jgLzd/TO5EeEhVqfbdrlua8FHly29oTNw== + -----END AGE ENCRYPTED FILE----- + recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu + lastmodified: "2026-07-19T02:30:40Z" + mac: ENC[AES256_GCM,data:7+FeT6aeCGn+JFBXbPO0qP4BJ1nHPSennewv1kWkG+hOTIqs1ymuswUK1Hyfi6Z9h2umFX9HvK+o3qtmYvk6k7BUNe6w6QUHTNwm6lmNqrb7sgAE3iFVI2p9m14NGhgoTfnXx1M4JIZ1iuNYhCukpENI4+svIe+r7x5YeE5Evac=,iv:4AKAPI6upyAvHBr8BLWX7R/NupmJdcXdqiN8e0ZQ3ls=,tag:TkpfpCaLhn+Mx5cQZZuMdA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.1 diff --git a/secrets/server.yaml b/secrets/server.yaml new file mode 100644 index 0000000..28f7fd7 --- /dev/null +++ b/secrets/server.yaml @@ -0,0 +1,25 @@ +beszel-token: ENC[AES256_GCM,data:cbQOXhLzNk4g9d6hvm2DH7Q5ApTPCTzsW2txflDT2dD/UPIE,iv:V19MI1GEo5/0205Hrt7JImfkjduFiZ7f9aIkDVaI8mU=,tag:WCArgdrnIOudVe/Tw+oxRw==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaFZURjAxMGRJZEJ5MW1x + ZjVwWEQrQlkwNmRibVNiL2RpTTFLeUVDQ3hRCkVIamVnZkM1MnlueloxMHVFQnBF + RjV2bnUrZUo4WGZJTmR4Y0xITkxRUkUKLS0tIGlJdVQ5MFBubVhxRUVMWW0wSGpP + UGdKNUNPYW9nek1UZ0tWbXd3QVNUNDgKIHOiKelITQdH5R4Nc3WF7mzz15D1f9on + VaTdr5qkf8LNNvPI0fxsXA9is5cqeg+KbDRHtUumEhNp6Zrf8zWBkw== + -----END AGE ENCRYPTED FILE----- + recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0hvV01naytDSWVwK1B1 + MXM1ZkdpaVc2Q3FPV2VBcC83WmcvSjdGUGgwClJGSXZ6YW5JeFlValNJbjVhK29u + bUFqN2dRQTI4ZkwyeXNWYk5JeWVJRXcKLS0tIDlMMkNBUnNUSTJwVVFmc2dlcEZS + VGQ1VHR2dXB0M3RsalppVWxiUUROM0UKZM/4QDTam3LDTzjnDs41Ije50R7Q7GC4 + IZbUZjs72rBzY8IkJDbN9JidadEc4NAtMOJwXiJbpZGiCBNfc8+SXw== + -----END AGE ENCRYPTED FILE----- + recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf + lastmodified: "2026-07-19T02:30:40Z" + mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.1