Archived
Migrate live secrets to sops-nix (Milestone 2)
Check NixOS configurations / eval-hosts (push) Failing after 11m5s
Check NixOS configurations / eval-hosts (push) Failing after 11m5s
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 <noreply@anthropic.com>
This commit is contained in:
Executable
+11
@@ -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
|
||||||
@@ -9,3 +9,7 @@ result-*
|
|||||||
auto-installer/flake.lock
|
auto-installer/flake.lock
|
||||||
auto-installer/result
|
auto-installer/result
|
||||||
auto-installer/nixos-auto.iso
|
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
|
||||||
|
|||||||
+36
@@ -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
|
||||||
@@ -86,7 +86,25 @@ review sessions.
|
|||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
|
|
||||||
Do not commit tokens, private keys, live credentials, or new password hashes.
|
Do not commit tokens, private keys, live credentials, or new password hashes
|
||||||
This repository currently contains committed password hashes in shared NixOS
|
as plaintext. Secrets are managed with [sops-nix](https://github.com/Mic92/sops-nix):
|
||||||
configuration; rotate those passwords and move hashes into host-local secret
|
encrypted files live under `secrets/`, recipients (per-host age keys derived
|
||||||
management before treating the repository as public or widely shared.
|
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/<file>.yaml"
|
||||||
|
```
|
||||||
|
|
||||||
|
then reference it from a module via `config.sops.secrets."<name>".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.
|
||||||
|
|||||||
Generated
+22
-1
@@ -166,7 +166,8 @@
|
|||||||
"disko": "disko",
|
"disko": "disko",
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"nixos-conf-editor": "nixos-conf-editor",
|
"nixos-conf-editor": "nixos-conf-editor",
|
||||||
"nixpkgs": "nixpkgs_2"
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"sops-nix": "sops-nix"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"snowfall-lib": {
|
"snowfall-lib": {
|
||||||
@@ -192,6 +193,26 @@
|
|||||||
"type": "github"
|
"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": {
|
"systems": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1681028828,
|
||||||
|
|||||||
@@ -12,9 +12,13 @@
|
|||||||
url = "github:nix-community/disko";
|
url = "github:nix-community/disko";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
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
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
@@ -31,6 +35,7 @@
|
|||||||
inherit system;
|
inherit system;
|
||||||
modules = [
|
modules = [
|
||||||
inputs.disko.nixosModules.disko
|
inputs.disko.nixosModules.disko
|
||||||
|
sops-nix.nixosModules.sops
|
||||||
./modules/common/configuration.nix
|
./modules/common/configuration.nix
|
||||||
./modules/platforms/${platform}.nix
|
./modules/platforms/${platform}.nix
|
||||||
./modules/build-types/${buildType}.nix
|
./modules/build-types/${buildType}.nix
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
{ ... }:
|
{ config, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
networking.hostName = "nix-cache";
|
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 = {
|
services.beszel.agent.environment = {
|
||||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||||
#HUB_URL = "http://docker.sweet.home:8090";
|
#HUB_URL = "http://docker.sweet.home:8090";
|
||||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
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
|
# Preserved from the pre-refactor `nix-cache` target — stateVersion must
|
||||||
# never be bumped on an already-installed machine.
|
# never be bumped on an already-installed machine.
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
{ ... }:
|
{ config, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
networking.hostName = "server";
|
networking.hostName = "server";
|
||||||
networking.hostId = "6689f93e";
|
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 = {
|
services.beszel.agent.environment = {
|
||||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||||
#HUB_URL = "http://docker.sweet.home:8090";
|
#HUB_URL = "http://docker.sweet.home:8090";
|
||||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||||
TOKEN = "48e71783-35df-4ea0-a8c2-05bc5e020d2e";
|
|
||||||
EXTRA_FILESYSTEMS = "/tank/docker/volumes";
|
EXTRA_FILESYSTEMS = "/tank/docker/volumes";
|
||||||
LOG_LEVEL = "debug";
|
LOG_LEVEL = "debug";
|
||||||
};
|
};
|
||||||
|
services.beszel.agent.environmentFile = config.sops.templates."server-beszel.env".path;
|
||||||
|
|
||||||
# Preserved from the pre-refactor `server` target — stateVersion must never
|
# Preserved from the pre-refactor `server` target — stateVersion must never
|
||||||
# be bumped on an already-installed machine.
|
# be bumped on an already-installed machine.
|
||||||
|
|||||||
@@ -5,7 +5,5 @@ services.beszel.agent.enable = true;
|
|||||||
services.beszel.agent.environment = {
|
services.beszel.agent.environment = {
|
||||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||||
HUB_URL = "http://docker.sweet.home:8090";
|
HUB_URL = "http://docker.sweet.home:8090";
|
||||||
#KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA2Snq66P/6GwTEafKuu3D/V1f7ofjKL9GD6ik+Kf/Gn";
|
|
||||||
#TOKEN = "235b3-25a94f482-84576-40d34c48";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,31 @@
|
|||||||
git
|
git
|
||||||
gcr
|
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
|
#Set root password
|
||||||
users.users.root = {
|
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’.
|
# Define a user account. Don't forget to set a password with ‘passwd’.
|
||||||
@@ -37,7 +59,7 @@ users.users.root = {
|
|||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
tree
|
tree
|
||||||
];
|
];
|
||||||
hashedPassword = "$6$Kwv9KAyvcurAViQF$H4.u3feqGE7lVoNgkFXhE3n2Pmo//9JYDTCz8ifrVHBxPjwa1xMby7tEZ8Bpt5MXs9Rkx6/YbZWxs5CpH0s/70";
|
hashedPasswordFile = config.sops.secrets."nixos-hashedPassword".path;
|
||||||
openssh.authorizedKeys.keys = [
|
openssh.authorizedKeys.keys = [
|
||||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq/Q5LvIXlZwO2kdeAN5nLGZ59nZB7JHYMEszHxmNtGMzv1lM31jiPNsr0z2EKVZhE7OOfa2IF9rhWYD7JUA9G0yzdZ4WTXFNGVVOJoOVH6vAF3XCxoVilOEwTc7h2Wiy+rzd0B28/3spffzQQWJhY6GRQVa8j+6xAGF60Fcvl1vLosYT9Bn2ZbK4TCWOwAn2jqXIieGpZdn/UNZbGOeKRiCvhktDfMAzuQzN/9jMu/oF4pkPn2X1UrsQdNlvp0Ci8md612MozIpncQJyAF1ADhunr3sMx0isUXiqD29R5DS4TftpekqLNLak+zcxFa8N7DcRNp3DcKfJvyTkwQrR4r+b7lFLYOLHLagSso9CzeW/paAS2q9I5SBm/2DtE1diLLg2jZikYcstsu/G5RgvbzbKqjiaMwTdXC3AMvDxQrs7U5pDRZFzoofG3cpODbTm+uy3m0kP70z0M1K45UbDG0p+itnTu9x40JbQEgefbx38AItNvAIx1A8HO4I1VX28= wayne@stream"
|
"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"
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
|
||||||
|
|||||||
@@ -17,11 +17,10 @@ in {
|
|||||||
|
|
||||||
programs.bash.enable = true;
|
programs.bash.enable = true;
|
||||||
|
|
||||||
home.file = {
|
# GitHub access-tokens setting used to live here in plaintext; it's now
|
||||||
".config/nix/nix.conf".text = ''
|
# rendered system-wide from a sops-nix secret via nix.extraOptions in
|
||||||
access-tokens = github.com=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
|
# modules/common/configuration.nix instead (covers the daemon for every
|
||||||
'';
|
# user, not just this one).
|
||||||
};
|
|
||||||
|
|
||||||
# Optional: packages
|
# Optional: packages
|
||||||
home.packages = with pkgs; [
|
home.packages = with pkgs; [
|
||||||
|
|||||||
@@ -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."<name>/hashedPassword"`, reference via `users.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 `*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.<name>.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 .#<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/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 <file>` 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)
|
||||||
@@ -70,6 +70,9 @@ EOF
|
|||||||
echo "Nix version:"
|
echo "Nix version:"
|
||||||
nix --version
|
nix --version
|
||||||
|
|
||||||
|
echo "Enabling tracked git hooks (pre-commit secret scan)..."
|
||||||
|
git config core.hooksPath .githooks
|
||||||
|
|
||||||
echo "Installing jq if unavailable..."
|
echo "Installing jq if unavailable..."
|
||||||
if ! command -v jq >/dev/null 2>&1; then
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
nix profile install nixpkgs#jq
|
nix profile install nixpkgs#jq
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user