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