nixos/nix-daemon-firewall: Add a module to firewall FODs

This commit is contained in:
Janne Heß
2025-11-29 21:26:56 +01:00
parent a499ef8e4a
commit 04bfb2ccf5
4 changed files with 381 additions and 0 deletions
+1
View File
@@ -1517,6 +1517,7 @@
./services/system/earlyoom.nix
./services/system/kerberos/default.nix
./services/system/localtimed.nix
./services/system/nix-daemon-firewall.nix
./services/system/nix-daemon.nix
./services/system/nscd.nix
./services/system/nvme-rs.nix
@@ -0,0 +1,141 @@
{ config, lib, ... }:
let
cfg = config.nix.firewall;
in
{
options.nix.firewall = {
enable = lib.mkEnableOption "firewalling for outgoing traffic of the nix daemon";
allowLoopback = lib.mkOption {
description = "Whether not not to allow traffic on the loopback interface. Traffic is still subject to protocol/port rules";
default = false;
example = true;
};
allowPrivateNetworks = lib.mkOption {
description = "Whether not not to allow traffic to local networks. Traffic is still subject to protocol/port rules. Note that this option may break DNS resolution when the DNS resolver is in a local network";
default = true;
example = false;
};
allowNonTCPUDP = lib.mkOption {
description = "Whether to allow traffic that is neither TCP nor UDP";
type = lib.types.bool;
default = false;
example = true;
};
allowedTCPPorts = lib.mkOption {
description = "TCP ports to which traffic is allowed. Specifying no ports will allow all TCP traffic";
type = lib.types.listOf (
lib.types.oneOf [
lib.types.singleLineStr
lib.types.port
]
);
default = [ ];
example = [
"http"
443
"30000-31000"
];
};
allowedUDPPorts = lib.mkOption {
description = "UDP ports to which traffic is allowed. Specifying no ports will allow all UDP traffic";
type = lib.types.listOf (
lib.types.oneOf [
lib.types.singleLineStr
lib.types.port
]
);
default = [ ];
example = [ 53 ];
};
extraNftablesRules = lib.mkOption {
description = "Extra nftables rules to prepend to the generated ones";
type = lib.types.listOf lib.types.singleLineStr;
default = [ ];
example = [ "ip daddr 1.1.1.1 udp dport accept" ];
};
};
config = lib.mkIf cfg.enable {
# Ensure we can properly use nftables
assertions = [
{
assertion = config.networking.nftables.enable;
message = ''
The nix-daemon firewall requires an nftables-based firewall.
networking.nftables.enable must be set to true.
'';
}
{
assertion = !config.networking.nftables.flushRuleset;
message = ''
The nix-daemon firewall writes extra tables to nftables.
networking.nftables.flushRuleset must be set to false.
'';
}
];
systemd.services.nix-daemon = {
after = [ "nftables.service" ];
# Add the cgroup ID to a nft set on daemon start
serviceConfig.NFTSet = "cgroup:inet:nix_daemon_firewall:nix_daemon";
};
# Generate nftables rules
networking.nftables.ruleset = ''
table inet nix_daemon_firewall {
set nix_daemon {
type cgroupsv2
}
chain output {
type filter hook output priority 0;
socket cgroupv2 level 2 @nix_daemon goto nix_daemon_traffic
accept
}
chain nix_daemon_traffic {
# Extra rules
${lib.concatStringsSep "\n" cfg.extraNftablesRules}
# Loopback
${lib.optionalString (!cfg.allowLoopback) "ip daddr 127.0.0.0/8 counter drop"}
${lib.optionalString (!cfg.allowLoopback) "ip6 daddr ::1 counter drop"}
# Local networks
${lib.optionalString (
!cfg.allowPrivateNetworks
) "ip daddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 } counter drop"}
${lib.optionalString (
!cfg.allowPrivateNetworks
) "ip6 daddr { fd00::/8, fe80::/10 } counter drop"}
# TCP
${lib.optionalString (cfg.allowedTCPPorts != [ ]) ''
tcp dport { ${lib.concatStringsSep ", " (map builtins.toString cfg.allowedTCPPorts)} } accept
ip protocol tcp counter drop
ip6 nexthdr tcp counter drop
''}
# UDP
${lib.optionalString (cfg.allowedUDPPorts != [ ]) ''
udp dport { ${lib.concatStringsSep ", " (map builtins.toString cfg.allowedUDPPorts)} } accept
ip protocol udp counter drop
ip6 nexthdr udp counter drop
''}
# Non-TCP and non-UDP
${lib.optionalString (!cfg.allowNonTCPUDP) "ip protocol != { tcp, udp } counter drop"}
${lib.optionalString (!cfg.allowNonTCPUDP) "ip6 nexthdr != { tcp, udp } counter drop"}
accept
}
}
'';
};
}
+1
View File
@@ -1067,6 +1067,7 @@ in
nitter = runTest ./nitter.nix;
nix-channel = pkgs.callPackage ../modules/config/nix-channel/test.nix { };
nix-config = runTest ./nix-config.nix;
nix-daemon-firewall = runTest ./nix-daemon-firewall.nix;
nix-ld = runTest ./nix-ld.nix;
nix-misc = handleTest ./nix/misc.nix { };
nix-required-mounts = runTest ./nix-required-mounts;
+238
View File
@@ -0,0 +1,238 @@
{ lib, pkgs, ... }:
let
# Hashes for the test files we serve. We have multiple of them to force nix
# to download the files instead of using the already-downloaded variant
hashes = {
a = "sha256-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=";
b = "sha256-PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0=";
c = "sha256-Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y=";
};
# Builds a .nix file that can download a file from the test server
mkFetcher =
{
name,
url,
hash,
}:
pkgs.writeText "${name}.nix" ''
derivation {
name = "${name}-fetch";
builder = "builtin:fetchurl";
system = "builtin";
preferLocalBuild = true;
url = "${url}";
outputHash = "${hash}";
outputHashMode = "flat";
}
'';
# Fetches a file from the public IP, port 80
publicFetcher = mkFetcher {
name = "public";
url = "http://1.0.0.1/a";
hash = hashes.a;
};
# Fetches a file from the private IP, port 80
privateFetcher = mkFetcher {
name = "private";
url = "http://192.168.0.1/b";
hash = hashes.b;
};
# Fetches a file from the public IP, port 81
publicFetcherOtherPort = mkFetcher {
name = "public";
url = "http://192.168.0.1:81/c";
hash = hashes.c;
};
# Fetches a file from localhost
localhostFetcher = mkFetcher {
name = "localhost";
url = "http://127.0.0.1/a";
hash = hashes.a;
};
# Generates a file but tires to resolve via DNS first
resolver = pkgs.writeText "pinger.nix" ''
derivation {
name = "resolver";
system = builtins.currentSystem;
preferLocalBuild = true;
builder = "/bin/sh";
args = [ "-c" "${pkgs.pkgsStatic.busybox}/bin/nslookup . && echo hello > $out" ];
outputHash = "sha256-WJG1tSLV3whtD/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgM=";
outputHashMode = "flat";
}
'';
# Base system for all fetchers
baseNixSystem = ipSuffix: {
networking = {
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.0.${toString ipSuffix}";
prefixLength = 24;
}
{
address = "1.0.0.${toString ipSuffix}";
prefixLength = 24;
}
];
};
nftables = {
enable = true;
flushRuleset = false;
};
};
nix.settings = {
# Gets rid of substitution warnings
substituters = lib.mkForce [ ];
# Gives us access inside the nix sandbox
extra-sandbox-paths = [ "${pkgs.pkgsStatic.busybox}" ];
};
# Easy way to get files to the system
environment.etc = {
"fetch-public.nix".source = publicFetcher;
"fetch-private.nix".source = privateFetcher;
"fetch-public-other-port.nix".source = publicFetcherOtherPort;
"fetch-localhost.nix".source = localhostFetcher;
"resolver.nix".source = resolver;
};
# For localhost tests
services.nginx = {
enable = true;
virtualHosts.default.locations."/a".return = "200 \"a\"";
};
};
in
{
name = "nix-daemon-firewall";
meta.maintainers = with lib.maintainers; [ das_j ];
nodes = {
httpServer = {
services.nginx = {
enable = true;
virtualHosts.default = {
listen = [
{
addr = "0.0.0.0";
port = 80;
}
{
addr = "0.0.0.0";
port = 81;
}
];
locations = {
"/a".return = "200 \"a\"";
"/b".return = "200 \"b\"";
"/c".return = "200 \"c\"";
};
};
};
networking = {
useDHCP = false;
interfaces.eth1 = {
ipv4.addresses = [
{
address = "192.168.0.1";
prefixLength = 24;
}
{
address = "1.0.0.1";
prefixLength = 24;
}
];
};
firewall.allowedTCPPorts = [
80
81
];
};
};
unfirewalled = {
imports = [ (baseNixSystem 2) ];
};
everythingAllowed = {
imports = [ (baseNixSystem 3) ];
nix.firewall = {
enable = true;
allowLoopback = true;
allowNonTCPUDP = true;
};
};
noLocalNets = {
imports = [ (baseNixSystem 4) ];
nix.firewall = {
enable = true;
allowPrivateNetworks = false;
};
};
onlyPort80 = {
imports = [ (baseNixSystem 5) ];
nix.firewall = {
enable = true;
allowedTCPPorts = [ 80 ];
};
};
resolved = {
imports = [ (baseNixSystem 6) ];
nix.firewall = {
enable = true;
};
services.resolved.enable = true;
};
};
testScript = ''
# Startup
start_all()
httpServer.wait_for_open_port(80)
# No firewall
unfirewalled.wait_for_unit("multi-user.target")
unfirewalled.fail("nft list ruleset | grep nix_daemon_traffic")
# Everything allowed
everythingAllowed.wait_for_unit("multi-user.target")
everythingAllowed.succeed("nft list ruleset | grep nix_daemon_traffic")
everythingAllowed.succeed("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-public.nix")
everythingAllowed.succeed("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-private.nix")
everythingAllowed.succeed("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-public-other-port.nix")
everythingAllowed.succeed("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-localhost.nix")
# No local networks and no localhost
noLocalNets.wait_for_unit("multi-user.target")
noLocalNets.succeed("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-public.nix")
noLocalNets.fail("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-private.nix")
noLocalNets.fail("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-localhost.nix")
# Ports
onlyPort80.wait_for_unit("multi-user.target")
onlyPort80.succeed("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-public.nix")
onlyPort80.fail("NIX_REMOTE=daemon nix-build --timeout 5 /etc/fetch-public-other-port.nix")
# systemd-resolved
resolved.wait_for_unit("multi-user.target")
resolved.succeed("NIX_REMOTE=daemon nix-build --timeout 5 /etc/resolver.nix")
'';
}