Files
nix-config/lib/genK3sAgentModule.nix
T
ryan4yin | 二花 e5b95affa6 feat(nixos): default-deny host inbound, trust the LAN/tailnet (#325)
Hosts used to trust the router as the only boundary (servers ran with the
firewall off; service ports were opened globally), so any LAN device could
reach every host service. Move filtering to the hosts as defence in depth
behind the router, which keeps its firewall on and uses a port mapping for
the few ports we actually need.

- base: nftables backend; deny inbound by default; trust the home LAN by
  source and the tailnet by interface (restrict the tailnet with ACLs);
  suppress per-service `openFirewall` global ports; allow node_exporter only
  from the monitoring host. The local podman bridge, the tailnet and the pod
  CIDRs are accepted only after the node_exporter drop.
- servers: stop disabling the firewall; drop redundant global port opens
  (caddy 80/443, NFS 2049, Jellyfin, localsend/test ports); delete the
  redundant desktop firewall module; aarch64/riscv64 import the firewall
  explicitly (akane was missing it).
- k3s: Cilium pods (10.0.0.0/8, fd00::/8) may reach host services; drop the
  flannel-era cni0/flannel.1 trust.
- youko: postgres listens on localhost + the LAN with pg_hba limited to the
  LAN; the NFS export stays LAN-scoped; remove the unauthenticated v2ray
  proxy.
- tests: `security-firewall` asserts every host runs the shared firewall, has
  the LAN CIDR rule and no global ports.
2026-09-26 14:02:03 +08:00

65 lines
1.7 KiB
Nix

{
pkgs,
masterHost,
tokenFile,
nodeLabels ? [ ],
k3sExtraArgs ? [ ],
...
}:
let
package = pkgs.k3s;
in
{
environment.systemPackages = [ package ];
# Kernel modules required by cilium
boot.kernelModules = [
"ip6_tables"
"ip6table_mangle"
"ip6table_raw"
"ip6table_filter"
];
networking.enableIPv6 = true;
networking.nat = {
enable = true;
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.
networking.firewall.extraInputRules = pkgs.lib.mkAfter ''
ip saddr 10.0.0.0/8 accept
ip6 saddr fd00::/104 accept
'';
services.k3s = {
enable = true;
inherit package tokenFile;
role = "agent";
serverAddr = "https://${masterHost}:6443";
# https://docs.k3s.io/cli/agent
extraFlags =
let
flagList = [
"--data-dir /var/lib/rancher/k3s"
]
++ (map (label: "--node-label=${label}") nodeLabels)
++ k3sExtraArgs;
in
pkgs.lib.concatStringsSep " " flagList;
};
# Link k3s's CNI directories to the conventional locations, so CNI plugins
# that write to /etc/cni/net.d or /opt/cni/bin (cilium, istio-cni) share them
# with k3s. Without this, cilium writes /etc/cni/net.d while istio-cni reads
# k3s's own dir, and the chained plugin never finds a network config.
# Mirrors the server module.
systemd.tmpfiles.rules = [
"L+ /opt/cni/bin - - - - /var/lib/rancher/k3s/data/cni/"
"d /var/lib/rancher/k3s/agent/etc/cni/net.d 0751 root root - -"
"L+ /etc/cni/net.d - - - - /var/lib/rancher/k3s/agent/etc/cni/net.d"
];
}