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:
1
.claude/scheduled_tasks.lock
Normal file
1
.claude/scheduled_tasks.lock
Normal file
@ -0,0 +1 @@
|
||||
{"sessionId":"de029a55-31d3-41c9-b62a-0fa17789202c","pid":31042,"procStart":"9453895","acquiredAt":1779827176270}
|
||||
14
.sops.yaml
Normal file
14
.sops.yaml
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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 M4–M6.5.
|
||||
|
||||
39
JOURNAL.md
39
JOURNAL.md
@ -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.
|
||||
|
||||
12
STATUS.md
12
STATUS.md
@ -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
24
flake.lock
generated
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
12
flake.nix
12
flake.nix
@ -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.
|
||||
|
||||
@ -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
18
modules/secrets.nix
Normal 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 = { };
|
||||
};
|
||||
}
|
||||
30
secrets/secrets.yaml
Normal file
30
secrets/secrets.yaml
Normal 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
|
||||
Reference in New Issue
Block a user