SDLC & Minimal Policies#
A software development life-cycle and the smallest set of policies that make it real: for a solo practice whose production system is a family.
Read this as written
This is deliberately informal, yet formal enough. I write security and compliance frameworks for a living, and I know the difference between governance and governance-pretend. Everything below is a control I actually run, phrased at the altitude a one-person shop can sustain. If a line here ever stops matching what the repository does, the repository wins: fix the doc.
1. Purpose & Scope#
This document governs this flake — tsunaminoai/nix-flake-final,
the single Nix configuration that builds and operates every computer, server,
and gadget in the household and at the family sites (Lafayette and Mom &
Pop’s place in Bedford).
It is scoped to this repository because this is the one repo I maintain where a mistake doesn’t just cost me — it takes down the family’s Wi-Fi, DNS, documents, Home Assistant, and phones. That production obligation is what earns the extra formality.
In scope: changes to NixOS / nix-darwin / home-manager configuration, modules, packages, secrets, and the documentation in this repo, and their deployment to the live fleet.
Out of scope (governed only informally): my other repositories (see §2). They carry no family-availability obligation, so they run on the spirit of this SDLC without the ceremony.
2. The Repo Constellation#
This SDLC is written for this flake, but it’s informed by how I actually work and on the following:
| Group | Repositories | Availability obligation |
|---|---|---|
| This homelab | nix-flake-final |
High — the family depends on it daily |
| GRC / compliance work | corp, praxis, PolicyPress (private, sc2in), security-center, internal-grc-system, ai-compliance, scf-api |
Professional — governed by their own controls |
| Zig experiments & forks | atlas, clay-zig, tomlz, zig-time, zcloc, md4c, ggtag, haxo-rs, blackstripes, game-boy, tinyradio, lastchoice, and my build-system branches on DOOM, nmap, gnuradio, FluidX3D, CM1 |
None — pure R&D |
| Web / creative | falseblue.com, craton.net, sc2.in, liturgy-latex, intaglio, ukiyoe, anikifm |
Low — mostly static on Cloudflare |
Why only this one is ‘production’
Most of my repos are experiments, forks I’m porting to a Zig build, or content sites. This flake is the only one with live human dependents who are not me. Proportionality (below) is the whole point: match ceremony to blast radius, and this is the repo with the biggest blast radius at home.
3. Guiding Principles#
- Proportionality. Formality scales with blast radius. A change to my prompt theme is not a change to the household DNS resolver. Tier your changes (§7) and spend rigor where a failure hurts the family.
- Determinism & reproducibility. Everything is Nix. A host can be rebuilt from scratch and come back identical. This is the primary safety control — it makes every change reversible and every rollback boring.
- Everything-as-code, one source of truth. Configuration, secrets (sealed), documentation, compliance controls, and topology all live in this repo. If it isn’t in the flake, it isn’t real (and if it must live outside — cloudflared on voile, HAOS on the appliance — that exception is written down).
- Nothing lands unproven. No configuration reaches
masterwithout passing automated evaluation, and nothing family-critical reaches the fleet without building and, where applicable, passing the compliance VM test. - Family-first operations. Deploys happen inside sane change windows, not during dinner, homework, or a movie. The people are the SLA.
- Honest self-governance. I am the developer, the reviewer, the approver, and the operator. I don’t pretend that’s segregation of duties — I name it as a risk and lean on automation as the compensating control (§5).
4. The Life Cycle#
The flow is intentionally small: branch → prove → merge → tag → converge → operate.
4.1 Plan / Intake#
Work starts as a GitHub issue; parked or half-done work is registered in In-flight work so it survives a change of maintainer. Classify the change’s tier (§7) before touching code — that decides how much of the rest of this section applies.
4.2 Develop#
- Cut a branch off
master; aspire to never commit directly tomaster. nix developgives the pinned toolchain; treefmt and pre-commit hooks (git-hooks.nix) format and lint on commit.just preflightlocally before pushing when the change is non-trivial.- Commits use conventional-commit subjects (
type(scope): subject) and carry no attribution trailers. Signing is currently off (commit.gpgSign = falsesince 2026-06-26): only the Nano 5C can sign, and the 5C NFC’s OpenPGP key was never provisioned — see YubiKey recovery.
4.3 Verify (CI is the reviewer I don’t have)#
Two-speed gating, by design (see .github/workflows/):
| Gate | Workflow | Trigger | What it does |
|---|---|---|---|
| Fast eval | eval.yml |
every branch push | nix flake check --no-build — catches eval errors, dangling refs, bad option refs. Cheap, quick feedback. |
| Full build | checks.yml |
PR to master and the master push after merge |
Full nix flake check that builds the checks: the compliance VM test (checks.compliance), the per-host nginx-config-<host>, gatus-config-<host> and telemetry-rules-<host> checks, and .#docs. It does not build host toplevels or the graphical/deploy VM tests (nixosConfigsBuildable is commented out in modules/flake/checks/builders.nix; full.nix exposes the VM tests as packages). Only om ci on ereshkigal builds host closures — om ci triage. Also re-runs on the merge commit to catch two-green-PRs-merge-red. |
| CVE / SBOM | cve.yml |
manual (workflow_dispatch) |
vulnix scan + sbomnix SBOM; flags CVSS > 7.0 findings. Currently manual — see §6. |
| Docs | docs.yml |
master push touching docs/modules/lib |
Builds .#docs, publishes to docs.falseblue.com (Cloudflare Pages). |
4.4 Review & Merge#
- Open a PR into
master. Even solo, the PR is the record: the diff, the CI result, and my reasoning in one place. - Run the self-review checklist honestly.
- Merge only when the full build gate is green — and remember what it does not
prove: GitHub CI builds no host toplevels, so a lock bump, overlay, or module
change also needs a green
om cion ereshkigal (the automation PR gets one automatically; a hand-made PR does not — runjust gate <host>for every affected host first). Squash-merge to keepmasterlinear.
4.5 Release#
- A push to
masterthat changesflake.locktriggersrelease.yml: it cuts a CalVer tag (vYYYY.MM.DD[.n]) and fast-forwards thereleasebranch. releaseis the moving pointer the fleet follows — not everymastercommit ships automatically; the release branch is the deliberate ship line.
4.6 Deploy / Converge#
- Hosts follow
releaseviasystem.autoUpgrade; interactive/targeted pushes use deploy-rs. - Stage family-critical changes: prove on a low-stakes host (e.g.
mokou) first, then converge the rest. - Respect the change window. Announce anything that could interrupt Wi-Fi, DNS, or Home Assistant.
4.7 Operate & Monitor#
- Telemetry (VictoriaMetrics/Logs + Grafana + Alloy) and the public status pages watch availability.
- Runbooks (
runbooks/) and the emergency guide cover the known failure modes and SPOFs (e.g. the HA Green triple-SPOF).
4.8 Rollback#
- Every deploy is a Nix generation:
nixos-rebuild --rollback/ boot the previous generation is the first move. - Reverting the
releasepointer or the offending PR restores the fleet’s target (git push origin +<good-tag>:release). Hosts that already converged do not roll themselves back — Retract a bad release. - Rollback is expected and cheap, not a failure event. Reach for it early.
5. Roles (honest about being one person)#
| Role | Who | How the conflict is managed |
|---|---|---|
| Developer | Ben | — |
| Reviewer / Approver | Ben | Automation is the second set of eyes. No merge without green CI; the self-review checklist is mandatory, not optional. |
| Operator | Ben | Staged rollout + rollback-by-default limit operator error. |
| Users / stakeholders | The family | Their experience is the acceptance test. “Take a photo and text Ben” is the highest level incident-report channel. |
Named limitation: no segregation of duties
A solo practice cannot separate author from approver. I don’t paper over it. The compensating controls are: deterministic builds, mandatory automated gates, staged rollout, trivially cheap rollback, and everything-as-code so any change is auditable after the fact.
6. Minimal Policies#
The smallest set worth writing down. Each is intent → control → evidence.
6.1 Change Management#
- Intent: no unproven change reaches the family’s systems.
- Control: branch → PR → green full-build gate → squash-merge →
releasetag. Direct pushes tomasterare (almost) never done. - Evidence: PR history, CI run logs (artifacts retained 30 days), CalVer tags.
6.2 Secrets Management#
- Intent: no plaintext secret in the repo or the Nix store.
- Control: sops-nix with age keys; policy in
.sops.yaml. Secrets decrypt to/run/secrets— never/var/run/*: sops-install-secrets would create/var/runas a real directory before systemd-tmpfiles makes the/var/run → /runsymlink, systemd taintsvar-run-bad, and dhcpcd/nscd fail read-only, so the host gets no IP on any interface (this took octopi off the network in 2026-06).modules/nixos/media/spotify.nixstill carries that path and is enabled nowhere. - Working with secrets: a declared
sops.secrets."a/b"must exist insecrets.yamlat eval time (add a placeholder first); write withsops set secrets.yaml '["a"]["b"]' '"value"', verify withsops --decrypt --extract; render files that mix secrets and config withsops.templates+config.sops.placeholder(pattern:modules/flake/nix/flakehub-netrc.nix);DynamicUserunits can read a root-owned0400secret viaEnvironmentFile. External accounts and their rotation tests: Accounts & Credentials. - Evidence:
secrets.yamlis ciphertext;.sops.yamlrecipient list; per-host key rotation viajust sops-updatekeys. Continuity gap, named:pki/dbdecrypts to two personal keys only — see CA escrow.
6.3 Vulnerability Management#
- Intent: know what’s in the closure and whether it’s vulnerable.
- Control:
cve.ymlruns vulnix + sbomnix; findings with CVSS > 7.0 are surfaced. Accepted findings go inwhitelist.tomlwith a written justification — an exception must be reasoned, not silent. - Evidence:
vulnix.json,critical_cves.json,sbom.json,whitelist.toml. -
Known gap
cve.ymlis currently manual (workflow_dispatch); the push/PR triggers are commented out. Re-enabling scheduled scanning is a tracked improvement, not the current state. Don’t claim continuous scanning until it’s on.
6.4 Compliance-as-Code#
- Intent: security controls are enforced by the build, not by good intentions.
- Control: the SCF-driven compliance layer (
modules/compliance/) applies Secure Controls Framework controls as NixOS modules and attests them inside a NixOS VM test that runs in the full-build gate. Managed via thescf-add-control,scf-generate, andscf-checkworkflows. - Evidence: the
compliance.evidenceattestation trail and the rendered attestation report. See Compliance.
6.5 Access Control & Third-Party Inputs#
- Intent: least privilege for both people and machine-to-machine trust.
- Control: service exposure is Tailscale / Cloudflare-Access gated; the private
sc2ininputs (corp→praxis,PolicyPress) are fetched overgit+sshusing a dedicated machine-account key (SC2_CI_SSH_KEY), not a personal credential. The key belongs to the GitHub machine accountsc2ben(titlenff-ci (private flake inputs: corp/praxis/PolicyPress)); it inherits sc2ben’s write scope on those repos, so a leak is mitigated by rotation. ereshkigal has no copy: itsom cistubs corp (--override-input flake/corp path:./ci/stub-corp), so the real SC2 module is validated only by GitHub CI. -
GitHub Actions repository secrets (distinct from anything in sops):
Secret Used by Purpose Not to be confused with SC2_CI_SSH_KEYeval.yml,checks.yml,docs.ymlFetch the private sc2ininputs (any host eval needs them — every input is in every host’snix.registry)sops github/ssh-key(Ben’s personal key)CLOUDFLARE_API_TOKENdocs.ymlCloudflare Pages deploy of master-documentation(docs.falseblue.com)sops cloudflare/terraform-token(DNS + Zero Trust) andcloudflare/inaba(ACME)CLOUDFLARE_ACCOUNT_IDdocs.ymlPages account (not secret; same value as modules/flake/terraform/default.nix)— ACTIONS_PATdocs.ymlgitHubTokenforcloudflare/pages-actiondeployment statusessops github/automation-token(stage-1 auto-update PAT) -
Rotation:
SC2_CI_SSH_KEY—ssh-keygen -t ed25519, add the public half to sc2ben (gh ssh-key add … --type authentication),gh secret set SC2_CI_SSH_KEY -R tsunaminoai/nix-flake-final < key, delete the old key; accept by pushing a branch and watchingeval.ymlgo green. The others: regenerate in the issuing console,gh secret set, push a docs change and watchdocs.yml. Full per-credential detail: Accounts & Credentials. - Evidence: CI SSH setup steps; registry wiring in
modules/flake/nix/;ci/stub-corp/flake.nix.
6.6 Backup & Recovery#
- Intent: the family’s data and the ability to rebuild survive a host loss.
- Control: Borg/BorgWarehouse backups; Nix makes the systems reproducible so recovery is rebuild-plus-restore-data, not reinstall-from-memory.
- Evidence: backup schedules and repos documented in Backups and the runbooks; retention (3 daily + 4 weekly archives per repo) in Backups → Retention.
6.7 Documentation#
- Intent: anyone in the family can act without me; future-me can understand past-me.
- Control: docs live in-repo and auto-publish to docs.falseblue.com; the family-facing guides are written for non-technical readers.
- Evidence: this docs site;
docs.yml.
6.8 Licensing & Non-Commercial Use#
- Intent: the license reflects that this is personal, non-commercial research.
- Control: Apache 2.0, no warranty, commercial users assume all liability
(see
README). - Evidence:
LICENSE,README.md.
7. Change Tiers#
Proportionality made concrete. Tier the change before you start.
| Tier | Examples | Required rigor |
|---|---|---|
| T1 — Family-critical | DNS, DHCP, Wi-Fi/RADIUS, Home Assistant, Paperless/documents, PKI/CA, the exit node, backups | Full gate + compliance VM green; stage on a non-critical host first; announce; deploy in a change window; rollback plan ready. |
| T2 — Household service | Jellyfin, Grafana, dashboards, scanners, non-critical containers | Full gate green; deploy off-window; watch telemetry after. |
| T3 — Personal / cosmetic | my desktop theme, shell, editor, dev tooling, docs wording | Fast eval green is enough; deploy anytime to my own machines. |
When unsure which tier, round up.
8. Self-Review Checklist#
Run before merging anything above T3:
- Change tier identified; rigor matches (§7).
- Fast eval gate green; for T1/T2, full build gate green (compliance VM included where applicable).
-
just gate <host>run for every host the change touches (format, host toplevel, telemetry/gatus checks, docs). Remember GitHub CI does not build toplevels;om cion ereshkigal does. - Touching a host path, an overlay, a substituter list
(
modules/flake/nix/nixos.nix), or thenixos-raspberrypipin? Runnix build --dry-run .#nixosConfigurations.octopi.config.system.build.topleveland confirm nothing under “will be built” mentionslinux-rpi— a cache-hit regression there is a 16–20 h emulated kernel build that no gate catches. - New custom package? Tests run in-derivation via
nativeCheckInputs(pkgs/worldsim/default.nixwithpytestCheckHook) sonix build .#<pkg>is the test run, plus an offline smoke check undermodules/flake/checks/registered inmodules/flake/checks/default.nix(checks/worldsim.nix). Worked example: Worldsim. - No new plaintext secret; anything sensitive went through sops.
- Blast radius understood — what breaks for the family if this is wrong?
- Are you pushing to master (eg will it affect any non-tsunami users if a rebuild is triggered)?
- Rollback path is clear (which generation / which revert).
- SPOFs and known-fragile areas (HA Green, exit node, octopi Wi-Fi) considered.
- Docs / runbooks updated if behavior the family sees changed.
- Deploy timing respects the change window.
9. Records & Evidence#
Where the audit trail actually lives — no separate GRC tool needed:
- Git history & PRs — every change, its reasoning, and its review.
- CI artifacts — flake-check / eval logs (30 days), CVE/SBOM results (90 days).
- CalVer tags &
releasebranch — what shipped and when. - Compliance attestation —
compliance.evidence+ rendered report. - In-repo registers — In-flight work, Accounts &
Credentials, the runbooks, and
.claude/*.mdfor agent-facing operational notes. Nothing load-bearing lives in a private memory store.
10. Review Cadence#
This is a living document. Review it:
- Quarterly, lightly — does it still describe what I actually do?
- On any material change to CI, the release flow, the secrets model, or the compliance layer.
- After any family-impacting incident — fold the lesson into a policy or a runbook so it can’t recur silently.
Last aligned with the repo: 2026-09-02.