diff --git a/SECURITY.md b/SECURITY.md index 9dff73b0..ceaa9af7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -33,18 +33,18 @@ small, reversible controls and narrow exceptions; benchmark before adopting expe ## Architecture and trust boundaries -| Boundary | Current design | Limitation | -| --------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| Administration | Key-based SSH; root key login retained for Colmena/MicroVM deployment | Deployment credentials are root-equivalent; password login being disabled does not restrict stolen keys | -| 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 | Existing firewall rules restrict exporter ports 9100, 9835 and 9633 to the monitoring host over IPv4 | IPv6 denial is planned in the baseline implementation; 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; 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 and documented backup procedures | A rollback does not undo compromised data or secrets; backup configuration alone does not prove restore works | +| Boundary | Current design | Limitation | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Administration | Key-based SSH; root key login retained for Colmena/MicroVM deployment | Deployment credentials are root-equivalent; password login being disabled does not restrict stolen keys | +| 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; IPv6 denial is planned in the baseline implementation; 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; 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 and documented backup procedures | A rollback does not undo compromised data or secrets; backup configuration alone does not prove restore works | 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), diff --git a/modules/nixos/base/monitoring.nix b/modules/nixos/base/monitoring.nix index 692c36cd..0b402668 100644 --- a/modules/nixos/base/monitoring.nix +++ b/modules/nixos/base/monitoring.nix @@ -4,12 +4,19 @@ myvars, ... }: +let + # Exporters bind to this host's static LAN IPv4 (the same address + # VictoriaMetrics scrapes), so the socket exists only on the LAN interface — + # defence in depth behind the firewall allowlist. Hosts without a static LAN + # address (laptops like shoukei, DHCP VMs like akane) run no exporters. + lanAddr = myvars.networking.hostsAddr.${config.networking.hostName}.ipv4 or null; +in { # enable the node exporter on all nixos hosts # https://github.com/NixOS/nixpkgs/blob/nixos-26.05/nixos/modules/services/monitoring/prometheus/exporters/node.nix - services.prometheus.exporters.node = { + services.prometheus.exporters.node = lib.mkIf (lanAddr != null) { enable = true; - listenAddress = "0.0.0.0"; + listenAddress = lanAddr; port = 9100; # There're already a lot of collectors enabled by default # https://github.com/prometheus/node_exporter?tab=readme-ov-file#enabled-by-default @@ -45,26 +52,31 @@ # node_nvme_info and namespace capacity, NOT SMART health, so this is the only # source of SSD lifespan data. `hardwareTools` is off on MicroVM/QEMU guests, # which have no physical disks to inspect. - services.prometheus.exporters.smartctl = lib.mkIf config.modules.hardwareTools.enable { - enable = true; - # Bind to this host's LAN address rather than 0.0.0.0; the firewall also - # drops 9633 from anything but the monitoring host. Fall back to loopback - # for hosts without a static LAN address (e.g. shoukei). - listenAddress = myvars.networking.hostsAddr.${config.networking.hostName}.ipv4 or "127.0.0.1"; - port = 9633; - maxInterval = "60s"; - }; + services.prometheus.exporters.smartctl = + lib.mkIf (lanAddr != null && config.modules.hardwareTools.enable) + { + enable = true; + listenAddress = lanAddr; + port = 9633; + maxInterval = "60s"; + }; - # The exporter binds to the LAN address, which networkd assigns during boot; - # without ordering it races the address and trips the start limit (seen on ai). - # wait-online is best-effort (120s timeout), so also retry patiently to - # self-heal when the link comes up late. - systemd.services.prometheus-smartctl-exporter = lib.mkIf config.modules.hardwareTools.enable { - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - serviceConfig = { - RestartSec = "10s"; - StartLimitIntervalSec = 0; - }; - }; + # LAN-bound exporters race networkd's address assignment at boot; without + # ordering they trip the start limit (seen on ai). wait-online is best-effort + # (120s timeout), so also retry patiently. + systemd.services = lib.mkIf (lanAddr != null) ( + lib.genAttrs + ( + [ "prometheus-node-exporter" ] + ++ lib.optionals config.modules.hardwareTools.enable [ "prometheus-smartctl-exporter" ] + ) + (_: { + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + RestartSec = "10s"; + StartLimitIntervalSec = 0; + }; + }) + ); }