docs: centralize documentation under docs/

Move standalone repository docs from the root into docs/ and add a documentation index that links the moved docs and the code-local runbooks.

Update Markdown links, source comments, and the root entry points to the new locations. Keep README.md and AGENTS.md at the root as required entry points.

Assisted-by: DeepSeek Harness (deepseek-flash)
This commit is contained in:
Ryan Yin committed 2026-11-02 17:48:57 +08:00
1 parent be3e27c49b
commit 4470b3d014
30 files changed
+176 -88

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
# Backups
How this fleet is backed up, where the data lives, and how to restore it.
## What protects against what
| Threat | Defence |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| accidental deletion, bad edit | btrbk: local btrfs snapshots |
| disk or host loss | restic: encrypted copies on youko |
| a compromised backup server | desktop repositories use a password youko never holds |
| a compromised host | youko takes hourly read-only btrfs snapshots of the REST store, which a desktop cannot reach; the immutable cloud copy (planned) is still the offsite layer |
## Layers
1. **btrbk — local snapshots** ([`modules/nixos/base/btrbk.nix`](../modules/nixos/base/btrbk.nix)).
Same-filesystem btrfs snapshots of `@persistent`, run on Tue/Sat 03:45:20, kept for 7 days
(minimum 2). Cheap rollback for mistakes; it does **not** survive disk loss.
2. **restic — encrypted copies on youko**
([`modules/nixos/base/restic-backup.nix`](../modules/nixos/base/restic-backup.nix)). Encryption
happens on the client, so the server only ever stores ciphertext. youko runs `restic-rest-server`
with `--private-repos`, authenticated by htpasswd, published behind caddy as
`restic.writefor.fun`. Deliberately not append-only: a desktop holds both the plaintext and the
repository password, so blocking deletion there protects little while denying the client the
automatic retention it needs. A compromised desktop can still `forget --prune` its own repository
through the REST API, so youko also snapshots `/data/backups` hourly into a read-only btrfs
subvolume the desktop cannot reach (see
`hosts/12kingdoms-youko/homelab-services/restic-server-snapshots.nix`).
3. **cloud copy** (planned). youko will copy the homelab repositories with `restic copy` (it holds
that password). Desktops copy their own: youko does not hold the desktop password. This layer is
what protects history against a compromised host, so the target should be immutable or versioned
(object lock or equivalent) rather than a plain bucket.
## Password model
Two independent repository passwords, deliberately not shared:
| Group | Secret | Recipients | Decrypts |
| ----------- | ----------------------------- | ----------------------- | -------------------- |
| desktop | `restic-password-desktop.age` | desktops | desktop repositories |
| homelab | `restic-password-homelab.age` | homelab hosts | homelab repositories |
| REST access | `restic-rest-credentials.age` | desktops | nothing (auth only) |
| REST access | `restic-rest-htpasswd.age` | the REST server (youko) | nothing (auth only) |
Consequences worth remembering:
- youko stores desktop repositories but cannot read or verify them. Any content check
(`restic snapshots`, `restic check`) for a desktop repository must run on that desktop.
- Keep both repository passwords offline (password manager or paper). If a host dies, its password
is the only way to restore its data.
- restic's own password is never stored inside a backed-up tree: `etc/agenix` and all key material
are excluded (see below).
- Recipients follow the secrets repository's rule: every secret is decryptable by the desktops
(`desktop_keys`, which also carries the offline `recovery_key`), so a desktop can edit or rekey
anything and no secret becomes unrecoverable when a host is lost. The desktop repository password
is the one secret the servers do not get.
## Schedule
Slots are assigned per host; the random delay only smooths load and must not be relied on to prevent
collisions.
| Time | Job |
| ---------------- | ----------------------------------- |
| 01:30 + 15m | desktops push to youko |
| 02:30 + 15m | youko backs itself up |
| hourly | youko snapshots the REST store |
| 03:45:20 Tue/Sat | btrbk local snapshots |
| 06:00 (planned) | youko copies homelab repos to cloud |
Set per host via `modules.restic-backup.onCalendar` and `randomizedDelaySec`.
## What is never backed up
- **Keys and credentials**, excluded by the module on every host: `etc/agenix`,
`etc/ssh/ssh_host_*`, `**/.ssh`, `**/.gnupg`, `**/.aws`, `**/.config/gcloud`. Backing these up
would put repository and host credentials inside the repository itself.
- **Regenerable bulk**, excluded per host: podman's storage tree, microVM/libvirt images, NFS and
cache/log directories on youko; language build artefacts, caches, model weights and anything
larger than 500M on desktops.
Prefer a specific `paths` allowlist plus a size cap over a long `exclude` list.
## Operations
```bash
# run a backup now, and read its log
systemctl start restic-backups-homelab
journalctl -u restic-backups-homelab -n 50 --no-pager
# timer state
systemctl list-timers 'restic-backups*'
```
Inspect or restore a repository where its password lives (a desktop repository on a desktop, a
homelab repository on a homelab host):
```bash
restic -r <repository> --password-file /run/agenix/restic-password snapshots
restic -r <repository> --password-file /run/agenix/restic-password check
restic -r <repository> --password-file /run/agenix/restic-password \
restore latest --target /tmp/restore --include /home/ryan/Documents
```
Remote (REST) repositories also need the service credentials:
```bash
set -a; . /run/agenix/restic-rest-credentials; set +a
restic -r rest:https://restic.writefor.fun/idols-ai/ \
--password-file /run/agenix/restic-password snapshots
```
Restoring a btrbk snapshot (offline; stop writers first):
1. `btrfs subvolume delete /btr_pool/@persistent`
2. `btrfs subvolume snapshot /btr_pool/@snapshots/@persistent.<timestamp> /btr_pool/@persistent`
3. reboot, or remount `/persistent`, to pick up the restored subvolume.
## Retention
- btrbk: automatic, 7 days with a 2 day minimum.
- youko's own restic repository: automatic, `--keep-daily 3 --keep-weekly 2 --keep-monthly 2`.
- Desktop restic repositories: automatic, the module defaults
(`--keep-daily 3 --keep-weekly 2 --keep-monthly 2`). Immutability is the cloud copy's job
(planned), not the local server's.
## Verifying
- Units: `systemctl status restic-backups-homelab`, `systemctl list-timers 'restic-backups*'`.
- Repository integrity (where the password is): `restic check`, which runs after every backup via
`modules.restic-backup.checkOpts` (a 5% data read, `1/20`, by default).
- Server side, ciphertext only: repository directories under `/data/backups/rest-server/<user>/`,
with no leftover files in `locks/` and no stale `.tmp` files.
- `restic-rest-server`'s log must not print `Invalid htpasswd entry`: that means the client's REST
password and the server's htpasswd disagree, and the credentials pair needs rebuilding from one
password.
- Metrics: the rest-server exposes the Go/promhttp defaults on loopback (scraped as the
`restic-rest-server` job; `/metrics` is blocked on the public vhost). Repository growth is charted
from `node-exporter`'s filesystem metrics for youko's `/data` mount instead — rest-server itself
has no per-repository size metrics.
- Failures are not silent: a failed `restic-backups-homelab` starts `restic-backup-notify`, which
posts to alertmanager and, above info severity, on to telegram. alertmanager clears it after
`resolve_timeout`.
## Planned
- **Restore drill**: actually restore a file from a restic snapshot and from a btrbk snapshot, on a
schedule. An unverified restore is not a backup.
- **Cloud copy**: youko copies the homelab repositories with `restic copy`; desktops copy their own.
The target must not live in the homelab, and should be immutable or versioned (object lock) — that
is the layer which survives a compromised host.
## Components
| Path | Role |
| --------------------------------------------------------------------------------- | ---------------------------------------------------- |
| [`modules/nixos/base/restic-backup.nix`](../modules/nixos/base/restic-backup.nix) | client module: repository, paths, excludes, schedule |
| [`modules/nixos/base/btrbk.nix`](../modules/nixos/base/btrbk.nix) | local btrfs snapshots |
| `hosts/12kingdoms-youko/default.nix` | the REST server and youko's own backup |
| `hosts/12kingdoms-youko/homelab-services/restic-server-snapshots.nix` | hourly read-only snapshots of the REST store |
| `hosts/12kingdoms-youko/homelab-services/caddy.nix` | the `restic.writefor.fun` vhost |
| `secrets/nixos.nix` | which secret is defined on which host |
| `hosts/idols-ai/restic.nix`, `hosts/12kingdoms-youko/default.nix` | per-host backup configuration |
+87 -3
View File
@@ -1,5 +1,89 @@
# Documentation
- [BIOS settings](./bios.md)
- [Wake-on-LAN](./wake-on-lan.md)
- [NixOS modernization options](./nixos-modernization-options.md)
Canonical index for this repository. Standalone reference material lives in this directory.
Component runbooks stay next to the code they describe so they remain visible during maintenance;
every human-facing document is linked here.
`README.md` and `AGENTS.md` remain at the repository root because GitHub and coding agents require
those entry points.
## Repository
- [Repository overview](../README.md) — systems, components, deployment and validation quick start.
- [Agent guide](../AGENTS.md) — repository conventions, command hazards and validation rules.
- [NixOS modernization options](nixos-modernization-options.md) — design options and decisions.
## Operations and policy
- [Security](SECURITY.md) — threat model, trust boundaries, implemented controls and audits.
- [Backups](BACKUP.md) — backup layers, password model, retention and restore procedure.
- [Workarounds and known gaps](WORKAROUNDS.md) — pins, temporary workarounds and removal conditions.
- [BIOS settings](bios.md) — firmware settings for the physical hosts.
- [Wake-on-LAN](wake-on-lan.md) — firmware prerequisites, NixOS setup and wake commands.
- [Runtime hardening validation](../hardening/tests/runtime-validation.md).
## Hosts and services
- [Host inventory and deployment notes](../hosts/README.md).
- AI desktop: [host runbook](../hosts/idols-ai/README.md) and [VR notes](../hosts/idols-ai/VR.md).
- Akane: [host runbook](../hosts/idols-akane/README.md).
- Aquamarine: [host runbook](../hosts/idols-aquamarine/README.md),
[iGPU notes](../hosts/idols-aquamarine/IGPU.md) and
[ROCm notes](../hosts/idols-aquamarine/ROCM.md).
- Kana: [host runbook](../hosts/idols-kana/README.md).
- Ruby: [host runbook](../hosts/idols-ruby/README.md).
- Shoukei: [host runbook](../hosts/12kingdoms-shoukei/README.md).
- Youko homelab: [services overview](../hosts/12kingdoms-youko/homelab-services/README.md),
[Immich](../hosts/12kingdoms-youko/homelab-services/immich/README.md),
[monitoring](../hosts/12kingdoms-youko/homelab-services/monitoring/README.md),
[alert rules](../hosts/12kingdoms-youko/homelab-services/monitoring/alert_rules/README.md),
[recording rules](../hosts/12kingdoms-youko/homelab-services/monitoring/recording_rules/README.md)
and [Grafana dashboards](../hosts/12kingdoms-youko/homelab-services/grafana/dashboards/README.md).
- Kubernetes cluster: [k8s host notes](../hosts/k8s/README.md) and
[disko configuration](../hosts/k8s/disko-config/README.md).
- Infrastructure: [infra overview](../infra/README.md), [RustFS](../infra/rustfs/README.md),
[Loki storage](../infra/rustfs/loki/README.md) and
[Terraform S3 backend](../infra/rustfs/tf-s3-backend/README.md).
- NixOS installation: [installer guide](../nixos-installer/README.md) and
[Shoukei notes](../nixos-installer/README.shoukei.md).
- Certificates: [certificate setup](../certs/README.md).
## Configuration structure
- [NixOS and nix-darwin modules](../modules/README.md), including
[Darwin modules](../modules/darwin/README.md),
[Mihomo networking](../modules/nixos/desktop/networking/mihomo/README.md) and
[Sunshine](../modules/nixos/desktop/networking/sunshine/README.md).
- [Home Manager modules](../home/README.md).
- [Flake outputs and tests](../outputs/README.md).
- [Secrets management](../secrets/README.md).
- [Overlays](../overlays/README.md), [shared library helpers](../lib/README.md) and
[shared variables](../vars/README.md).
- [Hardening wrappers and policy](../hardening/README.md).
## Desktop and Home Manager details
- [Darwin](../home/darwin/README.md), [per-host Home Manager profiles](../home/hosts/README.md) and
[Linux](../home/linux/README.md).
- [Linux desktop stack](../home/linux/gui/README.md): [base GUI](../home/linux/gui/base/README.md),
[Fcitx5](../home/linux/gui/base/fcitx5/README.md),
[hypridle](../home/linux/gui/base/hypridle/README.md),
[vinput](../home/linux/gui/base/vinput/README.md) and [i3](../home/linux/gui/i3/README.md).
- [Base Home Manager profile](../home/base/README.md):
[editors](../home/base/core/editors/README.md),
[editor glossary](../home/base/core/editors/Glossary.md),
[Helix](../home/base/core/editors/helix/README.md),
[Neovim](../home/base/core/editors/neovim/README.md), [Rime](../home/base/gui/rime/README.md) and
[terminal](../home/base/gui/terminal/README.md).
- Terminal tools: [agents](../home/base/tui/agents/README.md) and
[dsh notes](../home/base/tui/agents/dsh.md), [editors](../home/base/tui/editors/README.md),
[encryption](../home/base/tui/encryption/README.md), [GPG](../home/base/tui/gpg/README.md) and
[password store](../home/base/tui/password-store/README.md).
## Agent resources
- [Global rules, skills and evaluations](../agents/README.md).
- [Skill catalog](../agents/skills/README.md).
- [Global agent rules](../agents/global-rules.md),
[evaluation guide](../agents/evals/global-rules.md) and
[tooling installation notes](../agents/install-tooling.md).
+431
View File
@@ -0,0 +1,431 @@
# OS and Homelab Security
This is a personal infrastructure repository, not a supported security product. This document
describes the intended architecture, controls present in source, their limitations, and remaining
work. A configured control is not evidence that it is active on a running machine.
Related documents: [application hardening](../hardening/README.md) for sandbox wrappers and Linux
hardening notes, and [WORKAROUNDS.md](./WORKAROUNDS.md) for temporary exceptions and known gaps.
## Reporting problems
For non-sensitive configuration bugs, open an issue with the affected host role and a redacted
reproduction. Do not post credentials, kubeconfigs, private keys, secret contents, or exploit
details that would expose a live service. For sensitive findings, contact the maintainer privately
through the contact information on their GitHub profile first. There is no guaranteed response SLA.
## Threat model and priorities
Protect administrator credentials, desktop application data, cluster/storage state, and the ability
to recover after compromise or an unsuccessful update. Relevant attackers include compromised
applications and dependencies, malicious workloads, and devices or accounts on trusted networks.
Physical theft and boot tampering matter for desktops and physical hosts too.
Prioritize **patching, reducing exposure, least privilege, isolation, and recovery**, in that order
for each affected service. Kernel mitigations and module deny lists are defence in depth, not fixes
for vulnerabilities. A container shares its host kernel; a VM provides a separate kernel but still
exposes a hypervisor and host-side device/share implementations. Neither makes arbitrary code safe.
Use proportionate hardening: preserve normal performance, developer workflows and required cluster
features. Do not globally disable user namespaces, io_uring, SMT or hardware acceleration, switch to
a hardened kernel, or enforce untested AppArmor policies just to maximize restrictions. Prefer
small, reversible controls and narrow exceptions; benchmark before adopting expensive mitigations.
## Architecture and trust boundaries
| Boundary | Current design | Limitation |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Administration | Key-based SSH; root key login retained for Colmena/MicroVM deployment; only keys declared in Nix are honoured, and no password or keyboard-interactive login; remote-builder host keys only reach `nix-daemon` | Deployment credentials are root-equivalent file keys, usable by any same-user process through the desktop SSH agent; one stolen key reaches every host |
| Host network | Shared nftables firewall trusts the home LAN, tailnet interface and local Podman bridge; IPv6 link-local is accepted | These are broad trust zones, not service-level least privilege; router exposure and tailnet ACLs require separate verification |
| Monitoring | node/smartctl exporters bind the host LAN IPv4 (9100/9633); firewall rules restrict exporter ports 9100, 9835 and 9633 to the monitoring host over IPv4. RAPL counters are exposed only on x86 server hosts, to `node-exporter` through the `rapl` group. | nvidia_gpu_exporter (9835) still binds wildcard; the IPv6 denial for those three ports is in the shared firewall; current running hosts require separate verification. The node_exporter account can measure package energy for local side channels; interactive users, desktops, and guests keep the kernel-default root-only permission. |
| VM hosts | shoryu, shushou and youko run guests behind `br0`; ruby/kana are libvirt guests | A bridge is not network isolation; host compromise affects its guests |
| k3s | Three control-plane and three worker MicroVMs; Cilium, API VIP and NFS CSI | Cluster-admin, privileged/host-mounted pods and writable NFS data are powerful trust boundaries |
| MicroVM storage | Host store shared read-only through virtiofs; guest `/etc`, `/var` and `/home` persisted in separate images | Read-only store does not isolate the host-side virtiofs implementation or protect writable guest state |
| Desktop apps | Selected apps use nixpak/bubblewrap; Podman is rootless | Rules and shared directories vary per app; user namespaces are needed for these sandboxes |
| Secrets | agenix declarations here, encrypted data in the private nix-secrets repository | Recipients and plaintext file permissions must be reviewed; root on a recipient can read its decrypted secrets |
| Supply chain | Locked flake inputs; explicit binary-cache trust keys on NixOS; normal user is not a Nix trusted-user | A trusted cache or imported Nix module can supply code executed as root; a lock file is not a security audit |
| Recovery | NixOS generations, btrbk/restic, hourly read-only snapshots of the REST store, and documented backup procedures | A rollback does not undo compromised data or secrets; backup configuration alone does not prove restore works; the offsite immutable copy is still planned |
Sources: [host layout](../hosts/README.md), [k3s deployment](../hosts/k8s/README.md),
[firewall](../modules/nixos/base/networking/firewall.nix), [SSH](../modules/nixos/base/ssh.nix),
[Nix trust](../modules/base/nix.nix), [secrets](../secrets/README.md), and [backup](./BACKUP.md).
Cluster resources are maintained in the separate k8s-gitops repository.
The Macs set `nix.enable = false` ([nix-core.nix](../modules/darwin/nix-core.nix)), so nix-darwin
writes no `/etc/nix/nix.conf`: the substituters, trusted keys and access-token include declared here
do not reach them. Their Nix settings come from the Determinate installation, which this repository
does not manage; those settings were not verified on the Macs.
## Kernel and process baseline
The following explicit, overridable baseline is implemented in
[kernel-hardening.nix](../hardening/kernel-hardening.nix), which holds the values; the table below
summarizes them. It has Linux eval coverage and explicit ARM/RISC-V server imports. Source
implementation is not runtime verification. Some values are already kernel/systemd/NixOS defaults;
Darwin does not receive Linux sysctls.
| Setting | Value | Effect and compatibility |
| ------------------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `kernel.dmesg_restrict` | 1 | Kernel log access requires CAP_SYSLOG; journal access has separate permissions |
| `kernel.kptr_restrict` | 2 | Hide pointers printed with `%pK`, even from privileged readers; not a blanket ban on address leaks |
| `kernel.yama.ptrace_scope` | 1 | Restrict unrelated-process tracing; child-process debugging and explicit PR_SET_PTRACER exceptions remain possible |
| `kernel.perf_event_paranoid` | 2 | Restrict unprivileged kernel profiling; user-space profiling remains available; do not assume distro-specific 3/4 semantics |
| `kernel.unprivileged_bpf_disabled` | 2 | Disable unprivileged `bpf()` with an administrator-reversible setting; privileged Cilium/agents retain access |
| `net.core.bpf_jit_harden` | 1 | Harden only unprivileged BPF JIT; do not enable JIT blinding for privileged Cilium/observability workloads |
| `vm.unprivileged_userfaultfd` | 0 | Restrict unprivileged kernel-mode fault handling, not all userfaultfd usage; `/dev/userfaultfd` permissions are a separate boundary |
| `fs.protected_symlinks`, `fs.protected_hardlinks` | 1 | Mitigate cross-user link attacks; these controls are boolean |
| `fs.protected_fifos`, `fs.protected_regular` | 2 | Restrict unsafe O_CREAT access in world/group-writable sticky directories; shared temporary-file workflows may need adjustment |
Some values already come from kernel/systemd/NixOS defaults. Explicit declarations make the desired
baseline reviewable; they do not imply the previous system had no protection. Unsupported kernel
features may lack their sysctl paths, especially on custom kernels: runtime verification is
required.
JIT hardening is deliberately limited to unprivileged programs rather than forced on privileged
agents. Together with `unprivileged_bpf_disabled=2`, this preserves Cilium's normal privileged BPF
path while retaining protection if an administrator temporarily permits unprivileged BPF. This is a
configuration trade-off, not a measured claim of zero overhead.
User namespaces remain enabled: disabling them globally conflicts with Nix sandboxing and breaks
rootless containers and application sandboxes. Nonprivileged seccomp filters are not disabled by the
unprivileged `bpf()` restriction. Do not disable Cilium's privileged BPF, tc, container networking,
NFS, or required VM/filesystem modules in pursuit of a larger deny list.
The existing `esp4`, `esp6`, `rxrpc` deny list blocks modprobe/autoload paths. It does not unload
already loaded modules, affect built-in code, or stop a privileged direct module insertion. Verify
an advisory and affected kernel versions before adding CVE claims; search snippets are not evidence.
### Boot integrity and mandatory access control
- ai configures Lanzaboote in [secureboot.nix](../hosts/idols-ai/secureboot.nix). Confirm firmware
key enrollment and actual Secure Boot state with `sbctl status`; do not assume all hosts have it.
- Secure Boot, signed boot images, kernel lockdown and module signature enforcement are distinct
controls. Test custom kernels and NVIDIA/out-of-tree modules before extending enforcement.
- AppArmor is enabled, but the only explicit policy, sudo's, is in complain mode. Complain mode logs
rather than blocks. Existing FHS-oriented abstractions need Nix store path review; this is not
broad enforced application confinement.
- Systemd service sandboxing is per-service, not a global policy. The exposed services nixpkgs
leaves weakly sandboxed (caddy, rustfs, vmalert, whisper-asr) use the preset in
[systemdHardening.nix](../lib/systemdHardening.nix). Review capabilities, writable paths, network
access and syscall requirements before tightening an individual service.
See [application hardening](../hardening/README.md) for wrapper details.
### Audit trail
`modules.security.audit` ([auditd.nix](../modules/nixos/base/auditd.nix)) runs auditd on youko,
shoryu, shushou and aquamarine. It records, for login sessions only (SSH, and sudo or su from one):
every root command (`root-session`), access to decrypted secrets and SSH host keys
(`credential-access`), writes to SSH, account/PAM, Nix trust and audit configuration, module loads,
and denials.
- Services are not audited: a `never,task` rule gives tasks without a login uid no audit context, so
virtiofsd, hypervisors, NFS, PostgreSQL and Nix builds skip the per-syscall audit path. A
compromised service is therefore invisible until it opens a login session.
- Logs stay on the host and root can erase them; shipping them off-host is a P1 TODO.
- One watch on a missing path makes `auditctl -R` stop and leaves auditing off;
`nix build .#checks.x86_64-linux.security-auditd` loads the rules in a VM.
Query as root; sshd is built without libaudit, so use PAM's `USER_START` records for logins:
```sh
ausearch -i -m USER_START --start today # logins with address and session id
ausearch -i -k root-session --start today # root commands, with auid and ses
ausearch -i --session <ses> # everything one session did
```
## Updating, deploying and recovering
1. Check the advisory and affected package/kernel; prioritize active exploitation and reachable
attack paths. Review changed inputs and cache trust instead of treating updates as inherently
safe.
2. Run `just test` (must pass), build affected systems/runners and review closure differences.
Evaluate tests cannot prove runtime enforcement or workload compatibility.
3. Record the old system generation/runner and ensure backups and recovery access exist. Use `boot`
mode for bridge/network-stack changes or broad VM-host updates.
4. Deploy MicroVM closures to the **physical host**, not the guest's read-only store. Install the
reviewed runner and activate/restart through the documented MicroVM path. Preserve all images.
5. Keep control-plane restarts sequential to retain etcd quorum. Worker concurrency is an explicit
availability trade-off; simultaneous worker restarts can interrupt all worker workloads.
6. Verify the active/booted generation, guest boot ID, failed units, API VIP `/readyz`, Nodes,
Cilium, CSI and workloads. Updating a kernel on disk does not patch the running kernel: reboot
and verify.
7. On failure, restore the recorded generation/runner and recheck health before continuing. Preserve
rollback roots until stable. If compromise is suspected, isolate first, rotate affected secrets,
and restore verified data; merely switching generations is not incident recovery.
For the sysctl baseline, query the keys above with `sysctl <key>`, check
`journalctl -b -u systemd-sysctl`, and test rootless Podman, a sandboxed Nix build, desktop
sandboxes, debugging, and cluster networking/storage after activation. Do not globally relax
hardening to hide one application's incompatibility: make a narrow documented exception and test it.
These sysctls are reversible by administrator configuration, unlike `unprivileged_bpf_disabled=1` or
`kexec_load_disabled=1`, which cannot be relaxed without rebooting. This baseline does not enable
the latter or `security.protectKernelImage` (which also disables hibernation).
## API endpoint and TLS
The canonical endpoint is **`https://test-cluster-1.writefor.fun:6443`**. DNS points it to
kube-vip's `192.168.5.193`; the three servers include the DNS name in their serving-certificate
SANs, and the joining servers/agents use that domain. The first server initializes the cluster
without a join address. This is already a domain-based HA endpoint, not a bare-IP client design.
Use the existing authenticated client configuration to verify it:
```sh
k3s kubectl --server=https://test-cluster-1.writefor.fun:6443 --request-timeout=15s get --raw=/readyz
```
Bare-IP access may fail TLS hostname verification because the certificate does not include the VIP
IP SAN. That is not a fault in the canonical endpoint and does not require certificate rotation. If
bare-IP disaster-recovery access becomes an explicit requirement, add that SAN through normal
configuration and certificate lifecycle procedures. Never bypass TLS verification to conceal a
hostname mismatch. Eval tests protect the domain join endpoint and server SAN declarations; live TLS
checks remain necessary after deployment.
## Implementation status and completed audit findings
### Implementation status
| Component | Status | Runtime verification |
| ---------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------- |
| Architecture, agent-first policy, audits and canonical API tests | In this repository | Only the dated observations below were checked live |
| Explicit sysctl baseline and IPv6 exporter defence | In `hardening/` and the shared firewall | Live on 13 reachable hosts (readback 2026-10-22); ai root-privileged items still owed |
### Baseline implementation checks
- An explicit, overridable sysctl baseline preserves user namespaces and privileged BPF performance.
- IPv6 exporter denial precedes broad LAN/tailnet/container rules. The monitoring configuration uses
IPv4 targets for these ports, so no scrape endpoint migration is needed.
- x86_64/aarch64 eval checks cover baseline values, normal host overrides and generated exporter
rules. Canonical API endpoint checks cover all six cluster nodes.
- A reusable isolated VM check exercises allowed monitoring IPv4 access, denied other IPv4/IPv6
access to all three exporter ports, retained IPv6 loopback and a reachable ordinary IPv4/IPv6
service. Run it with `nix build .#checks.x86_64-linux.security-exporters`. It imports only the
shared firewall, not real-host secrets or workloads, and does not replace deployment-time checks.
### Read-only audit snapshot — 2026-10-03
- **API:** the canonical domain resolves to the VIP; authenticated `/readyz` returned `ok` with
normal TLS validation. No DNS or certificate changes were needed.
- **ai boot:** `sbctl status` reports Secure Boot enabled and Setup Mode disabled. It also reports
legacy sbctl configuration; migration is separate work, not permission to change firmware keys.
- **Listeners:** SSH listens on IPv4/IPv6 on sampled VM hosts and a master. SMART exporters bind the
physical hosts' LAN IPv4 addresses; node_exporter has wildcard listeners. youko's NFS listens on
IPv4/IPv6, and the sampled master API has a wildcard listener. These are listener observations,
not proof of reachability from every network or of router/WAN protection.
- **Workload inventory:** 36 of 78 observed pods had host networking, hostPath or explicitly
privileged containers. They were infrastructure components: Cilium, Istio CNI/ztunnel, NFS CSI,
kube-vip and OpenTelemetry log agents. Their host access is not automatically a misconfiguration;
remove it only after validating the component's requirements. Application pods were not flagged by
this limited check; that does not establish full workload isolation.
- **RBAC:** explicit cluster-admin bindings include `system:masters` and the Flux kustomize/Helm
controller service accounts. Operators also have broad resource permissions. Flux's bootstrap
privileges are intentional for cluster reconciliation; Git write access and controller credentials
therefore remain administration boundaries. This inventory is not a complete effective-access or
privilege-escalation analysis.
- **NFS:** the golden store is `0755 root:root`, exported read-write to an explicit client list (the
k3s nodes and the VM hosts) with `sec=sys,no_root_squash`. The client list stops other LAN devices
reaching the export, but a root user on an allowed client still has server-root write access.
`root_squash` is deferred: the existing PVC subdirectories are root-owned (observed
`drwxr-xr-x root root` and `drwxrwsr-x root 107`), so enabling it needs a one-time ownership
migration and a maintenance window. See docs/WORKAROUNDS.md.
- **Remote consoles:** the libvirt VNC consoles listen on `127.0.0.1` only, and the k3s firewall
drops 5900-5903 from pod sources. Reach a console through an SSH tunnel.
- **Pod-to-host firewall:** two independent layers deny pod access to the credential-bearing host
ports (SSH, NFS, Postgres, restic, VNC, Immich, etcd, exporter/monitoring). The
`deny-pod-to-node-admin-ports` `CiliumClusterwideNetworkPolicy` in
[k8s-gitops#52](https://github.com/ryan4yin/k8s-gitops/pull/52) denies pod egress to
`host`/`remote-node` on those ports at the pod's own node; the k3s nodes' nftables rule drops the
same ports for pod CIDR sources (`10.0.0.0/8`, `fd00::/104`). The nftables rule only matches when
the pod IP survives to the destination, which requires Cilium's eBPF datapath: kube-proxy's
iptables masquerade rewrote pod -> node traffic to the source node IP and defeated it. That is why
k3s runs `--disable-kube-proxy` ([#404](https://github.com/ryan4yin/nix-config/pull/404)) and
Cilium runs `kubeProxyReplacement` with `bpf.masquerade`. Verified live on all nodes: pod ->
node:22 is dropped while kubelet, ingress (80/443 stay open), ClusterIP and DNS keep working.
Only selected pod security fields, RBAC rules/binding metadata, listeners and export metadata were
queried. Secret contents and raw kubeconfigs were not inspected. No live access policy, workload,
export, credential, DNS record or firmware configuration was changed during the audit.
### Effective-access and credential follow-up — 2026-10-03
- Four authenticated `kubectl auth can-i` probes covered all 40 ServiceAccounts used by current
pods: reading secrets across namespaces, creating ClusterRoleBindings, escalating ClusterRoles,
and creating pods in the account's own namespace. Impersonation included the account's normal
service-account and authenticated groups; no secret contents or real pods/bindings were created.
- Flux Helm/Kustomize controllers and the VictoriaMetrics operator were allowed all four probes.
Kiali's operator could create ClusterRoleBindings, but the escalation probe was denied; create
permission alone does not prove unrestricted binding authority. Several controllers, including
Loki's account, can read secrets across namespaces and need a feature-specific scope review.
- Sampled staging application/default accounts were denied all four probes. This does not prove they
have no other permissions or cannot read particular secrets in their own namespace; unused
accounts, named-resource grants, token issuance and indirect escalation remain separate checks.
- Metadata checks on ai, reachable youko and the sampled master found owner-only decrypted files
(`0400`, `0500`, `0600` or `0000`), not group/world-readable files. ai's user-consumed
`/etc/agenix` copies have explicit owner-only modes. A symlink's `0777` is not the effective
target's access mode.
- User-readable credentials are intentional, including `nix-access-tokens` on servers as well as
desktops. Same-user agents are not isolated from these credentials by Unix file permissions. Mode
`0000` also does not stop privileged root. Other host/runtime ownership checks and recipient
membership were not completed; no private recipient repository or secret value was inspected.
- Darwin's `/etc/agenix` entries are agenix `path` symlinks, so agenix applies each secret's
declared owner and mode and the root-only `rclone.conf` stays root-owned. Runtime ownership
(`ls -lL /etc/agenix`) on each Darwin host still needs a check after the next activation.
- PostgreSQL's loopback TCP listener still trusts every role except `postgres` without a password,
so any local process can log in as `immich` or `playground`. No service configured here uses that
path: Immich and the exporter connect over the Unix socket with peer authentication. `postgres`
and replication now require `scram-sha-256`, closing the local-superuser path. Provisioning
per-role passwords for full loopback `scram-sha-256` is the follow-up; the disabled Grafana
`postgres-playground` datasource also waits on a `playground` password.
### Follow-up audit — 2026-10-22
- Baseline readback on live hosts: the ten unprivileged sysctl keys and the esp4/esp6/rxrpc modprobe
deny are in effect on all 13 reachable hosts; the #399/#400/#401 nft rules, NFS export scope and
pg_hba rules are live. `net.core.bpf_jit_harden` and ai's exact rule order need a root readback;
shoukei and akane were unreachable. k3s MicroVM guests have no system profile — compare
`/run/current-system` with `/run/booted-system` there.
- Exporter sockets are LAN-IPv4-bound on every scraped host except ai's NVIDIA exporter (`*:9835`),
a gap #373 missed; `hosts/idols-ai/hardware-nvidia.nix` now binds it and the `security-exporters`
eval covers every enabled exporter's `listenAddress`.
- Runtime drift on shoryu: the libvirt domains on 5902/5903 predate #400 and still bind wildcard;
only the nft VNC drop protects them until the guests are redefined and restarted.
- The pod-to-node deny lists omitted etcd's client port (2379, bound `192.168.5.114:2379`); both
layers now include it. 2381 (etcd metrics, scraped by in-cluster monitoring) stays reachable by
design.
- ServiceAccount effective access is now measured for all 41 live ns/SA pairs (on-node probes): the
Kiali operator gained a self-escalation path (`update`/`patch clusterroles` on a role bound to
itself, no `bind`/`escalate` verb needed); Kiali and kube-state-metrics deny `get secrets -A` but
allow `list`/`watch`, so probe sets must pair both verbs; 16 accounts enumerate secrets
cluster-wide and cert-manager can delete them. Narrowing proposals target k8s-gitops.
- k3s-test-1 pod networking has been down since the 2026-10-14 full guest cold-restart: Cilium
bootstraps through the VIP while the VIP requires kube-vip's lease over ClusterIP — a cycle the
kube-proxy-replacement switch created. The host nftables drops stay loaded, but the CLiCNP layer
enforces nothing while Cilium is down and the nft rule's pod-source-IP precondition is not met, so
the pod-to-node boundary is degraded until the cluster recovers.
- The cluster recovered manually (VIP attached on one master, then handed back to kube-vip), and the
cycle itself is fixed: kube-vip's pod now overrides the kubelet-injected
`KUBERNETES_SERVICE_HOST/PORT` to reach its own node's apiserver (port 6443) instead of the
ClusterIP, so the VIP no longer needs Cilium on a cold start. A full cold start of all six guests
on 2026-10-24 brought the VIP back with no manual action (6/6 nodes, Cilium 6/6, kube-vip 3/3, no
connection to the ClusterIP), and k8s-gitops#54 is merged and reconciled. A probe from a pod
confirms 2379 is denied while 2381 stays open for in-cluster metric scraping.
### Sandbox and agent boundaries
The installed agent CLIs run as the normal user, not inside a repository-defined OS sandbox.
AGENTS.md/tool permissions remain important execution policy, but they are not Unix process
isolation. Source review and socket metadata—not reads of private credentials—established:
- The current desktop session's SSH-agent socket is owned by and writable to the current UID.
Same-user subprocesses can potentially use its signing interface; read-only mounts do not prevent
socket use. Private-key bytes were not inspected and no signing operation was requested.
- `ssh -G` reports forwarding for explicit `192.168.*` destinations, but not the sampled `shoryu`
alias or GitHub destination. Do not treat forwarding as enabled for every LAN alias, or remove it
without checking deployment/build workflows.
- All nine desktop wrappers use shared nixpak policies: cleared environment, filtered session D-Bus
and selected mounts. The [permission inventory](../hardening/README.md#app-permissions) lists each
app's file, network, credential and media grants.
- Only Firefox/Chromium share host networking. Chromium is an intentional local-development
exception; all other apps isolate loopback with pasta but can still reach the LAN/Internet.
Browser media and browserpass are enabled only for Firefox/Chromium/Chrome. Brave Origin and Edge
are backup browsers with no media grants. browserpass exposes password-store/GPG data and agent
operations.
- Nixpak supplements Chromium's renderer sandbox, which remains enabled. Zoom/WeChat retain their
FHS runtimes inside the outer boundary; inner mounts cannot recover hidden host resources.
- Desktop access has limits: GPU drivers remain exposed, PulseAudio permits recording, read-only
sockets permit agent operations, and Zoom/WeChat's X11 connections have shared-display trust.
Zoom's 7.2.1 client receives only `/dev/video0` and `/dev/video1` because it captures the webcam
directly via V4L2; no other app gets raw camera nodes and no raw PipeWire access is added.
Chromium-family browsers use only the app-scoped Secret portal for v12 cookie encryption; no
browser receives broad GNOME Keyring authority. Firefox uses its own NSS storage. Firefox camera
and remote screen sharing are intentionally disabled: Firefox does not support the XDG
Camera/ScreenCast portal path used by this policy, and enabling those features would require
granting raw video-device or direct PipeWire access. We deliberately keep that privilege closed.
- **Rollout note (WA-045):** v12 is verified on a disposable profile, but if the portal is
unavailable Chromium may fall back to weaker v10 storage; portal initialization is monitored.
- Policy/build passes for all nine wrappers. Interactive validation passed for Firefox, Chrome,
Chromium, Brave Origin, Edge, QQ, Telegram and Zoom; the Chromium smoke test also validates
renderer sandboxing, environment clearing, bind scope and profile persistence. WeChat's
interactive Wayland check remains pending. See the
[runtime validation](../hardening/tests/runtime-validation.md).
#### Agent-first diagnostics by host role
- **Homelab:** configured root SSH is an acceptable path for autonomous, task-scoped read-only
diagnosis. Root access does not authorize deployment, restart, permission changes, destructive
operations or secret inspection merely because a diagnostic command is available.
- **Core desktops:** prefer normal-user service status, network information, metrics, own-process
inspection and journal access. Do not add passwordless sudo or remove existing human-admin
membership/workflows as an incidental security change. ai's normal user can query system-journal
metadata, including the kernel transport, without sudo; other desktops need their own readback.
Escalation to root, including local root SSH, needs explicit authorization rather than an
automatic fallback when a normal-user diagnostic is denied.
- A read-only API or log can still reveal credentials. Use bounded service/time filters and redact
sensitive output; never dump process environments or raw kubeconfigs. Narrow, reviewed per-service
log access is preferable when a desktop lacks access. Do not blanket-grant all journals or add a
generic privileged diagnostic-command dispatcher just for convenience.
- Preserve functioning SSH/browser/GPU integration. Stronger agent OS isolation, if needed, should
be an explicit workflow design with positive/negative tests, not a collection of surprise denials.
For routine diagnosis, prefer bounded reads such as `systemctl --failed --no-pager`,
`systemctl status SERVICE --no-pager`, `journalctl -u SERVICE -n 200 --no-pager`,
`journalctl --user -u SERVICE -n 200 --no-pager`, `ip -br address`, `ss -lntu` and
`ps -eo pid,comm,stat`. On Homelab these may use the configured root SSH connection; on desktops try
normal-user access first. Do not confuse read-only status with permission to restart a service, or
diagnose a log-access denial by reading credential files or removing protection globally.
### Update/reboot routine and kernel visibility
Recommended cadence: review advisories and pinned inputs weekly, with an out-of-band review for
active exploitation or a reachable high-impact vulnerability. Check affected versions and exposure,
then validate an update and schedule the required rollout/reboot. Do not auto-apply every input bump
or promise that a kernel version string alone proves security coverage.
Verify the booted kernel after each rollout instead of trusting the selected generation. The booted
image and the image selected for the next boot are independent:
- `readlink -f /run/booted-system/kernel` — kernel of the running system closure.
- `readlink -f /nix/var/nix/profiles/system/kernel` — kernel selected for the next boot.
A mismatch means the host still runs the old kernel and a reboot is pending. This catches boot-only
deployments even when `/run/current-system` is unchanged; `uname -r` gives the running release
string. A matching kernel path is not proof of vulnerability age, livepatch state or driver
compatibility, and a generation change that only affects the initrd, modules or userspace leaves the
kernel path unchanged. Guest profiles and a runner staged only on a MicroVM's physical host require
separate verification.
## Remaining prioritized TODOs
| Priority | Work | Acceptance / constraints |
| -------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P1 | Move root/deploy SSH to hardware-backed keys | `sk-ssh-ed25519` keys with touch for root, with SSH multiplexing to keep Colmena usable; an agent lifetime or confirmation; a tested break-glass path |
| P1 | Alert on SSH logins and ship auth/audit logs off-host | Off-host copy host root cannot rewrite; alert on an unknown key or source, a session-rate spike and `root-session`/`credential-access`; redact key material |
| P1 | Roll out and verify the implemented baseline/exporter rules | Runtime sysctl readback, allowed IPv4 scrape and denied IPv6/non-monitoring scrape tests; preserve Nix/Podman/Cilium/NFS functionality |
| P1 | Review router mappings and tailnet ACLs; narrow service exposure where warranted | Audit was limited to sampled host listeners; test intended and denied access without locking out deployment |
| P1 | Review NFS client scope and root identity requirements | Restrict to required VM hosts/CSI nodes where practical; test provisioning, permissions and existing PVCs before changing squash semantics |
| P1 | Narrow operator/secret access where justified; finish permission checks | Selected effective probes and ai/youko modes audited; validate broad secret-reader needs, remaining hosts and recipient scope; Darwin per-file ownership is now explicit and needs a runtime check |
| P1 | Adopt the documented cadence and verify the booted kernel after rollouts | Routine documented; compare booted and selected kernel paths and recheck API/workloads after reboot; no automated metric is deployed |
| P1 | Exercise backup restore and credential rotation | Use an approved isolated restore target and recovery access; never overwrite live data for a drill |
| P2 | Promote selected AppArmor profiles to enforce; harden exposed systemd services | Per-app positive/negative tests, store-path coverage, reviewed capabilities and reversible rollout |
| P2 | Maintain ai boot integrity and assess other host roles | Review sbctl configuration migration, recovery boot and signed custom/NVIDIA modules; ai Secure Boot state already verified |
| P2 | Review unused-module deny list against real workloads and verified advisories | Check loaded/built-in modules and autoload dependencies; do not block required filesystems, WiFi, VM or Cilium/NFS functionality |
| P2 | Evaluate stronger controls only against a concrete threat | Account for hibernation, crash recovery, applications and performance; no blanket io_uring/SMT/userns ban or hardened-kernel switch |
| P2 | Validate narrower sandbox/agent boundaries where needed | Source and selected SSH/journal metadata audited; use disposable data/socket probes and preserve core-desktop diagnosis, browserpass and Homelab root workflows |
| P3 | Improve security-event alerts and configuration drift checks | Actionable redacted alerts and periodic runtime control readback |
Record temporary exceptions in [WORKAROUNDS.md](./WORKAROUNDS.md) with a removal condition. Revisit
this architecture after a new exposed service, network/storage change, host addition or significant
kernel/nixpkgs update. No control here guarantees containment after host root compromise.
## References
- [Linux kernel sysctls](https://docs.kernel.org/admin-guide/sysctl/kernel.html)
- [Filesystem protections](https://docs.kernel.org/admin-guide/sysctl/fs.html)
- [BPF JIT hardening](https://docs.kernel.org/admin-guide/sysctl/net.html#bpf-jit-harden)
- [userfaultfd restrictions](https://docs.kernel.org/admin-guide/sysctl/vm.html#unprivileged-userfaultfd)
- [Yama](https://docs.kernel.org/admin-guide/LSM/Yama.html)
- [Kernel module signing](https://docs.kernel.org/admin-guide/module-signing.html)
- [NixOS security advisories](https://github.com/NixOS/nixpkgs/security/advisories)
- [CISA Known Exploited Vulnerabilities](https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
+92
View File
@@ -0,0 +1,92 @@
# Workarounds & known gaps
Temporary workarounds, version pins, carried patches, and known gaps are collected here, grouped by
type, each with the condition under which it can be removed. Review the list periodically so
obsolete workarounds get rolled back instead of lingering.
## How to use
- **Adding** — a change that introduces a workaround, pin, carried patch, or known gap adds a row
under the matching section below in the same PR, with a concrete **removal condition**.
- **Removing** — once the removal condition is met, delete the change and set `Status` to `removed`
(mention the ID in the commit), or delete the row.
- **Reviewing** — re-evaluate rows whose `Revisit` trigger is due and refresh `Status`.
## Pins
A version or revision pinned for a reason that can go away.
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------- | -------------------------- | ------ |
| WA-004 | `catppuccin` on `main` — `flake.nix` | `main` carries the nodejs→nodejs-slim deprecation fix, which is not in a release | Switch to a release tag once it includes the fix | 2026-09-25 | on catppuccin release | active |
| WA-006 | `nixpkgs-blender` — `flake.nix` | Blender 5.2 LTS + CUDA/OptiX pin; keeps `just up` from triggering a full source rebuild | Bump deliberately when needed; drop the pin if the current nixpkgs build is acceptable | 2026-07-24 | deliberate | active |
| WA-020 | `fcitx5-vinput` pinned to release tag `v2.4.2` — `flake.nix`; consumer `home/linux/gui/base/vinput/default.nix` | Tracks a release tag instead of a branch, so `just up` stays on that release | Bump to a newer release tag when its changes are wanted | 2026-10-04 | on vinput release | active |
| WA-021 | `nix-x86-on-aarch64` input — our fork of rowanG077's patched FEX + muvm runtime — `flake.nix`; `hosts/12kingdoms-shoukei/game/` | FEX does not support 16K pages, so x86/x86_64 apps and games on shoukei need this pinned flake's 4K-page microVM runtime; our fork lets upstream changes be reviewed before use | Drop once FEX supports 16K pages directly, or nixpkgs/nixos-apple-silicon ships an equivalent runtime | 2026-10-07 | on FEX/nixpkgs/muvm update | active |
| WA-023 | `nix.package = pkgs.nixVersions.latest` — `modules/nixos/base/nix.nix` | NixOS is opted into Nix 2.35 (2.35.3), which nixpkgs ships as `nixVersions.latest`, while `nixVersions.stable` is still 2.34 (2.34.9) | Drop the override and fall back to the default `pkgs.nix` once `nixVersions.stable` is 2.35 or newer | 2026-10-07 | eval warning | active |
| WA-037 | `nixpkgs-npu` input pinned to `2ddccb854d89` — the `openvino-npu-compiler` branch of `ryan4yin/nixpkgs`, carrying [NixOS/nixpkgs#570230](https://github.com/NixOS/nixpkgs/pull/570230) — `flake.nix`; consumer `pkgs-npu` in `outputs/default.nix`, `hosts/idols-aquamarine/asr.nix` | The Intel NPU compiler, and the driver change that loads it, are unmerged, so the NPU driver the Whisper service needs comes from that branch | Drop the input and load `intel-npu-driver` from `nixos-unstable` once #570230 merges | 2026-10-17 | when #570230 merges | active |
## Workarounds
A setting that works around a bug or a hardware quirk.
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------- | ------ |
| WA-009 | USB-SATA bridge mitigations (`usbcore.autosuspend=-1`, `usb-storage.delay_use=10`) — `hosts/12kingdoms-youko/default.nix` | flaky JMicron JMS567 resets with UDMA-CRC and I/O errors | Drop once the bridge/controller is replaced; the signal is smartctl CRC/media errors | 2026-09-21 | on hardware change or CRC errors | active |
| WA-010 | Insta360 Link reset-resume quirk (`usbcore.quirks=2e1a:4c01:b`) — `hosts/idols-ai/default.nix` | the camera stays enumerated with a stalled UVC endpoint after resume | Drop after the Insta360 firmware or the kernel `uvcvideo` fixes it | 2026-08-01 | on kernel or firmware change | active |
| WA-011 | Kernel module blacklist (`esp4`, `esp6`, `rxrpc`) — `hardening/kernel-hardening.nix` | attack surface for the Dirty Frag LPE (CVE-2026-43284 / CVE-2026-43500) | Kept as defence in depth; drop only if something needs those modules | 2026-10-03 | rarely | active |
| WA-018 | user-owned `~/.ssh/config` + disabled systemd ssh-proxy include — `home/base/tui/ssh.nix`, `modules/nixos/base/ssh.nix` | bwrap exposes root-owned store files as `nobody` | Drop once sandboxes preserve store ownership | 2026-10-06 | when sandbox layout changes | active |
| WA-019 | programs.steam.extraCompatPackages = [ pkgs.dwproton-bin pkgs.proton-ge-bin ] — modules/nixos/desktop/gaming.nix | ACE games fail on GE/mainline Proton, work with DW-Proton | Drop once mainline or GE-Proton runs these games (GE-Proton#433) | 2026-10-07 | on Proton/GE bump | active |
| WA-024 | Wi-Fi power saving disabled (`networking.networkmanager.wifi.powersave = false`) — `hosts/12kingdoms-shoukei/apple-silicon.nix` | On this BCM4378/brcmfmac laptop, powersave is a common source of intermittent ASSOC-REJECT / authentication-timeout failures; NetworkManager then mistakes the failure for a bad key, re-prompts, and the client (Noctalia, nmtui, nmcli) saves the answer as a duplicate `<SSID> 1` profile | Drop once the kernel carries the brcmfmac `port_authorized` fix (WA-025) and no association failures remain | 2026-10-07 | watch `journalctl -u wpa_supplicant` for ASSOC-REJECT / authentication timeout | active |
| WA-026 | guest deploys run `installOnHost` then `systemctl restart microvm@<guest>` on the physical host — `Justfile`, `lib/genMicrovmGuestModule.nix` | this is the fallback that microvm.nix's own `microvm.deploy.rebuild` uses when it cannot switch in place (`doc/src/ssh-deploy.md`), so only the in-place switch is lost: its sshSwitch step runs `nix-store --load-db` against the guest's read-only virtiofs `/nix/store` and dies on `cannot remount "/nix/store" writable` (nix 2.35) | Switch guests in place with sshSwitch once microvm.nix or nix handles a read-only store. Until then, note that a host activation re-links `/var/lib/microvms/<guest>/current` to the runner baked into the host's system, so switch the host after installing a guest from a newer tree | 2026-10-09 | on microvm.nix or nix bump | active |
| WA-028 | GitHub SSH pinned to IPv4 (`AddressFamily = "inet"`) — `home/base/tui/ssh.nix` | a v6 fake AAAA for `ssh.github.com` from the `2001:2::/64` pool (WA-029) is not proven on the 443 SSH tunnel yet | Drop once the v6 fake-ip pool is proven for GitHub SSH | 2026-10-10 | on a mihomo bump or a v6 pool change | active |
| WA-029 | mihomo `fake-ip-range6` is `2001:2::/64`, not the shipped ULA default — `modules/nixos/desktop/networking/mihomo/generate.nu` | the default `fdfe:dcba:9876::/64` sits in `fc00::/7`, which Local Network Access calls local, so a public page loaded over the v4 fake pool and pulling a dual-stack subresource prompts for local-network access (Chrome 154, Firefox). The same range also stalled ssh: a session to a fake AAAA authenticated and then never closed, on both `gvisor` and `mips`. `2001:2::/48` is RFC 5180 benchmarking -- the v6 twin of the `198.18.0.0/15` v4 default, absent from that table -- and both symptoms went with the move | Go back to the upstream default once mihomo ships a pool outside the LNA non-public table; the ULA default brings the ssh stall back | 2026-10-09 | on a browser or mihomo bump -- that table already grew to cover `2001:db8::/32` and `198.18.0.0/15` | active |
| WA-030 | mihomo unit sets `SKIP_SYSTEM_IPV6_CHECK=1` — `modules/nixos/desktop/networking/mihomo/default.nix` | mihomo checks at startup whether another NIC already holds an IPv6 address and, finding none, skips the TUN's v6 half -- but `strict-route` still installs its v6 blackhole (`ip -6 rule add priority 9000 unreachable`). On a cold boot that rule lands before networkd's Router Solicitation is answered, so the host can never send RS/NS, never gets a GUA, and mihomo never re-checks: self-locking until the core restarts. The env var forces the TUN v6 address up, which installs mihomo's normal v6 rules whose `lookup main suppress_prefixlength 0` keeps on-link neighbor discovery and the RA default route off the TUN | Drop once mihomo either re-runs the IPv6 check or skips the v6 strict-route blackhole when the v6 TUN is disabled | 2026-10-10 | on a mihomo bump | active |
| WA-031 | `iommu=pt` kernel parameter — `hosts/idols-aquamarine/hardware-amd-gpu.nix` | ROCm's system requirements warn that multi-GPU systems may need IOMMU passthrough to prevent application hangs, and this host has two Radeon AI PRO R9700 cards. Added before the first inference run; not yet observed on gfx1201. | Drop once a ROCm release states that the default IOMMU mode is safe for multi-GPU gfx1201, or when the host runs a single GPU | 2026-10-13 | on a ROCm bump, or on the first hang report | active |
| WA-032 | `amdgpu.runpm=0` kernel parameter — `hosts/idols-aquamarine/hardware-amd-gpu.nix` | this board's DSDT advertises ATPX power control (`\_SB.PC00.GFX0.ATPX`) but never defines the `PEG0.PEGP._OFF`/`_ON` symbols its body calls, so the first idle resume of the headless R9700 leaves it in `runtime_status = error` (`resume of IP block <smu> failed -62`) until reboot, and amdgpu discards the ACPI error. Root cause, kernel-source trail and the measured field comparison: `hosts/idols-aquamarine/ROCM.md`. | Drop once a BIOS update defines those ACPI methods (upstream releases 1023/1066 exist for this board; a flash changes PCR7, so re-enroll TPM2), or a kernel release stops trusting ATPX when its power methods are missing | 2026-10-14 | on a BIOS update or kernel bump | active |
| WA-036 | catppuccin eval-time ports kept in the home closure (`home.extraDependencies`) — `home/base/core/theme.nix` | catppuccin/nix reads some ports at eval time (`lib.importTOML "${sources.starship}/<flavor>.toml"`) and inlines the result, so the built port, catppuccin's build/install hooks and the fetched port source stay unreachable: every gc deletes them, and the deploy after it rebuilds them (build logs: 2026-10-16). `home.extraDependencies` records the port in the generation, which is a gc root. Also read at eval time upstream: `bottom`, `element` (option `element-desktop`), `firefox`, `rio`, `palette` (options `skim`, `tty`), `spotify-player`. | Drop once catppuccin/nix stops reading port files at eval time, or those ports come from a binary cache | 2026-10-16 | on a catppuccin bump | active |
| WA-039 | niri's NVIDIA free-buffer pool bounded (`GLVidHeapReuseRatio` 0) — `hosts/idols-ai/hardware-nvidia.nix` | the driver does not return freed vidmem buffers to its free pool, so niri's VRAM grows to ~1.1 GiB instead of the ~100 MiB its NVIDIA notes expect (measured 1137 MiB after 24 h uptime; [NVIDIA/egl-wayland#126](https://github.com/NVIDIA/egl-wayland/issues/126)) | Drop once a driver stops growing niri's VRAM without the profile, or niri ships its own default | 2026-10-19 | on an nvidia driver bump | active |
| WA-041 | Monitor OSD "no-signal auto power-off" disabled — no code in this repo (the P32A2V OSD setting) | during a DPMS-off idle the panel drops and re-adds HPD every ~13 s, and after wake niri keeps scanning out while the panel stays dark until the monitor is power cycled; the panel's own auto power-off drives that state, so it is now disabled. Pinning the mode to 120 Hz did not change the symptom | Drop once a driver retrains the DP link after a DPMS wake, the monitor firmware stops cycling HPD, or this monitor is replaced | 2026-10-21 | confirm on the next full idle cycle, then on an nvidia or monitor firmware update | active |
| WA-042 | Colmena nodes set `system.nixos.versionSuffix` and `revision` from the flake's lib — `outputs/default.nix` (`rawColmenaHive`) | Colmena evaluates each node with nixpkgs' `eval-config.nix` and plain lib, which reports `26.11pre-git`, so without the override a Colmena deploy differs from the `nixosConfigurations` toplevel; the `colmena-parity` eval test guards the match | Drop once Colmena evaluates nodes with the flake's lib or accepts `nixosConfigurations` directly, keeping `colmena-parity` green | 2026-10-21 | on a colmena bump | active |
| WA-046 | xdg-desktop-portal-wlr pinned to 0.8.2 — `modules/nixos/desktop/xdg.nix` | niri's GNOME ScreenCast path exposes only DMA-BUF with modifiers, which Zoom 7.2.1 rejects; xdg-desktop-portal-wlr 0.8.2 provides the working SHM path, while 0.8.4 regresses Zoom's SelectSources. | Drop once niri's ScreenCast path works with Zoom or xdg-desktop-portal-wlr 0.8.4+ is fixed. | 2026-10-31 | eval assertion, or on a Zoom update | active |
| WA-048 | Nixpak GUI fontconfig comes from the host — `hardening/nixpaks/modules/desktop-base.nix` | Nixpak normally generates a standalone `/etc/fonts` and font package list for each sandbox; that bypasses the host `defaultFonts` and made browser chrome, Telegram, QQ and other sandboxed apps choose a different CJK fallback from the host. The wrapper now disables Nixpak fonts and bind-mounts `/etc/static/fonts` at `/etc/fonts`, so sandboxed GUI apps reuse the host font packages and family preferences through the already-mounted `/nix/store`. | Drop once Nixpak can directly reuse the host fontconfig or no longer overrides `/etc/fonts`, then remove the bind and restore the library-supported configuration path | 2026-11-01 | on a nixpak update | active |
If the KINGBANK KP260 NVMe freezes on `ai` recur (formerly WA-008, now back at the kernel's APST
defaults), restore the parameters commented out in `hosts/idols-ai/hardware-configuration.nix` —
`nvme_core.default_ps_max_latency_us=0 nvme_core.io_timeout=4294967295 pcie_aspm=off`.
If Wi-Fi wedges after a failed association, restart the stack instead of rebooting:
`sudo systemctl restart wpa_supplicant`, then `sudo systemctl restart NetworkManager`. Deleting an
_active_ NetworkManager profile wedges the supplicant as well; switch to another profile first, and
only delete inactive duplicates.
## Known issues
A known limitation with no fix yet.
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------------------- | ------ |
| WA-016 | subscription rule snapshots — `modules/nixos/desktop/networking/mihomo/` (`rules_from:`) | mihomo's `proxy-providers` imports proxies only, so the `rules` and `proxy-groups` a subscription ships with are dropped by the core; the generator re-imports them from a local YAML copy that has to be refreshed by hand when the provider changes its routing policy | Drop the snapshots once mihomo can import rules from a proxy-provider, or the providers publish their rules as rule-providers | 2026-10-04 | when mihomo gains rule import | active |
| WA-022 | qemu-user binfmt removed — `hosts/12kingdoms-shoukei/hardware-configuration.nix` | the x86-on-arm module owns the x86 ELF handlers and this host does not need riscv64, but dropping `boot.binfmt.emulatedSystems` also drops `nix.settings.extra-platforms` and qemu-user, so `podman run --platform=linux/amd64`, x86 chroots and local builds of uncached x86 derivations need a remote x86_64 builder | Restore `boot.binfmt.emulatedSystems` (without x86_64/i686, which the module asserts against) if such a workload is needed | 2026-10-07 | when an amd64 container/chroot or uncached x86 build is needed | active |
| WA-025 | brcmfmac 4-way-handshake offload authentication — upstream kernel/wpa_supplicant, no code in this repo | Since hostap commit 416386060, wpa_supplicant 2.11+ expects `NL80211_CMD_PORT_AUTHORIZED` from drivers that offload the 4-way handshake; brcmfmac never reports it, so authentication intermittently fails on Apple Silicon (BRCM4378/4387/4388). nixpkgs already carries the Broadcom revert in wpa_supplicant 2.12; the proper fix, Janne Grunau's `wifi: brcmfmac: cfg80211: Report port_authorized for 4-way HS offload` (2026-09), is still under review. Entering the key again after such a failure only creates a duplicate profile, so cancel and retry instead | Fix lands via a nixpkgs kernel update (or by carrying the patch through `nixpkgs-patched`) | 2026-10-07 | on every kernel bump | active |
| WA-033 | USB-C/XHCI port `_PLD` ACPI noise — this board's DSDT, no code in this repo | every boot logs 16 `ACPI Error: Aborting method \_SB.PC00.TXHC.RHUB.SS01._PLD ... (AE_NOT_FOUND)` lines, plus `\_SB.PC02.XHCI.RHUB.HS01._PLD`; the ports work, and these messages are unrelated to the GPU's ATPX failure, which names `\_SB.PC00.GFX0.ATPX` instead | Drop if a BIOS update defines the `_PLD` descriptors | 2026-10-14 | on a BIOS update | active |
| WA-035 | SSH retry + multiplexing to LAN peers, ARP fallback tuning — `home/base/tui/ssh.nix`, `hosts/idols-ai/neighbour-probe.nix` | the desktop reaches the LAN over a Mesh WiFi backhaul, which drops unicast to a peer whose MAC it has aged out of its forwarding table while still flooding broadcast: a capture shows seven SYNs from one socket addressed to the cached MAC with no answer, three unicast ARP probes unanswered, then a broadcast ARP answered in 0.4 ms after ~12 s of failed connects. The ssh settings retry a short connect and keep a master warm so the entry stays fresh; the neighbour-probe tunings walk the kernel to that broadcast fallback in ~1 s | Drop once the backhaul is wired or the mesh stops dropping unknown unicast | 2026-10-16 | when the backhaul changes | active |
## WIP
An unfinished feature or gap.
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------- | ------------------------ | ---------- |
| WA-013 | NixOS tests — `outputs/README.md` | full-host tests still rebuild custom packages (no private cache) and need the host ssh key to decrypt agenix; the shared-firewall security-exporters VM test now runs in CI (KVM); `outputs/x86_64-linux/nixos-tests/idols-ruby.nix` is exposed as `packages.x86_64-linux.ruby-nixos-tests` but is not in `checks` or CI | Resolve when a private cache exists and tests can run per-service | 2024-03-13 | quarterly | active |
| WA-027 | NFS golden store `no_root_squash` — `hosts/12kingdoms-youko/nfs-golden.nix` | the existing PVC subdirectories are root-owned, so enabling `root_squash` would break running workloads until they are migrated; the CSI node plugin is hostNetwork and sources from the allowlisted node IPs | Drop when the PVC subdirectories are migrated and `root_squash` is enabled | 2026-10-07 | next maintenance window | active |
| WA-044 | transmission AppArmor lock permission — `hosts/12kingdoms-youko/homelab-services/transmission.nix` | nixpkgs' transmission 4.1.4 profile grants `rw` on the daemon's config dir but not `k`, while 4.1.4 takes an fcntl lock on `~/.config/transmission-daemon/lock`; the daemon exits with "Another process is already using". A `local/bin.transmission-daemon` include adds `rwk` for that lock file only | Drop once the nixpkgs profile includes the lock permission | 2026-10-31 | on the next nixpkgs bump | active |
| WA-045 | Chromium cookie crypto — `hardening/nixpaks/modules/chromium-browser.nix` | Chromium-family browsers use the app-scoped Secret portal and write v12 cookies. If the portal is unavailable, Chromium may fall back to weak v10 storage. | Drop once the v10 fallback is eliminated; monitor portal initialization and the `Local State.os_crypt.portal.prev_init_success` flag | 2026-10-31 | on Chromium updates | active gap |
## Related
- Security architecture, threat model and audit status: [SECURITY.md](./SECURITY.md).
- Unmerged **nixpkgs** PRs are carried via the `nixpkgs-patched` input and the
[`.agents/skills/nixpkgs-patched`](../.agents/skills/nixpkgs-patched/SKILL.md) workflow (currently
none).
- Routine version pins (colmena, agenix, disko, lanzaboote, haumea, home-manager, `nixpkgs-stable`,
`nixpkgs-darwin`, …) are the versions in use, not workarounds, so they are not listed here.
+8 -8
View File
@@ -39,7 +39,7 @@ fallback, and a root-readable Cloudflare token in agenix.
**Pros:** certificates become automatic and every tailnet device trusts them (today a phone without
`ecc-ca` gets a certificate error); removes the Cloudflare token and the DNS-01 propagation
workaround from those services; access control moves from "IP trust zone" to tailnet identity/ACL,
which is exactly the limitation `SECURITY.md` records; `--tcp=<port>` can also expose raw TCP.
which is exactly the limitation `docs/SECURITY.md` records; `--tcp=<port>` can also expose raw TCP.
**Cons:** Serve routes by mount path on one name, or (via the nixpkgs module) as Tailscale Services
with `tcp:<port>` endpoints only — there is no Host/SNI vhost equivalent, so path-sensitive apps
@@ -285,10 +285,10 @@ flake input is maintained separately for Secure Boot.
the lanzaboote flake input; checksum validation of kernel and initrd; nicer menu.
**Cons:** different trust model — it signs **limine itself** and validates artifacts by checksum,
rather than shipping a firmware-verified UKI, so `SECURITY.md` and the threat model must be updated;
key enrollment is a manual firmware dance (Setup Mode + `sbctl enroll-keys`) and is the one change
in this repository that can leave a machine unbootable; the existing lzpt bundle may or may not be
reusable; less NixOS mileage than systemd-boot.
rather than shipping a firmware-verified UKI, so `docs/SECURITY.md` and the threat model must be
updated; key enrollment is a manual firmware dance (Setup Mode + `sbctl enroll-keys`) and is the one
change in this repository that can leave a machine unbootable; the existing lzpt bundle may or may
not be reusable; less NixOS mileage than systemd-boot.
**Reversibility:** low on the physical desktop (firmware + ESP state), high on a VM.
@@ -466,7 +466,7 @@ prevent boot or login. **Recommendation for the group: evaluate in a disposable
| D4 | `system.nixos-init.enable` | `activationScripts` | bashless init | makes `activationScripts` a silent no-op — youko's `data-ro-backing`, `installCaddyWallpapers` and `installHomepageDashboardConfig` would stop running without an error | lab only |
| D5 | `security.pam.services.<name>.rules` | per-service booleans | freeform PAM config | upstream marks it subject to breaking changes; a mistake locks you out | lab only |
| D6 | `security.shadow.enable = false` + greetd | shadow suite provides login/su | fewer setuid programs | assertion couples the two; changes VT login semantics | lab only |
| D7 | `security.apparmor.policies` | `policies.sudo` already in use (`hardening/apparmor/default.nix`); the old `profiles` option is removed upstream | policy set is now data | writing profiles is real work and breakage-prone; `SECURITY.md` explicitly warns against untested profiles | lab only |
| D7 | `security.apparmor.policies` | `policies.sudo` already in use (`hardening/apparmor/default.nix`); the old `profiles` option is removed upstream | policy set is now data | writing profiles is real work and breakage-prone; `docs/SECURITY.md` explicitly warns against untested profiles | lab only |
---
@@ -520,7 +520,7 @@ header) should then be trimmed to **adopt** items only.
`services/system/{userborn,nix-daemon,zram-generator}.nix`,
`services/monitoring/prometheus/exporters.nix`.
- NixOS 26.05 release notes; Nix manual, Experimental Features.
- Repo context: `SECURITY.md` (trust zones and control limits), `WORKAROUNDS.md` (WA-001, WA-008,
WA-009, WA-023), `modules/nixos/base/networking/mdns.nix`,
- Repo context: `docs/SECURITY.md` (trust zones and control limits), `docs/WORKAROUNDS.md` (WA-001,
WA-008, WA-009, WA-023), `modules/nixos/base/networking/mdns.nix`,
`hosts/12kingdoms-youko/homelab-services/caddy.nix`,
`hosts/12kingdoms-youko/homelab-services/README.md`.