Files
nix-config-ryan4yin/SECURITY.md
T

39 KiB

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 for sandbox wrappers and Linux hardening notes, and 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 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
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, k3s deployment, firewall, SSH, Nix trust, secrets, and backup. Cluster resources are maintained in the separate k8s-gitops repository.

The Macs set nix.enable = false (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, 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. 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. Review capabilities, writable paths, network access and syscall requirements before tightening an individual service.

See application hardening for wrapper details.

Audit trail

modules.security.audit (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:

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:

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 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 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) 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 activation now re-applies each secret's declared ownership explicitly instead of a blanket chown over /etc/agenix/*, so the root-only rclone.conf stays root-owned. Runtime ownership (ls -l /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 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.

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 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