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