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#

  1. 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.
  2. 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.
  3. 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).
  4. Nothing lands unproven. No configuration reaches master without passing automated evaluation, and nothing family-critical reaches the fleet without building and, where applicable, passing the compliance VM test.
  5. Family-first operations. Deploys happen inside sane change windows, not during dinner, homework, or a movie. The people are the SLA.
  6. 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.

flowchart LR A[Plan / Issue] --> B[Branch] B --> C[Develop + local checks] C --> D[Push: fast eval gate] D --> E[PR to master: full build gate] E --> F[Self-review + merge] F --> G[release tag + branch advance] G --> H[Fleet converges / deploy-rs] H --> I[Operate / Monitor] I -->|rollback if needed| H I --> A

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 to master.
  • nix develop gives the pinned toolchain; treefmt and pre-commit hooks (git-hooks.nix) format and lint on commit.
  • just preflight locally 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 = false since 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 ci on ereshkigal (the automation PR gets one automatically; a hand-made PR does not — run just gate <host> for every affected host first). Squash-merge to keep master linear.

4.5 Release#

  • A push to master that changes flake.lock triggers release.yml: it cuts a CalVer tag (vYYYY.MM.DD[.n]) and fast-forwards the release branch.
  • release is the moving pointer the fleet follows — not every master commit ships automatically; the release branch is the deliberate ship line.

4.6 Deploy / Converge#

  • Hosts follow release via system.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 release pointer 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 → release tag. Direct pushes to master are (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/run as a real directory before systemd-tmpfiles makes the /var/run → /run symlink, systemd taints var-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.nix still carries that path and is enabled nowhere.
  • Working with secrets: a declared sops.secrets."a/b" must exist in secrets.yaml at eval time (add a placeholder first); write with sops set secrets.yaml '["a"]["b"]' '"value"', verify with sops --decrypt --extract; render files that mix secrets and config with sops.templates + config.sops.placeholder (pattern: modules/flake/nix/flakehub-netrc.nix); DynamicUser units can read a root-owned 0400 secret via EnvironmentFile. External accounts and their rotation tests: Accounts & Credentials.
  • Evidence: secrets.yaml is ciphertext; .sops.yaml recipient list; per-host key rotation via just sops-updatekeys. Continuity gap, named: pki/db decrypts 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.yml runs vulnix + sbomnix; findings with CVSS > 7.0 are surfaced. Accepted findings go in whitelist.toml with a written justification — an exception must be reasoned, not silent.
  • Evidence: vulnix.json, critical_cves.json, sbom.json, whitelist.toml.
  • Known gap

    cve.yml is 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 the scf-add-control, scf-generate, and scf-check workflows.
  • Evidence: the compliance.evidence attestation 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 sc2in inputs (corp → praxis, PolicyPress) are fetched over git+ssh using a dedicated machine-account key (SC2_CI_SSH_KEY), not a personal credential. The key belongs to the GitHub machine account sc2ben (title nff-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: its om ci stubs 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_KEY eval.yml, checks.yml, docs.yml Fetch the private sc2in inputs (any host eval needs them — every input is in every host’s nix.registry) sops github/ssh-key (Ben’s personal key)
    CLOUDFLARE_API_TOKEN docs.yml Cloudflare Pages deploy of master-documentation (docs.falseblue.com) sops cloudflare/terraform-token (DNS + Zero Trust) and cloudflare/inaba (ACME)
    CLOUDFLARE_ACCOUNT_ID docs.yml Pages account (not secret; same value as modules/flake/terraform/default.nix) —
    ACTIONS_PAT docs.yml gitHubToken for cloudflare/pages-action deployment statuses sops 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 watching eval.yml go green. The others: regenerate in the issuing console, gh secret set, push a docs change and watch docs.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 ci on ereshkigal does.
  • Touching a host path, an overlay, a substituter list (modules/flake/nix/nixos.nix), or the nixos-raspberrypi pin? Run nix build --dry-run .#nixosConfigurations.octopi.config.system.build.toplevel and confirm nothing under “will be built” mentions linux-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.nix with pytestCheckHook) so nix build .#<pkg> is the test run, plus an offline smoke check under modules/flake/checks/ registered in modules/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 & release branch — what shipped and when.
  • Compliance attestation — compliance.evidence + rendered report.
  • In-repo registers — In-flight work, Accounts & Credentials, the runbooks, and .claude/*.md for 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.