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