diff --git a/SECURITY.md b/SECURITY.md index ceaa9af7..e63fabac 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -198,6 +198,11 @@ checks remain necessary after deployment. `sec=sys,no_root_squash`. File mode 0755 does not contain an allowed client with root privileges; such a client can act as server root within this export. The network allowlist and client trust are therefore material boundaries, not merely filesystem permissions. +- **Pod-to-host firewall:** pod CIDR sources (`10.0.0.0/8`, `fd00::/104`) are accepted on the k3s + nodes, but the credential-bearing host ports are denied first (SSH, NFS, Postgres, restic, VNC, + Immich, and the exporter/monitoring ports). The broad accept stays so Cilium and in-cluster + service traffic keep working; a full port allowlist is future work. Ports 80/443 are deliberately + not denied, so in-cluster calls through the ingress are not broken. 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, diff --git a/lib/genK3sAgentModule.nix b/lib/genK3sAgentModule.nix index af225501..c6729a57 100644 --- a/lib/genK3sAgentModule.nix +++ b/lib/genK3sAgentModule.nix @@ -25,10 +25,16 @@ in enableIPv6 = true; }; - # Cilium pods reach host services (e.g. the API server) from the pod CIDR. - # `mkAfter` keeps this after the shared base rules (incl. the node_exporter - # drop), so pods cannot scrape node_exporter either. + # Pods may reach the host services they need, but they are not part of the + # host's trusted LAN. Deny the credential-bearing host ports before the broad + # pod-network accept below, so a compromised pod cannot reach NFS, the + # database, the backup server, the VNC consoles, or an exporter. `mkAfter` + # keeps this after the shared base rules (incl. the node_exporter drop). + # Ports 80/443 stay reachable so in-cluster calls through the ingress are not + # broken; the ingress is the authentication boundary there. networking.firewall.extraInputRules = pkgs.lib.mkAfter '' + ip saddr 10.0.0.0/8 tcp dport { 22, 2049, 2283, 5432, 5900, 5901, 5902, 5903, 8000, 8081, 8082, 9090, 9093, 9100, 9633, 9835 } drop + ip6 saddr fd00::/104 tcp dport { 22, 2049, 2283, 5432, 5900, 5901, 5902, 5903, 8000, 8081, 8082, 9090, 9093, 9100, 9633, 9835 } drop ip saddr 10.0.0.0/8 accept ip6 saddr fd00::/104 accept ''; diff --git a/lib/genK3sServerModule.nix b/lib/genK3sServerModule.nix index 3d1856e9..46f8539f 100644 --- a/lib/genK3sServerModule.nix +++ b/lib/genK3sServerModule.nix @@ -52,10 +52,16 @@ in enableIPv6 = true; }; - # Cilium pods reach host services (e.g. the API server) from the pod CIDR. - # `mkAfter` keeps this after the shared base rules (incl. the node_exporter - # drop), so pods cannot scrape node_exporter either. + # Pods may reach the host services they need, but they are not part of the + # host's trusted LAN. Deny the credential-bearing host ports before the broad + # pod-network accept below, so a compromised pod cannot reach NFS, the + # database, the backup server, the VNC consoles, or an exporter. `mkAfter` + # keeps this after the shared base rules (incl. the node_exporter drop). + # Ports 80/443 stay reachable so in-cluster calls through the ingress are not + # broken; the ingress is the authentication boundary there. networking.firewall.extraInputRules = lib.mkAfter '' + ip saddr 10.0.0.0/8 tcp dport { 22, 2049, 2283, 5432, 5900, 5901, 5902, 5903, 8000, 8081, 8082, 9090, 9093, 9100, 9633, 9835 } drop + ip6 saddr fd00::/104 tcp dport { 22, 2049, 2283, 5432, 5900, 5901, 5902, 5903, 8000, 8081, 8082, 9090, 9093, 9100, 9633, 9835 } drop ip saddr 10.0.0.0/8 accept ip6 saddr fd00::/104 accept ''; diff --git a/outputs/x86_64-linux/tests/security-k3s-pod-firewall/expected.nix b/outputs/x86_64-linux/tests/security-k3s-pod-firewall/expected.nix new file mode 100644 index 00000000..81b3fbb5 --- /dev/null +++ b/outputs/x86_64-linux/tests/security-k3s-pod-firewall/expected.nix @@ -0,0 +1,5 @@ +{ + lib, + outputs, +}: +lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) (_: true) diff --git a/outputs/x86_64-linux/tests/security-k3s-pod-firewall/expr.nix b/outputs/x86_64-linux/tests/security-k3s-pod-firewall/expr.nix new file mode 100644 index 00000000..df2ab437 --- /dev/null +++ b/outputs/x86_64-linux/tests/security-k3s-pod-firewall/expr.nix @@ -0,0 +1,33 @@ +{ + lib, + outputs, +}: +let + # Index of the first occurrence of `needle` in `haystack`, or null. + firstIndex = + needle: haystack: + let + parts = lib.splitString needle haystack; + in + if builtins.length parts < 2 then null else builtins.stringLength (builtins.head parts); +in +lib.genAttrs (builtins.attrNames outputs.nixosConfigurations) ( + name: + let + cfg = outputs.nixosConfigurations.${name}.config; + rules = cfg.networking.firewall.extraInputRules; + dropAt = firstIndex "saddr 10.0.0.0/8 tcp dport" rules; + acceptAt = firstIndex "saddr 10.0.0.0/8 accept" rules; + in + if !(cfg.services.k3s.enable or false) then + true + else + # The port deny must exist and must precede the broad pod accept, because + # nftables takes the first matching rule. + dropAt != null + && acceptAt != null + && dropAt < acceptAt + && lib.hasInfix "2049" rules + && lib.hasInfix "5432" rules + && lib.hasInfix "5902" rules +)