11 cfg = config.services.nsd;
14 stateDir = "/var/lib/nsd";
15 pidFile = stateDir + "/var/nsd.pid";
17 # build nsd with the options needed for the given config
18 nsdPkg = pkgs.nsd.override {
19 bind8Stats = cfg.bind8Stats;
21 ratelimit = cfg.ratelimit.enable;
22 rootServer = cfg.rootServer;
23 zoneStats = length (collect (x: (x.zoneStats or null) != null) cfg.zones) > 0;
26 mkZoneFileName = name: if name == "." then "root" else name;
28 # replaces include: directives for keys with fake keys for nsd-checkconf
32 mapAttrsToList (keyName: keyOptions: ''
33 fakeKey="$(${pkgs.bind}/bin/tsig-keygen -a ${
38 } | grep -oP "\s*secret \"\K.*(?=\";)")"
39 sed "s@^\s*include:\s*\"${stateDir}/private/${keyName}\"\$@secret: $fakeKey@" -i $out/nsd.conf
43 nsdEnv = pkgs.buildEnv {
46 paths = [ configFile ] ++ mapAttrsToList (name: zone: writeZoneData name zone.data) zoneConfigs;
49 echo "checking zone files"
53 echo "|- checking zone '$out/zones/$zoneFile'"
54 ${nsdPkg}/sbin/nsd-checkzone "$zoneFile" "$zoneFile" || {
55 if grep -q \\\\\\$ "$zoneFile"; then
56 echo zone "$zoneFile" contains escaped dollar signs \\\$
57 echo Escaping them is not needed any more. Please make sure \
58 to unescape them where they prefix a variable name.
65 echo "checking configuration file"
66 # Save original config file including key references...
67 cp $out/nsd.conf{,.orig}
68 # ...inject mock keys into config
69 ${injectFakeKeys cfg.keys}
71 ${nsdPkg}/sbin/nsd-checkconf $out/nsd.conf
72 # ... and restore original config file.
73 mv $out/nsd.conf{.orig,}
80 name = "nsd-zone-${mkZoneFileName name}";
82 destination = "/zones/${mkZoneFileName name}";
85 # options are ordered alphanumerically by the nixos option name
86 configFile = pkgs.writeTextDir "nsd.conf" ''
91 # The directory for zonefile: files. The daemon chdirs here.
92 zonesdir: "${stateDir}"
94 # the list of dynamically added zones.
96 xfrdfile: "${stateDir}/var/xfrd.state"
97 xfrdir: "${stateDir}/tmp"
98 zonelistfile: "${stateDir}/var/zone.list"
101 ${forEach " ip-address: " cfg.interfaces}
103 ip-freebind: ${yesOrNo cfg.ipFreebind}
104 hide-version: ${yesOrNo cfg.hideVersion}
105 identity: "${cfg.identity}"
106 ip-transparent: ${yesOrNo cfg.ipTransparent}
107 do-ip4: ${yesOrNo cfg.ipv4}
108 ipv4-edns-size: ${toString cfg.ipv4EDNSSize}
109 do-ip6: ${yesOrNo cfg.ipv6}
110 ipv6-edns-size: ${toString cfg.ipv6EDNSSize}
111 log-time-ascii: ${yesOrNo cfg.logTimeAscii}
112 ${maybeString "nsid: " cfg.nsid}
113 port: ${toString cfg.port}
114 reuseport: ${yesOrNo cfg.reuseport}
115 round-robin: ${yesOrNo cfg.roundRobin}
116 server-count: ${toString cfg.serverCount}
117 ${maybeToString "statistics: " cfg.statistics}
118 tcp-count: ${toString cfg.tcpCount}
119 tcp-query-count: ${toString cfg.tcpQueryCount}
120 tcp-timeout: ${toString cfg.tcpTimeout}
121 verbosity: ${toString cfg.verbosity}
122 ${maybeString "version: " cfg.version}
123 xfrd-reload-timeout: ${toString cfg.xfrdReloadTimeout}
124 zonefiles-check: ${yesOrNo cfg.zonefilesCheck}
125 zonefiles-write: ${toString cfg.zonefilesWrite}
127 ${maybeString "rrl-ipv4-prefix-length: " cfg.ratelimit.ipv4PrefixLength}
128 ${maybeString "rrl-ipv6-prefix-length: " cfg.ratelimit.ipv6PrefixLength}
129 rrl-ratelimit: ${toString cfg.ratelimit.ratelimit}
130 ${maybeString "rrl-slip: " cfg.ratelimit.slip}
131 rrl-size: ${toString cfg.ratelimit.size}
132 rrl-whitelist-ratelimit: ${toString cfg.ratelimit.whitelistRatelimit}
137 control-enable: ${yesOrNo cfg.remoteControl.enable}
138 control-key-file: "${cfg.remoteControl.controlKeyFile}"
139 control-cert-file: "${cfg.remoteControl.controlCertFile}"
140 ${forEach " control-interface: " cfg.remoteControl.interfaces}
141 control-port: ${toString cfg.remoteControl.port}
142 server-key-file: "${cfg.remoteControl.serverKeyFile}"
143 server-cert-file: "${cfg.remoteControl.serverCertFile}"
145 ${concatStrings (mapAttrsToList zoneConfigFile zoneConfigs)}
150 yesOrNo = b: if b then "yes" else "no";
151 maybeString = prefix: x: optionalString (x != null) ''${prefix} "${x}"'';
152 maybeToString = prefix: x: optionalString (x != null) ''${prefix} ${toString x}'';
153 forEach = pre: l: concatMapStrings (x: pre + x + "\n") l;
155 keyConfigFile = concatStrings (
156 mapAttrsToList (keyName: keyOptions: ''
159 algorithm: "${keyOptions.algorithm}"
160 include: "${stateDir}/private/${keyName}"
164 copyKeys = concatStrings (
165 mapAttrsToList (keyName: keyOptions: ''
166 secret=$(cat "${keyOptions.keyFile}")
167 dest="${stateDir}/private/${keyName}"
168 install -m 0400 -o "${username}" -g "${username}" <(echo " secret: \"$secret\"") "$dest"
172 # options are ordered alphanumerically by the nixos option name
173 zoneConfigFile = name: zone: ''
176 zonefile: "${stateDir}/zones/${mkZoneFileName name}"
177 ${maybeString "outgoing-interface: " zone.outgoingInterface}
178 ${forEach " rrl-whitelist: " zone.rrlWhitelist}
179 ${maybeString "zonestats: " zone.zoneStats}
181 ${maybeToString "max-refresh-time: " zone.maxRefreshSecs}
182 ${maybeToString "min-refresh-time: " zone.minRefreshSecs}
183 ${maybeToString "max-retry-time: " zone.maxRetrySecs}
184 ${maybeToString "min-retry-time: " zone.minRetrySecs}
186 allow-axfr-fallback: ${yesOrNo zone.allowAXFRFallback}
187 multi-master-check: ${yesOrNo zone.multiMasterCheck}
188 ${forEach " allow-notify: " zone.allowNotify}
189 ${forEach " request-xfr: " zone.requestXFR}
191 ${forEach " notify: " zone.notify}
192 notify-retry: ${toString zone.notifyRetry}
193 ${forEach " provide-xfr: " zone.provideXFR}
196 zoneConfigs = zoneConfigs' { } "" { children = cfg.zones; };
201 !(zone ? children) || zone.children == null || zone.children == { }
202 # leaf -> actual zone
204 listToAttrs [ (nameValuePair name (parent // zone)) ]
208 zipAttrsWith (name: head) (
210 name: child: zoneConfigs' (parent // zone // { children = { }; }) name child
214 # options are ordered alphanumerically
215 zoneOptions = types.submodule {
218 allowAXFRFallback = mkOption {
222 If NSD as secondary server should be allowed to AXFR if the primary
223 server does not allow IXFR.
227 allowNotify = mkOption {
228 type = types.listOf types.str;
232 "10.0.0.1-10.0.0.5 my_tsig_key_name"
233 "10.0.3.4&255.255.0.0 BLOCKED"
236 Listed primary servers are allowed to notify this secondary server.
238 Format: `<ip> <key-name | NOKEY | BLOCKED>`
240 `<ip>` either a plain IPv4/IPv6 address or range.
241 Valid patters for ranges:
242 * `10.0.0.0/24`: via subnet size
243 * `10.0.0.0&255.255.255.0`: via subnet mask
244 * `10.0.0.1-10.0.0.254`: via range
246 A optional port number could be added with a '@':
247 * `2001:1234::1@1234`
249 `<key-name | NOKEY | BLOCKED>`
250 * `<key-name>` will use the specified TSIG key
251 * `NOKEY` no TSIG signature is required
252 * `BLOCKED`notifies from non-listed or blocked IPs will be ignored
256 children = mkOption {
257 # TODO: This relies on the fact that `types.anything` doesn't set any
258 # values of its own to any defaults, because in the above zoneConfigs',
259 # values from children override ones from parents, but only if the
260 # attributes are defined. Because of this, we can't replace the element
261 # type here with `zoneConfigs`, since that would set all the attributes
262 # to default values, breaking the parent inheriting function.
263 type = types.attrsOf types.anything;
266 Children zones inherit all options of their parents. Attributes
267 defined in a child will overwrite the ones of its parent. Only
268 leaf zones will be actually served. This way it's possible to
269 define maybe zones which share most attributes without
270 duplicating everything. This mechanism replaces nsd's patterns
271 in a save and functional way.
279 The actual zone data. This is the content of your zone file.
280 Use imports or pkgs.lib.readFile if you don't want this data in your config file.
284 dnssec = mkEnableOption "DNSSEC";
287 algorithm = mkOption {
289 default = "RSASHA256";
290 description = "Which algorithm to use for DNSSEC";
295 description = "TTL for dnssec records";
297 coverage = mkOption {
301 The length of time to ensure that keys will be correct; no action will be taken to create new keys to be activated after this time.
312 description = "Key policy for zone signing keys";
322 description = "Key policy for key signing keys";
326 maxRefreshSecs = mkOption {
327 type = types.nullOr types.int;
330 Limit refresh time for secondary zones. This is the timer which
331 checks to see if the zone has to be refetched when it expires.
332 Normally the value from the SOA record is used, but this option
333 restricts that value.
337 minRefreshSecs = mkOption {
338 type = types.nullOr types.int;
341 Limit refresh time for secondary zones.
345 maxRetrySecs = mkOption {
346 type = types.nullOr types.int;
349 Limit retry time for secondary zones. This is the timeout after
350 a failed fetch attempt for the zone. Normally the value from
351 the SOA record is used, but this option restricts that value.
355 minRetrySecs = mkOption {
356 type = types.nullOr types.int;
359 Limit retry time for secondary zones.
363 multiMasterCheck = mkOption {
367 If enabled, checks all masters for the last zone version.
368 It uses the higher version from all configured masters.
369 Useful if you have multiple masters that have different version numbers served.
374 type = types.listOf types.str;
377 "10.0.0.1@3721 my_key"
381 This primary server will notify all given secondary servers about
384 Format: `<ip> <key-name | NOKEY>`
386 `<ip>` a plain IPv4/IPv6 address with on optional port number (ip@port)
389 - `<key-name>` sign notifies with the specified key
390 - `NOKEY` don't sign notifies
394 notifyRetry = mkOption {
398 Specifies the number of retries for failed notifies. Set this along with notify.
402 outgoingInterface = mkOption {
403 type = types.nullOr types.str;
405 example = "2000::1@1234";
407 This address will be used for zone-transfer requests if configured
408 as a secondary server or notifications in case of a primary server.
409 Supply either a plain IPv4 or IPv6 address with an optional port
414 provideXFR = mkOption {
415 type = types.listOf types.str;
419 "192.0.2.0/24 my_tsig_key_name"
422 Allow these IPs and TSIG to transfer zones, addr TSIG|NOKEY|BLOCKED
423 address range 192.0.2.0/24, 1.2.3.4&255.255.0.0, 3.0.2.20-3.0.2.40
427 requestXFR = mkOption {
428 type = types.listOf types.str;
431 Format: `[AXFR|UDP] <ip-address> <key-name | NOKEY>`
435 rrlWhitelist = mkOption {
452 Whitelists the given rrl-types.
456 zoneStats = mkOption {
457 type = types.nullOr types.str;
461 When set to something distinct to null NSD is able to collect
462 statistics per zone. All statistics of this zone(s) will be added
463 to the group specified by this given name. Use "%s" to use the zones
464 name as the group. The groups are output from nsd-control stats
471 keyPolicy = types.submodule {
475 description = "Key size in bits";
477 prePublish = mkOption {
479 description = "How long in advance to publish new keys";
481 postPublish = mkOption {
483 description = "How long after deactivation to keep a key in the zone";
485 rollPeriod = mkOption {
487 description = "How frequently to change keys";
492 dnssecZones = (filterAttrs (n: v: if v ? dnssec then v.dnssec else false) zoneConfigs);
494 dnssec = dnssecZones != { };
496 dnssecTools = pkgs.bind.override { enablePython = true; };
498 signZones = optionalString dnssec ''
499 install -m 0600 -o "${username}" -g "${username}" -d "${stateDir}/dnssec"
501 ${concatStrings (mapAttrsToList signZone dnssecZones)}
503 signZone = name: zone: ''
504 ${dnssecTools}/bin/dnssec-keymgr -g ${dnssecTools}/bin/dnssec-keygen -s ${dnssecTools}/bin/dnssec-settime -K ${stateDir}/dnssec -c ${policyFile name zone.dnssecPolicy} ${name}
505 ${dnssecTools}/bin/dnssec-signzone -S -K ${stateDir}/dnssec -o ${name} -O full -N date ${stateDir}/zones/${name}
506 ${nsdPkg}/sbin/nsd-checkzone ${name} ${stateDir}/zones/${name}.signed && mv -v ${stateDir}/zones/${name}.signed ${stateDir}/zones/${name}
510 pkgs.writeText "${name}.policy" ''
512 algorithm ${policy.algorithm};
513 key-size zsk ${toString policy.zsk.keySize};
514 key-size ksk ${toString policy.ksk.keySize};
515 keyttl ${policy.keyttl};
516 pre-publish zsk ${policy.zsk.prePublish};
517 pre-publish ksk ${policy.ksk.prePublish};
518 post-publish zsk ${policy.zsk.postPublish};
519 post-publish ksk ${policy.ksk.postPublish};
520 roll-period zsk ${policy.zsk.rollPeriod};
521 roll-period ksk ${policy.ksk.rollPeriod};
522 coverage ${policy.coverage};
527 # options are ordered alphanumerically
528 options.services.nsd = {
530 enable = mkEnableOption "NSD authoritative DNS server";
532 bind8Stats = mkEnableOption "BIND8 like statistics";
534 dnssecInterval = mkOption {
538 How often to check whether dnssec key rollover is required
542 extraConfig = mkOption {
550 hideVersion = mkOption {
554 Whether NSD should answer VERSION.BIND and VERSION.SERVER CHAOS class queries.
558 identity = mkOption {
560 default = "unidentified server";
562 Identify the server (CH TXT ID.SERVER entry).
566 interfaces = mkOption {
567 type = types.listOf types.str;
573 What addresses the server should listen to.
577 ipFreebind = mkOption {
581 Whether to bind to nonlocal addresses and interfaces that are down.
582 Similar to ip-transparent.
586 ipTransparent = mkOption {
590 Allow binding to non local addresses.
598 Whether to listen on IPv4 connections.
602 ipv4EDNSSize = mkOption {
606 Preferred EDNS buffer size for IPv4.
614 Whether to listen on IPv6 connections.
618 ipv6EDNSSize = mkOption {
622 Preferred EDNS buffer size for IPv6.
626 logTimeAscii = mkOption {
630 Log time in ascii, if false then in unix epoch seconds.
635 type = types.nullOr types.str;
638 NSID identity (hex string, or "ascii_somestring").
646 Port the service should bind do.
650 reuseport = mkOption {
652 default = pkgs.stdenv.hostPlatform.isLinux;
653 defaultText = literalExpression "pkgs.stdenv.hostPlatform.isLinux";
655 Whether to enable SO_REUSEPORT on all used sockets. This lets multiple
656 processes bind to the same port. This speeds up operation especially
657 if the server count is greater than one and makes fast restarts less
662 rootServer = mkOption {
666 Whether this server will be a root server (a DNS root server, you
667 usually don't want that).
671 roundRobin = mkEnableOption "round robin rotation of records";
673 serverCount = mkOption {
677 Number of NSD servers to fork. Put the number of CPUs to use here.
681 statistics = mkOption {
682 type = types.nullOr types.int;
685 Statistics are produced every number of seconds. Prints to log.
686 If null no statistics are logged.
690 tcpCount = mkOption {
694 Maximum number of concurrent TCP connections per server.
698 tcpQueryCount = mkOption {
702 Maximum number of queries served on a single TCP connection.
707 tcpTimeout = mkOption {
711 TCP timeout in seconds.
715 verbosity = mkOption {
724 type = types.nullOr types.str;
727 The version string replied for CH TXT version.server and version.bind
728 queries. Will use the compiled package version on null.
729 See hideVersion for enabling/disabling this responses.
733 xfrdReloadTimeout = mkOption {
737 Number of seconds between reloads triggered by xfrd.
741 zonefilesCheck = mkOption {
745 Whether to check mtime of all zone files on start and sighup.
749 zonefilesWrite = mkOption {
753 Write changed secondary zones to their zonefile every N seconds.
754 If the zone (pattern) configuration has "" zonefile, it is not written.
755 Zones that have received zone transfer updates are written to their zonefile.
756 0 disables writing to zone files.
761 type = types.attrsOf (
765 algorithm = mkOption {
767 default = "hmac-sha256";
769 Authentication algorithm for this key.
776 Path to the file which contains the actual base64 encoded
777 key. The key will be copied into "${stateDir}/private" before
778 NSD starts. The copied file is only accessibly by the NSD
787 example = literalExpression ''
788 { "tsig.example.org" = {
789 algorithm = "hmac-md5";
790 keyFile = "/path/to/my/key";
795 Define your TSIG keys here.
801 enable = mkEnableOption "ratelimit capabilities";
803 ipv4PrefixLength = mkOption {
804 type = types.nullOr types.int;
807 IPv4 prefix length. Addresses are grouped by netblock.
811 ipv6PrefixLength = mkOption {
812 type = types.nullOr types.int;
815 IPv6 prefix length. Addresses are grouped by netblock.
819 ratelimit = mkOption {
823 Max qps allowed from any query source.
824 0 means unlimited. With an verbosity of 2 blocked and
825 unblocked subnets will be logged.
830 type = types.nullOr types.int;
833 Number of packets that get discarded before replying a SLIP response.
834 0 disables SLIP responses. 1 will make every response a SLIP response.
842 Size of the hashtable. More buckets use more memory but lower
843 the chance of hash hash collisions.
847 whitelistRatelimit = mkOption {
851 Max qps allowed from whitelisted sources.
852 0 means unlimited. Set the rrl-whitelist option for specific
853 queries to apply this limit instead of the default to them.
861 enable = mkEnableOption "remote control via nsd-control";
863 controlCertFile = mkOption {
865 default = "/etc/nsd/nsd_control.pem";
867 Path to the client certificate signed with the server certificate.
868 This file is used by nsd-control and generated by nsd-control-setup.
872 controlKeyFile = mkOption {
874 default = "/etc/nsd/nsd_control.key";
876 Path to the client private key, which is used by nsd-control
877 but not by the server. This file is generated by nsd-control-setup.
881 interfaces = mkOption {
882 type = types.listOf types.str;
888 Which interfaces NSD should bind to for remote control.
896 Port number for remote control operations (uses TLS over TCP).
900 serverCertFile = mkOption {
902 default = "/etc/nsd/nsd_server.pem";
904 Path to the server self signed certificate, which is used by the server
905 but and by nsd-control. This file is generated by nsd-control-setup.
909 serverKeyFile = mkOption {
911 default = "/etc/nsd/nsd_server.key";
913 Path to the server private key, which is used by the server
914 but not by nsd-control. This file is generated by nsd-control-setup.
921 type = types.attrsOf zoneOptions;
923 example = literalExpression ''
925 provideXFR = [ "10.1.2.3 NOKEY" ];
931 @ IN SOA a.ns.example.com. admin.example.com. (
939 @ IN SOA a.ns.example.com. admin.example.com. (
947 provideXFR = [ "10.3.2.1 NOKEY" ];
955 Define your zones here. Zones can cascade other zones and therefore
956 inherit settings from parent zones. Look at the definition of
957 children to learn about inheritance and child zones.
958 The given example will define 3 zones (example.(com|org|net).). Both
959 example.com. and example.org. inherit their configuration from
965 config = mkIf cfg.enable {
967 assertions = singleton {
968 assertion = zoneConfigs ? "." -> cfg.rootServer;
970 "You have a root zone configured. If this is really what you "
971 + "want, please enable 'services.nsd.rootServer'.";
975 systemPackages = [ nsdPkg ];
976 etc."nsd/nsd.conf".source = "${configFile}/nsd.conf";
979 users.groups.${username}.gid = config.ids.gids.nsd;
981 users.users.${username} = {
982 description = "NSD service user";
985 uid = config.ids.uids.nsd;
989 systemd.services.nsd = {
990 description = "NSD authoritative only domain name service";
992 after = [ "network.target" ];
993 wantedBy = [ "multi-user.target" ];
996 startLimitIntervalSec = 5 * 60; # 5 mins
998 ExecStart = "${nsdPkg}/sbin/nsd -d -c ${nsdEnv}/nsd.conf";
999 StandardError = "null";
1006 rm -Rf "${stateDir}/private/"
1007 rm -Rf "${stateDir}/tmp/"
1009 install -dm 0700 -o "${username}" -g "${username}" "${stateDir}/private"
1010 install -dm 0700 -o "${username}" -g "${username}" "${stateDir}/tmp"
1011 install -dm 0700 -o "${username}" -g "${username}" "${stateDir}/var"
1013 cat > "${stateDir}/don't touch anything in here" << EOF
1014 Everything in this directory except NSD's state in var and dnssec
1015 is automatically generated and will be purged and redeployed by
1016 the nsd.service pre-start script.
1019 rm -rf "${stateDir}/zones"
1020 cp -rL "${nsdEnv}/zones" "${stateDir}/zones"
1026 systemd.timers.nsd-dnssec = mkIf dnssec {
1027 description = "Automatic DNSSEC key rollover";
1029 wantedBy = [ "nsd.service" ];
1032 OnActiveSec = cfg.dnssecInterval;
1033 OnUnitActiveSec = cfg.dnssecInterval;
1037 systemd.services.nsd-dnssec = mkIf dnssec {
1038 description = "DNSSEC key rollover";
1040 wantedBy = [ "nsd.service" ];
1041 before = [ "nsd.service" ];
1046 /run/current-system/systemd/bin/systemctl kill -s SIGHUP nsd.service
1052 meta.maintainers = with lib.maintainers; [ hrdinka ];