M0 complete: sops-nix wiring + decrypt-a-test-secret; M0 gate CLAIMED

Host decrypts /run/secrets/test_secret via its ssh host key (age identity);
off-box master recovery recipient. sops-nix pinned to a buildGoModule-era rev
for nixpkgs 24.11 compat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 21:41:45 +01:00
parent 9bffb55b28
commit deb4a0fbed
12 changed files with 154 additions and 11 deletions

View File

@ -0,0 +1 @@
{"sessionId":"de029a55-31d3-41c9-b62a-0fa17789202c","pid":31042,"procStart":"9453895","acquiredAt":1779827176270}

14
.sops.yaml Normal file
View File

@ -0,0 +1,14 @@
# sops creation rules. Recipients:
# host — cc-ci's age key, derived from its ed25519 SSH host key (ssh-to-age).
# Used at activation to decrypt into /run/secrets (sops-nix, age.sshKeyPaths).
# master — off-box recovery/admin key; private half lives ONLY on the build host at
# /srv/cc-ci/.sops/master-age.txt (never in this repo). Lets us re-key if cc-ci is lost.
keys:
- &host age1h90utdztfc23kx8ewrtrtk80mnddvrf8pg4ppej55rwwwupzhfvqhmp3qa
- &master age1cmk26t9e30ls8594s8txgmf2exenydmntfxqpcd3qdqm3ru2lpnqpdkdz9
creation_rules:
- path_regex: secrets/.*\.(yaml|json|env)$
key_groups:
- age:
- *host
- *master

View File

@ -8,9 +8,10 @@ Two single-writer sections (§6.1): Builder edits only `## Build backlog`; Adver
### M0 — Foundations
- [x] Author flake.nix (NixOS host cc-ci) + hosts/cc-ci/{configuration,hardware}.nix from baseline
- [x] Deploy mechanism decision + first rebuild from repo (DECISIONS.md) — switch --flake on host
- [ ] sops-nix wiring: host age key, secrets/secrets.yaml, decrypt a test secret on host
- [ ] Gate: M0 — `ssh cc-ci 'systemctl is-system-running'` healthy after rebuild from repo (base
rebuild verified healthy 2026-05-26; will CLAIM gate once sops test-secret also lands)
- [x] sops-nix wiring: host age key (from ssh host key) + master recovery key; secrets/secrets.yaml;
decrypt a test secret on host → /run/secrets/test_secret (0400 root) verified
- [x] Gate: M0 — `ssh cc-ci 'systemctl is-system-running'` healthy after rebuild from repo
→ CLAIMED 2026-05-26, awaiting Adversary (see STATUS.md)
### M1 — Swarm + abra target
- [ ] Docker + single-node swarm via Nix

View File

@ -25,7 +25,12 @@ Architecture decisions and dead-ends. One line of rationale each. (§0, §8)
is a true no-op-then-base. Bump deliberately, never drift.
- **Webhook scope:** default per-repo via enroll script.
- **Drone runner type:** default exec (must drive host abra).
- **Secret tool:** default sops-nix.
- **Secret tool — SETTLED (M0):** sops-nix. cc-ci decrypts at activation using its **ed25519 SSH
host key** as the age identity (`sops.age.sshKeyPaths`), so no extra key file to manage on the box.
Recipients in `/.sops.yaml`: the host age key (`age1h90ut…`, from ssh-to-age) + an off-box
**master recovery key** (`age1cmk26t…`; private half only at `/srv/cc-ci/.sops/master-age.txt` on
the build host, never in the repo) for re-keying if cc-ci is lost. Encrypt new secrets by writing
plaintext into `secrets/<f>.yaml` then `sops -e -i` (run inside the repo so `.sops.yaml` is found).
- **D10 recipe set:** lock six early. Candidates favouring already-mirrored: custom-html (simple),
cryptpad (stateful no-DB), keycloak (SSO/DB), matrix-synapse (DB+media), lasuite-docs (multi+S3),
bluesky-pds (TLS-passthrough) — covers all five categories. Confirm during M4M6.5.

View File

