nixos/nix-daemon-firewall: Add a module to firewall FODs
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user