at 23.05-pre 20 kB view raw
1/* This module enables a simple firewall. 2 3 The firewall can be customised in arbitrary ways by setting 4 networking.firewall.extraCommands. For modularity, the firewall 5 uses several chains: 6 7 - nixos-fw is the main chain for input packet processing. 8 9 - nixos-fw-accept is called for accepted packets. If you want 10 additional logging, or want to reject certain packets anyway, you 11 can insert rules at the start of this chain. 12 13 - nixos-fw-log-refuse and nixos-fw-refuse are called for 14 refused packets. (The former jumps to the latter after logging 15 the packet.) If you want additional logging, or want to accept 16 certain packets anyway, you can insert rules at the start of 17 this chain. 18 19 - nixos-fw-rpfilter is used as the main chain in the mangle table, 20 called from the built-in PREROUTING chain. If the kernel 21 supports it and `cfg.checkReversePath` is set this chain will 22 perform a reverse path filter test. 23 24 - nixos-drop is used while reloading the firewall in order to drop 25 all traffic. Since reloading isn't implemented in an atomic way 26 this'll prevent any traffic from leaking through while reloading 27 the firewall. However, if the reloading fails, the firewall-stop 28 script will be called which in return will effectively disable the 29 complete firewall (in the default configuration). 30 31*/ 32 33{ config, lib, pkgs, ... }: 34 35with lib; 36 37let 38 39 cfg = config.networking.firewall; 40 41 inherit (config.boot.kernelPackages) kernel; 42 43 kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false); 44 45 helpers = import ./helpers.nix { inherit config lib; }; 46 47 writeShScript = name: text: let dir = pkgs.writeScriptBin name '' 48 #! ${pkgs.runtimeShell} -e 49 ${text} 50 ''; in "${dir}/bin/${name}"; 51 52 defaultInterface = { default = mapAttrs (name: value: cfg.${name}) commonOptions; }; 53 allInterfaces = defaultInterface // cfg.interfaces; 54 55 startScript = writeShScript "firewall-start" '' 56 ${helpers} 57 58 # Flush the old firewall rules. !!! Ideally, updating the 59 # firewall would be atomic. Apparently that's possible 60 # with iptables-restore. 61 ip46tables -D INPUT -j nixos-fw 2> /dev/null || true 62 for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do 63 ip46tables -F "$chain" 2> /dev/null || true 64 ip46tables -X "$chain" 2> /dev/null || true 65 done 66 67 68 # The "nixos-fw-accept" chain just accepts packets. 69 ip46tables -N nixos-fw-accept 70 ip46tables -A nixos-fw-accept -j ACCEPT 71 72 73 # The "nixos-fw-refuse" chain rejects or drops packets. 74 ip46tables -N nixos-fw-refuse 75 76 ${if cfg.rejectPackets then '' 77 # Send a reset for existing TCP connections that we've 78 # somehow forgotten about. Send ICMP "port unreachable" 79 # for everything else. 80 ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset 81 ip46tables -A nixos-fw-refuse -j REJECT 82 '' else '' 83 ip46tables -A nixos-fw-refuse -j DROP 84 ''} 85 86 87 # The "nixos-fw-log-refuse" chain performs logging, then 88 # jumps to the "nixos-fw-refuse" chain. 89 ip46tables -N nixos-fw-log-refuse 90 91 ${optionalString cfg.logRefusedConnections '' 92 ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: " 93 ''} 94 ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) '' 95 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \ 96 -j LOG --log-level info --log-prefix "refused broadcast: " 97 ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \ 98 -j LOG --log-level info --log-prefix "refused multicast: " 99 ''} 100 ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse 101 ${optionalString cfg.logRefusedPackets '' 102 ip46tables -A nixos-fw-log-refuse \ 103 -j LOG --log-level info --log-prefix "refused packet: " 104 ''} 105 ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse 106 107 108 # The "nixos-fw" chain does the actual work. 109 ip46tables -N nixos-fw 110 111 # Clean up rpfilter rules 112 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true 113 ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true 114 ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true 115 116 ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 117 # Perform a reverse-path test to refuse spoofers 118 # For now, we just drop, as the mangle table doesn't have a log-refuse yet 119 ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true 120 ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN 121 122 # Allows this host to act as a DHCP4 client without first having to use APIPA 123 iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN 124 125 # Allows this host to act as a DHCPv4 server 126 iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN 127 128 ${optionalString cfg.logReversePathDrops '' 129 ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: " 130 ''} 131 ip46tables -t mangle -A nixos-fw-rpfilter -j DROP 132 133 ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter 134 ''} 135 136 # Accept all traffic on the trusted interfaces. 137 ${flip concatMapStrings cfg.trustedInterfaces (iface: '' 138 ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept 139 '')} 140 141 # Accept packets from established or related connections. 142 ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept 143 144 # Accept connections to the allowed TCP ports. 145 ${concatStrings (mapAttrsToList (iface: cfg: 146 concatMapStrings (port: 147 '' 148 ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 149 '' 150 ) cfg.allowedTCPPorts 151 ) allInterfaces)} 152 153 # Accept connections to the allowed TCP port ranges. 154 ${concatStrings (mapAttrsToList (iface: cfg: 155 concatMapStrings (rangeAttr: 156 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 157 '' 158 ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 159 '' 160 ) cfg.allowedTCPPortRanges 161 ) allInterfaces)} 162 163 # Accept packets on the allowed UDP ports. 164 ${concatStrings (mapAttrsToList (iface: cfg: 165 concatMapStrings (port: 166 '' 167 ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 168 '' 169 ) cfg.allowedUDPPorts 170 ) allInterfaces)} 171 172 # Accept packets on the allowed UDP port ranges. 173 ${concatStrings (mapAttrsToList (iface: cfg: 174 concatMapStrings (rangeAttr: 175 let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in 176 '' 177 ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"} 178 '' 179 ) cfg.allowedUDPPortRanges 180 ) allInterfaces)} 181 182 # Optionally respond to ICMPv4 pings. 183 ${optionalString cfg.allowPing '' 184 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) 185 "-m limit ${cfg.pingLimit} " 186 }-j nixos-fw-accept 187 ''} 188 189 ${optionalString config.networking.enableIPv6 '' 190 # Accept all ICMPv6 messages except redirects and node 191 # information queries (type 139). See RFC 4890, section 192 # 4.4. 193 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP 194 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP 195 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept 196 197 # Allow this host to act as a DHCPv6 client 198 ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept 199 ''} 200 201 ${cfg.extraCommands} 202 203 # Reject/drop everything else. 204 ip46tables -A nixos-fw -j nixos-fw-log-refuse 205 206 207 # Enable the firewall. 208 ip46tables -A INPUT -j nixos-fw 209 ''; 210 211 stopScript = writeShScript "firewall-stop" '' 212 ${helpers} 213 214 # Clean up in case reload fails 215 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 216 217 # Clean up after added ruleset 218 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true 219 220 ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 221 ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true 222 ''} 223 224 ${cfg.extraStopCommands} 225 ''; 226 227 reloadScript = writeShScript "firewall-reload" '' 228 ${helpers} 229 230 # Create a unique drop rule 231 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 232 ip46tables -F nixos-drop 2>/dev/null || true 233 ip46tables -X nixos-drop 2>/dev/null || true 234 ip46tables -N nixos-drop 235 ip46tables -A nixos-drop -j DROP 236 237 # Don't allow traffic to leak out until the script has completed 238 ip46tables -A INPUT -j nixos-drop 239 240 ${cfg.extraStopCommands} 241 242 if ${startScript}; then 243 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 244 else 245 echo "Failed to reload firewall... Stopping" 246 ${stopScript} 247 exit 1 248 fi 249 ''; 250 251 canonicalizePortList = 252 ports: lib.unique (builtins.sort builtins.lessThan ports); 253 254 commonOptions = { 255 allowedTCPPorts = mkOption { 256 type = types.listOf types.port; 257 default = [ ]; 258 apply = canonicalizePortList; 259 example = [ 22 80 ]; 260 description = 261 lib.mdDoc '' 262 List of TCP ports on which incoming connections are 263 accepted. 264 ''; 265 }; 266 267 allowedTCPPortRanges = mkOption { 268 type = types.listOf (types.attrsOf types.port); 269 default = [ ]; 270 example = [ { from = 8999; to = 9003; } ]; 271 description = 272 lib.mdDoc '' 273 A range of TCP ports on which incoming connections are 274 accepted. 275 ''; 276 }; 277 278 allowedUDPPorts = mkOption { 279 type = types.listOf types.port; 280 default = [ ]; 281 apply = canonicalizePortList; 282 example = [ 53 ]; 283 description = 284 lib.mdDoc '' 285 List of open UDP ports. 286 ''; 287 }; 288 289 allowedUDPPortRanges = mkOption { 290 type = types.listOf (types.attrsOf types.port); 291 default = [ ]; 292 example = [ { from = 60000; to = 61000; } ]; 293 description = 294 lib.mdDoc '' 295 Range of open UDP ports. 296 ''; 297 }; 298 }; 299 300in 301 302{ 303 304 ###### interface 305 306 options = { 307 308 networking.firewall = { 309 enable = mkOption { 310 type = types.bool; 311 default = true; 312 description = 313 lib.mdDoc '' 314 Whether to enable the firewall. This is a simple stateful 315 firewall that blocks connection attempts to unauthorised TCP 316 or UDP ports on this machine. It does not affect packet 317 forwarding. 318 ''; 319 }; 320 321 package = mkOption { 322 type = types.package; 323 default = pkgs.iptables; 324 defaultText = literalExpression "pkgs.iptables"; 325 example = literalExpression "pkgs.iptables-legacy"; 326 description = 327 lib.mdDoc '' 328 The iptables package to use for running the firewall service. 329 ''; 330 }; 331 332 logRefusedConnections = mkOption { 333 type = types.bool; 334 default = true; 335 description = 336 lib.mdDoc '' 337 Whether to log rejected or dropped incoming connections. 338 Note: The logs are found in the kernel logs, i.e. dmesg 339 or journalctl -k. 340 ''; 341 }; 342 343 logRefusedPackets = mkOption { 344 type = types.bool; 345 default = false; 346 description = 347 lib.mdDoc '' 348 Whether to log all rejected or dropped incoming packets. 349 This tends to give a lot of log messages, so it's mostly 350 useful for debugging. 351 Note: The logs are found in the kernel logs, i.e. dmesg 352 or journalctl -k. 353 ''; 354 }; 355 356 logRefusedUnicastsOnly = mkOption { 357 type = types.bool; 358 default = true; 359 description = 360 lib.mdDoc '' 361 If {option}`networking.firewall.logRefusedPackets` 362 and this option are enabled, then only log packets 363 specifically directed at this machine, i.e., not broadcasts 364 or multicasts. 365 ''; 366 }; 367 368 rejectPackets = mkOption { 369 type = types.bool; 370 default = false; 371 description = 372 lib.mdDoc '' 373 If set, refused packets are rejected rather than dropped 374 (ignored). This means that an ICMP "port unreachable" error 375 message is sent back to the client (or a TCP RST packet in 376 case of an existing connection). Rejecting packets makes 377 port scanning somewhat easier. 378 ''; 379 }; 380 381 trustedInterfaces = mkOption { 382 type = types.listOf types.str; 383 default = [ ]; 384 example = [ "enp0s2" ]; 385 description = 386 lib.mdDoc '' 387 Traffic coming in from these interfaces will be accepted 388 unconditionally. Traffic from the loopback (lo) interface 389 will always be accepted. 390 ''; 391 }; 392 393 allowPing = mkOption { 394 type = types.bool; 395 default = true; 396 description = 397 lib.mdDoc '' 398 Whether to respond to incoming ICMPv4 echo requests 399 ("pings"). ICMPv6 pings are always allowed because the 400 larger address space of IPv6 makes network scanning much 401 less effective. 402 ''; 403 }; 404 405 pingLimit = mkOption { 406 type = types.nullOr (types.separatedString " "); 407 default = null; 408 example = "--limit 1/minute --limit-burst 5"; 409 description = 410 lib.mdDoc '' 411 If pings are allowed, this allows setting rate limits 412 on them. If non-null, this option should be in the form of 413 flags like "--limit 1/minute --limit-burst 5" 414 ''; 415 }; 416 417 checkReversePath = mkOption { 418 type = types.either types.bool (types.enum ["strict" "loose"]); 419 default = kernelHasRPFilter; 420 defaultText = literalMD "`true` if supported by the chosen kernel"; 421 example = "loose"; 422 description = 423 lib.mdDoc '' 424 Performs a reverse path filter test on a packet. If a reply 425 to the packet would not be sent via the same interface that 426 the packet arrived on, it is refused. 427 428 If using asymmetric routing or other complicated routing, set 429 this option to loose mode or disable it and setup your own 430 counter-measures. 431 432 This option can be either true (or "strict"), "loose" (only 433 drop the packet if the source address is not reachable via any 434 interface) or false. Defaults to the value of 435 kernelHasRPFilter. 436 ''; 437 }; 438 439 logReversePathDrops = mkOption { 440 type = types.bool; 441 default = false; 442 description = 443 lib.mdDoc '' 444 Logs dropped packets failing the reverse path filter test if 445 the option networking.firewall.checkReversePath is enabled. 446 ''; 447 }; 448 449 connectionTrackingModules = mkOption { 450 type = types.listOf types.str; 451 default = [ ]; 452 example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ]; 453 description = 454 lib.mdDoc '' 455 List of connection-tracking helpers that are auto-loaded. 456 The complete list of possible values is given in the example. 457 458 As helpers can pose as a security risk, it is advised to 459 set this to an empty list and disable the setting 460 networking.firewall.autoLoadConntrackHelpers unless you 461 know what you are doing. Connection tracking is disabled 462 by default. 463 464 Loading of helpers is recommended to be done through the 465 CT target. More info: 466 https://home.regit.org/netfilter-en/secure-use-of-helpers/ 467 ''; 468 }; 469 470 autoLoadConntrackHelpers = mkOption { 471 type = types.bool; 472 default = false; 473 description = 474 lib.mdDoc '' 475 Whether to auto-load connection-tracking helpers. 476 See the description at networking.firewall.connectionTrackingModules 477 478 (needs kernel 3.5+) 479 ''; 480 }; 481 482 extraCommands = mkOption { 483 type = types.lines; 484 default = ""; 485 example = "iptables -A INPUT -p icmp -j ACCEPT"; 486 description = 487 lib.mdDoc '' 488 Additional shell commands executed as part of the firewall 489 initialisation script. These are executed just before the 490 final "reject" firewall rule is added, so they can be used 491 to allow packets that would otherwise be refused. 492 ''; 493 }; 494 495 extraPackages = mkOption { 496 type = types.listOf types.package; 497 default = [ ]; 498 example = literalExpression "[ pkgs.ipset ]"; 499 description = 500 lib.mdDoc '' 501 Additional packages to be included in the environment of the system 502 as well as the path of networking.firewall.extraCommands. 503 ''; 504 }; 505 506 extraStopCommands = mkOption { 507 type = types.lines; 508 default = ""; 509 example = "iptables -P INPUT ACCEPT"; 510 description = 511 lib.mdDoc '' 512 Additional shell commands executed as part of the firewall 513 shutdown script. These are executed just after the removal 514 of the NixOS input rule, or if the service enters a failed 515 state. 516 ''; 517 }; 518 519 interfaces = mkOption { 520 default = { }; 521 type = with types; attrsOf (submodule [ { options = commonOptions; } ]); 522 description = 523 lib.mdDoc '' 524 Interface-specific open ports. 525 ''; 526 }; 527 } // commonOptions; 528 529 }; 530 531 532 ###### implementation 533 534 # FIXME: Maybe if `enable' is false, the firewall should still be 535 # built but not started by default? 536 config = mkIf cfg.enable { 537 538 networking.firewall.trustedInterfaces = [ "lo" ]; 539 540 environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages; 541 542 boot.kernelModules = (optional cfg.autoLoadConntrackHelpers "nf_conntrack") 543 ++ map (x: "nf_conntrack_${x}") cfg.connectionTrackingModules; 544 boot.extraModprobeConfig = optionalString cfg.autoLoadConntrackHelpers '' 545 options nf_conntrack nf_conntrack_helper=1 546 ''; 547 548 assertions = [ 549 # This is approximately "checkReversePath -> kernelHasRPFilter", 550 # but the checkReversePath option can include non-boolean 551 # values. 552 { assertion = cfg.checkReversePath == false || kernelHasRPFilter; 553 message = "This kernel does not support rpfilter"; } 554 ]; 555 556 systemd.services.firewall = { 557 description = "Firewall"; 558 wantedBy = [ "sysinit.target" ]; 559 wants = [ "network-pre.target" ]; 560 before = [ "network-pre.target" ]; 561 after = [ "systemd-modules-load.service" ]; 562 563 path = [ cfg.package ] ++ cfg.extraPackages; 564 565 # FIXME: this module may also try to load kernel modules, but 566 # containers don't have CAP_SYS_MODULE. So the host system had 567 # better have all necessary modules already loaded. 568 unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 569 unitConfig.DefaultDependencies = false; 570 571 reloadIfChanged = true; 572 573 serviceConfig = { 574 Type = "oneshot"; 575 RemainAfterExit = true; 576 ExecStart = "@${startScript} firewall-start"; 577 ExecReload = "@${reloadScript} firewall-reload"; 578 ExecStop = "@${stopScript} firewall-stop"; 579 }; 580 }; 581 582 }; 583 584}