@ -58,3 +58,42 @@ baseline; networking is up. Clean up by choosing one stack later.
**Next:** sops-nix wiring (host age key from ssh host key + a decrypt-a-test-secret proof), then
CLAIM the M0 gate for the Adversary.
## 2026-05-26 — M0: sops-nix wiring + decrypt-a-test-secret (M0 COMPLETE, gate CLAIMED)
**Keys:**
- Host age recipient from ssh host key: `ssh cc-ci 'nix run nixpkgs#ssh-to-age -- -i
/etc/ssh/ssh_host_ed25519_key.pub'` → `age1h90utdztfc23kx8ewrtrtk80mnddvrf8pg4ppej55rwwwupzhfvqhmp3qa`.
- Master recovery key generated on host (`age-keygen`), public `age1cmk26t…`; private moved off-box
to `/srv/cc-ci/.sops/master-age.txt` (mode 600) and `shred`-ded from the host. Never in repo.
**Files:** `.sops.yaml` (both recipients, rule `secrets/.*\.(yaml|json|env)$`); `modules/secrets.nix`
(`sops.age.sshKeyPaths=[/etc/ssh/ssh_host_ed25519_key]`, `secrets.test_secret={}`); flake gains
`sops-nix` input + `sops-nix.nixosModules.sops`; configuration.nix imports the module.
**sops-nix version pin (dead-end avoided):** master sops-nix wants `buildGo125Module` (Go 1.25),
absent in pinned nixpkgs 24.11 → eval error. Pinned sops-nix to `77c423a…` (2025-06-17, last using
plain `buildGoModule`). Verified the file at that rev uses `buildGoModule`. Build then OK.
**Encrypt test secret:** on host, `printf 'test_secret: cc-ci-m0-<rand>' > secrets/secrets.yaml`
then `nix run nixpkgs#sops -- --encrypt --in-place secrets/secrets.yaml` (run inside repo so
`.sops.yaml` resolves) → rc=0, two age recipients in the file.
**Build + switch (commands + output):**
- `nixos-rebuild build --flake .#cc-ci` → `BUILD EXIT 0` (built sops-install-secrets w/ Go 1.23.8).
- `systemd-run --unit=ccci-rebuild2 ... nixos-rebuild switch --flake /root/cc-ci#cc-ci` →
`Result=success ExecMainStatus=0`.
**Gate verification (M0):**
- `systemctl is-system-running` → `running`; `systemctl --failed` → none.
- `ls -la /run/secrets/test_secret` → `-r-------- 1 root root 41` ; `stat` → `root:root 400`.
- `head -c9` → `cc-ci-m0-` (matches generated value), `wc -c` → 41 (9 + 32 hex). Decrypt path proven.
- Pulled encrypted `secrets/secrets.yaml` + `flake.lock` back to clone; `grep cc-ci-m0 secrets.yaml`
→ no plaintext leak; lock inputs = nixpkgs, sops-nix.
**Gate handshake:** set `Gate: M0 — CLAIMED, awaiting Adversary` in STATUS.md. REVIEW.md still empty
(no Adversary activity yet). Per §6.1 liveness I won't idle-block: I keep M0 claimed and proceed
with M1 (independent infra build), without advancing to M2 until M0 shows PASS.
**Next:** M1 — Docker + single-node swarm via Nix (modules/swarm.nix), then Traefik (file provider
→ /var/lib/ci-certs/live/) + abra, then a by-hand HTTPS deploy/teardown of a trivial recipe.

View File

@ -1,11 +1,15 @@
# STATUS — cc-ci Builder
**Phase:** M0 — Foundations
**In-flight:** Base flake config deployed + verified. Next M0 task: sops-nix + decrypt a test secret.
**Last updated:** 2026-05-26 (M0 base config live)
**Phase:** M0 → M1. M0 complete & CLAIMED; starting M1 (swarm + Traefik + abra) while awaiting verdict.
**In-flight:** M1 — Docker + single-node swarm via Nix (first M1 task).
**Last updated:** 2026-05-26 (M0 claimed)
## Gates
- (none claimed yet — M0 gate pends sops wiring)
- **Gate: M0 — CLAIMED, awaiting Adversary** (2026-05-26). Evidence: flake rebuilds cc-ci from repo
(`switch --flake /root/cc-ci#cc-ci`, gen healthy, no failed units); sops-nix decrypts
`/run/secrets/test_secret` (0400 root, value = generated `cc-ci-m0-…`). Repro: clone repo, sync to
host, `nixos-rebuild switch --flake .#cc-ci`, then `systemctl is-system-running` + check the secret.
Per §6.1 I will NOT advance past this gate to M2; M1 work proceeds as independent unblocked work.
## Blocked
- (none)

24
flake.lock generated
View File

@ -18,7 +18,29 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1750119275,
"narHash": "sha256-Rr7Pooz9zQbhdVxux16h7URa6mA80Pb/G07T4lHvh0M=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "77c423a03b9b2b79709ea2cb63336312e78b72e2",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"rev": "77c423a03b9b2b79709ea2cb63336312e78b72e2",
"type": "github"
}
}
},

View File

