vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / unbound.nix
blob66451328ddddc4bd4165734c5fad8f4db1fc1a2a
1 { config, lib, pkgs, ... }:
3 with lib;
4 let
5   cfg = config.services.unbound;
7   yesOrNo = v: if v then "yes" else "no";
9   toOption = indent: n: v: "${indent}${toString n}: ${v}";
11   toConf = indent: n: v:
12     if builtins.isFloat v then (toOption indent n (builtins.toJSON v))
13     else if isInt v       then (toOption indent n (toString v))
14     else if isBool v      then (toOption indent n (yesOrNo v))
15     else if isString v    then (toOption indent n v)
16     else if isList v      then (concatMapStringsSep "\n" (toConf indent n) v)
17     else if isAttrs v     then (concatStringsSep "\n" (
18                                   ["${indent}${n}:"] ++ (
19                                     mapAttrsToList (toConf "${indent}  ") v
20                                   )
21                                 ))
22     else throw (traceSeq v "services.unbound.settings: unexpected type");
24   confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]);
25   confServer = concatStringsSep "\n" (mapAttrsToList (toConf "  ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ]));
27   confFileUnchecked = pkgs.writeText "unbound.conf" ''
28     server:
29     ${optionalString (cfg.settings.server.define-tag != "") (toOption "  " "define-tag" cfg.settings.server.define-tag)}
30     ${confServer}
31     ${confNoServer}
32   '';
33   confFile = if cfg.checkconf then pkgs.runCommandLocal "unbound-checkconf" { } ''
34     cp ${confFileUnchecked} unbound.conf
36     # fake stateDir which is not accessible in the sandbox
37     mkdir -p $PWD/state
38     sed -i unbound.conf \
39       -e '/auto-trust-anchor-file/d' \
40       -e "s|${cfg.stateDir}|$PWD/state|"
41     ${cfg.package}/bin/unbound-checkconf unbound.conf
43     cp ${confFileUnchecked} $out
44   '' else confFileUnchecked;
46   rootTrustAnchorFile = "${cfg.stateDir}/root.key";
48 in {
50   ###### interface
52   options = {
53     services.unbound = {
55       enable = mkEnableOption "Unbound domain name server";
57       package = mkPackageOption pkgs "unbound-with-systemd" { };
59       user = mkOption {
60         type = types.str;
61         default = "unbound";
62         description = "User account under which unbound runs.";
63       };
65       group = mkOption {
66         type = types.str;
67         default = "unbound";
68         description = "Group under which unbound runs.";
69       };
71       stateDir = mkOption {
72         type = types.path;
73         default = "/var/lib/unbound";
74         description = "Directory holding all state for unbound to run.";
75       };
77       checkconf = mkOption {
78         type = types.bool;
79         default = !cfg.settings ? include && !cfg.settings ? remote-control;
80         defaultText = "!services.unbound.settings ? include && !services.unbound.settings ? remote-control";
81         description = ''
82           Whether to check the resulting config file with unbound checkconf for syntax errors.
84           If settings.include is used, this options is disabled, as the import can likely not be accessed at build time.
85           If settings.remote-control is used, this option is disabled, too as the control-key-file, server-cert-file and server-key-file cannot be accessed at build time.
86         '';
87       };
89       resolveLocalQueries = mkOption {
90         type = types.bool;
91         default = true;
92         description = ''
93           Whether unbound should resolve local queries (i.e. add 127.0.0.1 to
94           /etc/resolv.conf).
95         '';
96       };
98       enableRootTrustAnchor = mkOption {
99         default = true;
100         type = types.bool;
101         description = "Use and update root trust anchor for DNSSEC validation.";
102       };
104       localControlSocketPath = mkOption {
105         default = null;
106         # FIXME: What is the proper type here so users can specify strings,
107         # paths and null?
108         # My guess would be `types.nullOr (types.either types.str types.path)`
109         # but I haven't verified yet.
110         type = types.nullOr types.str;
111         example = "/run/unbound/unbound.ctl";
112         description = ''
113           When not set to `null` this option defines the path
114           at which the unbound remote control socket should be created at. The
115           socket will be owned by the unbound user (`unbound`)
116           and group will be `nogroup`.
118           Users that should be permitted to access the socket must be in the
119           `config.services.unbound.group` group.
121           If this option is `null` remote control will not be
122           enabled. Unbounds default values apply.
123         '';
124       };
126       settings = mkOption {
127         default = {};
128         type = with types; submodule {
130           freeformType = let
131             validSettingsPrimitiveTypes = oneOf [ int str bool float ];
132             validSettingsTypes = oneOf [ validSettingsPrimitiveTypes (listOf validSettingsPrimitiveTypes) ];
133             settingsType = oneOf [ str (attrsOf validSettingsTypes) ];
134           in attrsOf (oneOf [ settingsType (listOf settingsType) ])
135               // { description = ''
136                 unbound.conf configuration type. The format consist of an attribute
137                 set of settings. Each settings can be either one value, a list of
138                 values or an attribute set. The allowed values are integers,
139                 strings, booleans or floats.
140               '';
141             };
143           options = {
144             remote-control.control-enable = mkOption {
145               type = bool;
146               default = false;
147               internal = true;
148             };
149           };
150         };
151         example = literalExpression ''
152           {
153             server = {
154               interface = [ "127.0.0.1" ];
155             };
156             forward-zone = [
157               {
158                 name = ".";
159                 forward-addr = "1.1.1.1@853#cloudflare-dns.com";
160               }
161               {
162                 name = "example.org.";
163                 forward-addr = [
164                   "1.1.1.1@853#cloudflare-dns.com"
165                   "1.0.0.1@853#cloudflare-dns.com"
166                 ];
167               }
168             ];
169             remote-control.control-enable = true;
170           };
171         '';
172         description = ''
173           Declarative Unbound configuration
174           See the {manpage}`unbound.conf(5)` manpage for a list of
175           available options.
176         '';
177       };
178     };
179   };
181   ###### implementation
183   config = mkIf cfg.enable {
185     services.unbound.settings = {
186       server = {
187         directory = mkDefault cfg.stateDir;
188         username = ''""'';
189         chroot = ''""'';
190         pidfile = ''""'';
191         # when running under systemd there is no need to daemonize
192         do-daemonize = false;
193         interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
194         access-control = mkDefault ([ "127.0.0.0/8 allow" ] ++ (optional config.networking.enableIPv6 "::1/128 allow"));
195         auto-trust-anchor-file = mkIf cfg.enableRootTrustAnchor rootTrustAnchorFile;
196         tls-cert-bundle = mkDefault "/etc/ssl/certs/ca-certificates.crt";
197         # prevent race conditions on system startup when interfaces are not yet
198         # configured
199         ip-freebind = mkDefault true;
200         define-tag = mkDefault "";
201       };
202       remote-control = {
203         control-enable = mkDefault false;
204         control-interface = mkDefault ([ "127.0.0.1" ] ++ (optional config.networking.enableIPv6 "::1"));
205         server-key-file = mkDefault "${cfg.stateDir}/unbound_server.key";
206         server-cert-file = mkDefault "${cfg.stateDir}/unbound_server.pem";
207         control-key-file = mkDefault "${cfg.stateDir}/unbound_control.key";
208         control-cert-file = mkDefault "${cfg.stateDir}/unbound_control.pem";
209       } // optionalAttrs (cfg.localControlSocketPath != null) {
210         control-enable = true;
211         control-interface = cfg.localControlSocketPath;
212       };
213     };
215     environment.systemPackages = [ cfg.package ];
217     users.users = mkIf (cfg.user == "unbound") {
218       unbound = {
219         description = "unbound daemon user";
220         isSystemUser = true;
221         group = cfg.group;
222       };
223     };
225     users.groups = mkIf (cfg.group == "unbound") {
226       unbound = {};
227     };
229     networking = mkIf cfg.resolveLocalQueries {
230       resolvconf = {
231         useLocalResolver = mkDefault true;
232       };
233     };
235     environment.etc."unbound/unbound.conf".source = confFile;
237     systemd.services.unbound = {
238       description = "Unbound recursive Domain Name Server";
239       after = [ "network.target" ];
240       before = [ "nss-lookup.target" ];
241       wantedBy = [ "multi-user.target" "nss-lookup.target" ];
243       path = mkIf cfg.settings.remote-control.control-enable [ pkgs.openssl ];
245       preStart = ''
246         ${optionalString cfg.enableRootTrustAnchor ''
247           ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
248         ''}
249         ${optionalString cfg.settings.remote-control.control-enable ''
250           ${cfg.package}/bin/unbound-control-setup -d ${cfg.stateDir}
251         ''}
252       '';
254       restartTriggers = [
255         confFile
256       ];
258       serviceConfig = {
259         ExecStart = "${cfg.package}/bin/unbound -p -d -c /etc/unbound/unbound.conf";
260         ExecReload = "+/run/current-system/sw/bin/kill -HUP $MAINPID";
262         NotifyAccess = "main";
263         Type = "notify";
265         AmbientCapabilities = [
266           "CAP_NET_BIND_SERVICE"
267           "CAP_NET_RAW" # needed if ip-transparent is set to true
268         ];
269         CapabilityBoundingSet = [
270           "CAP_NET_BIND_SERVICE"
271           "CAP_NET_RAW"
272         ];
274         User = cfg.user;
275         Group = cfg.group;
277         MemoryDenyWriteExecute = true;
278         NoNewPrivileges = true;
279         PrivateDevices = true;
280         PrivateTmp = true;
281         ProtectHome = true;
282         ProtectControlGroups = true;
283         ProtectKernelModules = true;
284         ProtectSystem = "strict";
285         ProtectClock = true;
286         ProtectHostname = true;
287         ProtectProc = "invisible";
288         ProcSubset = "pid";
289         ProtectKernelLogs = true;
290         ProtectKernelTunables = true;
291         RuntimeDirectory = "unbound";
292         ConfigurationDirectory = "unbound";
293         StateDirectory = "unbound";
294         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" "AF_UNIX" ];
295         RestrictRealtime = true;
296         SystemCallArchitectures = "native";
297         SystemCallFilter = [ "@system-service" ];
298         RestrictNamespaces = true;
299         LockPersonality = true;
300         RestrictSUIDSGID = true;
302         ReadWritePaths = [ cfg.stateDir ];
304         Restart = "on-failure";
305         RestartSec = "5s";
306       };
307     };
308   };
310   imports = [
311     (mkRenamedOptionModule [ "services" "unbound" "interfaces" ] [ "services" "unbound" "settings" "server" "interface" ])
312     (mkChangedOptionModule [ "services" "unbound" "allowedAccess" ] [ "services" "unbound" "settings" "server" "access-control" ] (
313       config: map (value: "${value} allow") (getAttrFromPath [ "services" "unbound" "allowedAccess" ] config)
314     ))
315     (mkRemovedOptionModule [ "services" "unbound" "forwardAddresses" ] ''
316       Add a new setting:
317       services.unbound.settings.forward-zone = [{
318         name = ".";
319         forward-addr = [ # Your current services.unbound.forwardAddresses ];
320       }];
321       If any of those addresses are local addresses (127.0.0.1 or ::1), you must
322       also set services.unbound.settings.server.do-not-query-localhost to false.
323     '')
324     (mkRemovedOptionModule [ "services" "unbound" "extraConfig" ] ''
325       You can use services.unbound.settings to add any configuration you want.
326     '')
327   ];