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}