at 21.11-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 raw 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 raw -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true 113 ip46tables -t raw -F nixos-fw-rpfilter 2> /dev/null || true 114 ip46tables -t raw -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 raw table doesn't have a log-refuse yet 119 ip46tables -t raw -N nixos-fw-rpfilter 2> /dev/null || true 120 ip46tables -t raw -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 raw -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 raw -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 raw -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: " 130 ''} 131 ip46tables -t raw -A nixos-fw-rpfilter -j DROP 132 133 ip46tables -t raw -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 # Accept IPv4 multicast. Not a big security risk since 183 # probably nobody is listening anyway. 184 #iptables -A nixos-fw -d 224.0.0.0/4 -j nixos-fw-accept 185 186 # Optionally respond to ICMPv4 pings. 187 ${optionalString cfg.allowPing '' 188 iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null) 189 "-m limit ${cfg.pingLimit} " 190 }-j nixos-fw-accept 191 ''} 192 193 ${optionalString config.networking.enableIPv6 '' 194 # Accept all ICMPv6 messages except redirects and node 195 # information queries (type 139). See RFC 4890, section 196 # 4.4. 197 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP 198 ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP 199 ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept 200 201 # Allow this host to act as a DHCPv6 client 202 ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept 203 ''} 204 205 ${cfg.extraCommands} 206 207 # Reject/drop everything else. 208 ip46tables -A nixos-fw -j nixos-fw-log-refuse 209 210 211 # Enable the firewall. 212 ip46tables -A INPUT -j nixos-fw 213 ''; 214 215 stopScript = writeShScript "firewall-stop" '' 216 ${helpers} 217 218 # Clean up in case reload fails 219 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 220 221 # Clean up after added ruleset 222 ip46tables -D INPUT -j nixos-fw 2>/dev/null || true 223 224 ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) '' 225 ip46tables -t raw -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true 226 ''} 227 228 ${cfg.extraStopCommands} 229 ''; 230 231 reloadScript = writeShScript "firewall-reload" '' 232 ${helpers} 233 234 # Create a unique drop rule 235 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 236 ip46tables -F nixos-drop 2>/dev/null || true 237 ip46tables -X nixos-drop 2>/dev/null || true 238 ip46tables -N nixos-drop 239 ip46tables -A nixos-drop -j DROP 240 241 # Don't allow traffic to leak out until the script has completed 242 ip46tables -A INPUT -j nixos-drop 243 244 ${cfg.extraStopCommands} 245 246 if ${startScript}; then 247 ip46tables -D INPUT -j nixos-drop 2>/dev/null || true 248 else 249 echo "Failed to reload firewall... Stopping" 250 ${stopScript} 251 exit 1 252 fi 253 ''; 254 255 canonicalizePortList = 256 ports: lib.unique (builtins.sort builtins.lessThan ports); 257 258 commonOptions = { 259 allowedTCPPorts = mkOption { 260 type = types.listOf types.port; 261 default = [ ]; 262 apply = canonicalizePortList; 263 example = [ 22 80 ]; 264 description = 265 '' 266 List of TCP ports on which incoming connections are 267 accepted. 268 ''; 269 }; 270 271 allowedTCPPortRanges = mkOption { 272 type = types.listOf (types.attrsOf types.port); 273 default = [ ]; 274 example = [ { from = 8999; to = 9003; } ]; 275 description = 276 '' 277 A range of TCP ports on which incoming connections are 278 accepted. 279 ''; 280 }; 281 282 allowedUDPPorts = mkOption { 283 type = types.listOf types.port; 284 default = [ ]; 285 apply = canonicalizePortList; 286 example = [ 53 ]; 287 description = 288 '' 289 List of open UDP ports. 290 ''; 291 }; 292 293 allowedUDPPortRanges = mkOption { 294 type = types.listOf (types.attrsOf types.port); 295 default = [ ]; 296 example = [ { from = 60000; to = 61000; } ]; 297 description = 298 '' 299 Range of open UDP ports. 300 ''; 301 }; 302 }; 303 304in 305 306{ 307 308 ###### interface 309 310 options = { 311 312 networking.firewall = { 313 enable = mkOption { 314 type = types.bool; 315 default = true; 316 description = 317 '' 318 Whether to enable the firewall. This is a simple stateful 319 firewall that blocks connection attempts to unauthorised TCP 320 or UDP ports on this machine. It does not affect packet 321 forwarding. 322 ''; 323 }; 324 325 package = mkOption { 326 type = types.package; 327 default = pkgs.iptables; 328 defaultText = "pkgs.iptables"; 329 example = literalExample "pkgs.iptables-nftables-compat"; 330 description = 331 '' 332 The iptables package to use for running the firewall service." 333 ''; 334 }; 335 336 logRefusedConnections = mkOption { 337 type = types.bool; 338 default = true; 339 description = 340 '' 341 Whether to log rejected or dropped incoming connections. 342 ''; 343 }; 344 345 logRefusedPackets = mkOption { 346 type = types.bool; 347 default = false; 348 description = 349 '' 350 Whether to log all rejected or dropped incoming packets. 351 This tends to give a lot of log messages, so it's mostly 352 useful for debugging. 353 ''; 354 }; 355 356 logRefusedUnicastsOnly = mkOption { 357 type = types.bool; 358 default = true; 359 description = 360 '' 361 If <option>networking.firewall.logRefusedPackets</option> 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 '' 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 '' 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 '' 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 '' 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 example = "loose"; 421 description = 422 '' 423 Performs a reverse path filter test on a packet. If a reply 424 to the packet would not be sent via the same interface that 425 the packet arrived on, it is refused. 426 427 If using asymmetric routing or other complicated routing, set 428 this option to loose mode or disable it and setup your own 429 counter-measures. 430 431 This option can be either true (or "strict"), "loose" (only 432 drop the packet if the source address is not reachable via any 433 interface) or false. Defaults to the value of 434 kernelHasRPFilter. 435 436 (needs kernel 3.3+) 437 ''; 438 }; 439 440 logReversePathDrops = mkOption { 441 type = types.bool; 442 default = false; 443 description = 444 '' 445 Logs dropped packets failing the reverse path filter test if 446 the option networking.firewall.checkReversePath is enabled. 447 ''; 448 }; 449 450 connectionTrackingModules = mkOption { 451 type = types.listOf types.str; 452 default = [ ]; 453 example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ]; 454 description = 455 '' 456 List of connection-tracking helpers that are auto-loaded. 457 The complete list of possible values is given in the example. 458 459 As helpers can pose as a security risk, it is advised to 460 set this to an empty list and disable the setting 461 networking.firewall.autoLoadConntrackHelpers unless you 462 know what you are doing. Connection tracking is disabled 463 by default. 464 465 Loading of helpers is recommended to be done through the 466 CT target. More info: 467 https://home.regit.org/netfilter-en/secure-use-of-helpers/ 468 ''; 469 }; 470 471 autoLoadConntrackHelpers = mkOption { 472 type = types.bool; 473 default = false; 474 description = 475 '' 476 Whether to auto-load connection-tracking helpers. 477 See the description at networking.firewall.connectionTrackingModules 478 479 (needs kernel 3.5+) 480 ''; 481 }; 482 483 extraCommands = mkOption { 484 type = types.lines; 485 default = ""; 486 example = "iptables -A INPUT -p icmp -j ACCEPT"; 487 description = 488 '' 489 Additional shell commands executed as part of the firewall 490 initialisation script. These are executed just before the 491 final "reject" firewall rule is added, so they can be used 492 to allow packets that would otherwise be refused. 493 ''; 494 }; 495 496 extraPackages = mkOption { 497 type = types.listOf types.package; 498 default = [ ]; 499 example = literalExample "[ pkgs.ipset ]"; 500 description = 501 '' 502 Additional packages to be included in the environment of the system 503 as well as the path of networking.firewall.extraCommands. 504 ''; 505 }; 506 507 extraStopCommands = mkOption { 508 type = types.lines; 509 default = ""; 510 example = "iptables -P INPUT ACCEPT"; 511 description = 512 '' 513 Additional shell commands executed as part of the firewall 514 shutdown script. These are executed just after the removal 515 of the NixOS input rule, or if the service enters a failed 516 state. 517 ''; 518 }; 519 520 interfaces = mkOption { 521 default = { }; 522 type = with types; attrsOf (submodule [ { options = commonOptions; } ]); 523 description = 524 '' 525 Interface-specific open ports. 526 ''; 527 }; 528 } // commonOptions; 529 530 }; 531 532 533 ###### implementation 534 535 # FIXME: Maybe if `enable' is false, the firewall should still be 536 # built but not started by default? 537 config = mkIf cfg.enable { 538 539 networking.firewall.trustedInterfaces = [ "lo" ]; 540 541 environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages; 542 543 boot.kernelModules = (optional cfg.autoLoadConntrackHelpers "nf_conntrack") 544 ++ map (x: "nf_conntrack_${x}") cfg.connectionTrackingModules; 545 boot.extraModprobeConfig = optionalString cfg.autoLoadConntrackHelpers '' 546 options nf_conntrack nf_conntrack_helper=1 547 ''; 548 549 assertions = [ 550 # This is approximately "checkReversePath -> kernelHasRPFilter", 551 # but the checkReversePath option can include non-boolean 552 # values. 553 { assertion = cfg.checkReversePath == false || kernelHasRPFilter; 554 message = "This kernel does not support rpfilter"; } 555 ]; 556 557 systemd.services.firewall = { 558 description = "Firewall"; 559 wantedBy = [ "sysinit.target" ]; 560 wants = [ "network-pre.target" ]; 561 before = [ "network-pre.target" ]; 562 after = [ "systemd-modules-load.service" ]; 563 564 path = [ cfg.package ] ++ cfg.extraPackages; 565 566 # FIXME: this module may also try to load kernel modules, but 567 # containers don't have CAP_SYS_MODULE. So the host system had 568 # better have all necessary modules already loaded. 569 unitConfig.ConditionCapability = "CAP_NET_ADMIN"; 570 unitConfig.DefaultDependencies = false; 571 572 reloadIfChanged = true; 573 574 serviceConfig = { 575 Type = "oneshot"; 576 RemainAfterExit = true; 577 ExecStart = "@${startScript} firewall-start"; 578 ExecReload = "@${reloadScript} firewall-reload"; 579 ExecStop = "@${stopScript} firewall-stop"; 580 }; 581 }; 582 583 }; 584 585}