@ -5,9 +5,14 @@
# Pinned to the exact revision cc-ci already runs, so the first rebuild from
# this repo is a true no-op-then-base (M0). Bump deliberately, not drift.
nixpkgs.url = "github:NixOS/nixpkgs/50ab793786d9de88ee30ec4e4c24fb4236fc2674";
# Pinned to a commit that still uses plain `buildGoModule` — sops-nix master moved to
# `buildGo125Module` (Go 1.25), which our pinned nixpkgs 24.11 (2025-06-30) does not have.
sops-nix.url = "github:Mic92/sops-nix/77c423a03b9b2b79709ea2cb63336312e78b72e2";
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs }:
outputs = { self, nixpkgs, sops-nix }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
@ -15,7 +20,10 @@
{
nixosConfigurations.cc-ci = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ ./hosts/cc-ci/configuration.nix ];
modules = [
sops-nix.nixosModules.sops
./hosts/cc-ci/configuration.nix
];
};
# Devshell for working on the harness/bridge locally.

View File

@ -5,6 +5,7 @@
{
imports = [
./hardware.nix
../../modules/secrets.nix
];
# --- Tailscale (ACCESS-CRITICAL: do not break, this is the only route in) ---

18
modules/secrets.nix Normal file
View File

@ -0,0 +1,18 @@
# sops-nix wiring (D6 infra secrets). cc-ci decrypts secrets at activation using its own
# ed25519 SSH host key as the age identity (no separate key file to manage on the box).
# Encrypted material lives in ../secrets/*.yaml, committed and readable only by recipients
# listed in /.sops.yaml (host key + off-box master recovery key).
{ ... }:
{
sops = {
defaultSopsFile = ../secrets/secrets.yaml;
# Decrypt using the host's SSH host key (converted to an age identity by sops-nix).
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
# Do not also look for a GPG key.
gnupg.sshKeyPaths = [ ];
# M0 proof secret — confirms the decrypt path works end to end. Real infra secrets
# (Drone RPC, webhook HMAC, OAuth, registry creds) are added in their milestones.
secrets.test_secret = { };
};
}

View File

30
secrets/secrets.yaml Normal file
View File

@ -0,0 +1,30 @@
test_secret: ENC[AES256_GCM,data:VOxNiRyeSQQPKeF2PUK9AtezhzX+Hdm9ji5ZYm+gNd2NJ+wwXc67En8=,iv:Bn1oQeBN98E2/To1KRAw3wsLUF0/HsQFBm8s28L5aqo=,tag:KPS5Y+25Elf3alSF2H6npw==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1h90utdztfc23kx8ewrtrtk80mnddvrf8pg4ppej55rwwwupzhfvqhmp3qa
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoRzN4Q0Eramhkdk8xbmNn
NGRwclpaT3YvR24yemJxQjF1N001cVI3MXdjCk9wUE5QTE1UNmxTa1BWalk1V1hS
M1o2elpOVHNGcWdyWVI1RHdUZi96M0UKLS0tIGpaRDNNa1ZuRWkybitPTlM5ajVK
VjNldWh1Q3lTc2lCNlE4aGNmaVNOT1kKMhsp/z5LbEyAezDHTodL2vS3L/wlNOc0
xiDLBYX1AJSiT8DOBvSMSZFj+ygsNL8GBYABjC0Ioar1PIK/KI00oA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1cmk26t9e30ls8594s8txgmf2exenydmntfxqpcd3qdqm3ru2lpnqpdkdz9
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwTk9uQ3JSeEFidEF6U2FU
Mjk1bVYvNTdYaDJjL0g1ZGlZdXkyR3VUREJrClV5S283elRFbXZkUXNiQkoyYWwr
ZUkxV1UyRE1xcW5DSTNVbEk1dXEyWmcKLS0tIFo5SUdhaVVqYUpZUGQxVGQvR2s0
a2RKRWVaTGhNb0d3TFlnL2NtejZOaEUK2dQaAzlYk4Z7aBej77cO4Ug9Afkka6wg
G1SumwxX0wMocpgz4WhDUPkBC66uWlaR3u1AWzwpzRseuwAZ94gAxA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-26T20:32:25Z"
mac: ENC[AES256_GCM,data:cfMw8ILys0FXVXiRUbsZ1Jjz+S7X+0Q+DI4oyWNEP8Ka/iZeLkF1NjYbcdOVpqTuiF2gGde2uUjFRy3GtXOUYn8u8Sf2Ve1D1QpJ+8IiDCcaPUjxOxAKd3yGgDi4mm5EWxg+DerlugEj5hwlxWWBdj0I7XDnL2RPANN+yA9Ypko=,iv:hewvYjUmDfEg0PfUMaY0+SZuLy8sAR++d2Dlzm5Yr4w=,tag:nHqjRCHHM98Hzdv1HYHr3A==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4