nixos-config/config/service/authoritative-dns.nix
2024-05-21 23:16:02 -07:00

383 lines
12 KiB
Nix

{ config, lib, pkgs, ... }@toplevel:
with lib;
let
hostname = config.instance.hostname;
cfg = config.fudo.services.authoritative-dns;
inherit (pkgs.lib)
getHostIps getSiteGatewayV4 getSiteV4PrefixLength getSiteV6PrefixLength;
hostSecrets = config.fudo.secrets.host-secrets."${hostname}";
domainName = config.instance.local-domain;
primaryZone = config.fudo.domains."${domainName}".zone;
siteName = config.instance.local-site;
zoneKeySecret = zone: "${zone}-ksk";
containerModule = { pkgs, config, ... }: {
config = mkIf (cfg.enable && !isNull cfg.container) {
containers.nameserver = let
securedZones =
filterAttrs (_: zoneOpts: !isNull zoneOpts.ksk) cfg.zones;
in {
autoStart = true;
additionalCapabilities = [ "CAP_NET_ADMIN" ];
macvlans = [ cfg.container.interface ];
bindMounts = {
"/var/lib/nsd" = {
hostPath = cfg.state-directory;
isReadOnly = false;
};
} // (mapAttrs' (zoneName: _:
nameValuePair "/run/nsd/keys/${zoneName}" {
hostPath =
dirOf hostSecrets."${zoneKeySecret zoneName}".target-file;
}) securedZones);
config = let
nameserverHost = cfg.container.hostname;
nameserverDeets =
config.fudo.zones."${primaryZone}".hosts."${nameserverHost}";
in {
imports = [ pkgs.moduleRegistry.authoritativeDns ];
nixpkgs.pkgs = pkgs;
networking = {
enableIPv6 = false;
defaultGateway = {
address = getSiteGatewayV4 siteName;
interface = "mv-${cfg.container.interface}";
};
firewall = {
enable = true;
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
interfaces."mv-${cfg.container.interface}" = {
ipv4.addresses = optional (nameserverDeets.ipv4-address != null) {
address = nameserverDeets.ipv4-address;
prefixLength = getSiteV4PrefixLength siteName;
};
ipv6.addresses = optional (nameserverDeets.ipv6-address != null) {
address = nameserverDeets.ipv6-address;
prefixLength = getSiteV6PrefixLength siteName;
};
};
};
services.authoritative-dns = {
enable = true;
identity = "${nameserverHost}.${primaryZone}";
listen-ips = getHostIps nameserverHost;
state-directory = "/var/lib/nsd";
timestamp = toString config.instance.build-timestamp;
ip-host-map = cfg.ip-host-map;
domains = mapAttrs' (zoneName: zoneCfg:
nameValuePair zoneCfg.domain {
ksk.key-file = "/run/nsd/keys/${zoneName}/${
baseNameOf
hostSecrets."${zoneKeySecret zoneName}".target-file
}";
reverse-zones = zoneCfg.reverse-zones;
notify = mkIf cfg.enable-notifications {
ipv4 = concatMap
(ns: optional (ns.ipv4-address != null) ns.ipv4-address)
cfg.nameservers.external;
ipv6 = concatMap
(ns: optional (ns.ipv6-address != null) ns.ipv6-address)
cfg.nameservers.external;
};
zone = config.fudo.zones."${zoneName}";
}) cfg.zones;
};
};
};
};
};
hostModule = { config, ... }: {
config.services.authoritative-dns =
mkIf (cfg.enable && isNull cfg.container) {
enable = true;
identity = "${hostname}.${primaryZone}";
listen-ips = getHostIps hostname;
state-directory = cfg.state-directory;
timestamp = toString config.instance.build-timestamp;
ip-host-map = cfg.ip-host-map;
domains = mapAttrs' (zoneName: zoneCfg:
nameValuePair zoneCfg.domain {
ksk.key-file = mkIf (hasAttr (zoneKeySecret zoneName) hostSecrets)
hostSecrets."${zoneKeySecret zoneName}".target-file;
reverse-zones = zoneCfg.reverse-zones;
notify = mkIf cfg.enable-notifications {
ipv4 = concatMap
(ns: optional (ns.ipv4-address != null) ns.ipv4-address)
cfg.nameservers.external;
ipv6 = concatMap
(ns: optional (ns.ipv6-address != null) ns.ipv6-address)
cfg.nameservers.external;
};
zone = config.fudo.zones."${zoneName}";
}) cfg.zones;
};
};
zoneOpts = { name, ... }:
let zoneName = name;
in {
options = with types; {
enable = mkOption {
type = bool;
description = "Enable ${zone-name} zone on the local nameserver.";
default = true;
};
default-host = mkOption {
type = nullOr (submodule pkgs.lib.fudo-types.networkHost);
description =
"Host which will respond to requests for the base domain.";
default = null;
};
domain = mkOption {
type = str;
description = "Domain which this zone serves.";
default = zoneName;
};
reverse-zones = mkOption {
type = listOf str;
description = "List of networks for which to generate reverse zones.";
default = [ ];
};
mail = {
smtp-servers = mkOption {
type = listOf str;
description = "List of SMTP server for this zone.";
default = [ ];
};
imap-servers = mkOption {
type = listOf str;
description = "List of IMAP server for this zone.";
default = [ ];
};
};
ksk = mkOption {
type = nullOr (submodule {
options = {
private-key = mkOption {
type = path;
description = "KSK private key.";
};
public-key = mkOption {
type = path;
description = "KSK public key.";
};
ds = mkOption {
type = path;
description = "KSK ds record.";
};
};
});
description =
"Location of the zone-signing private & public keys and DS record.";
default =
toplevel.config.fudo.secrets.files.dns.key-signing-keys."${zoneName}";
};
};
};
in {
options.fudo.services.authoritative-dns = with types; {
enable = mkEnableOption "Enable Authoritative DNS server.";
container = mkOption {
type = nullOr (submodule {
options = {
interface = mkOption {
type = str;
description = "Interface on which to listen for DNS traffic.";
};
hostname = mkOption {
type = str;
description = ''
Hostname (in the zone) of the container nameserver.
The associated IP(s) will be assigned to the container,
and must be accessible.
'';
};
};
});
default = null;
};
enable-notifications =
mkEnableOption "Enable notifications to secondary servers.";
zones = mkOption {
type = attrsOf (submodule zoneOpts);
description = "Map of served zone to extra zone details.";
default = { };
};
ip-host-map = mkOption {
type = attrsOf str;
description =
"Map of ip address to authoritative hostname, for reverse zones.";
default = { };
};
nameservers = {
primary = mkOption {
type = str;
description = "Hostname of the primary nameserver.";
};
secondary = mkOption {
type = listOf str;
description = "List of internal secondary nameservers.";
default = [ ];
};
external = mkOption {
type = listOf (submodule pkgs.lib.fudo-types.networkHost);
description = "List of external secondary nameserver attributes.";
default = [ ];
};
};
state-directory = mkOption {
type = str;
description =
"Directory at which to store DNS state data, including keys.";
};
};
imports = [ hostModule containerModule ];
config = mkIf cfg.enable {
fileSystems."/var/lib/nsd" = mkIf (isNull cfg.container) {
device = cfg.state-directory;
options = [ "bind" ];
};
fudo = {
secrets.host-secrets."${hostname}" = concatMapAttrs (zone: zoneCfg: {
"${zoneKeySecret zone}" = {
source-file = zoneCfg.ksk.private-key;
target-file =
"/run/nsd/${zone}/${baseNameOf zoneCfg.ksk.private-key}";
};
"${zone}-ds" = {
source-file = zoneCfg.ksk.ds;
target-file = "/run/nsd/${zone}/${baseNameOf zoneCfg.ksk.ds}";
};
"${zone}-pubkey" = {
source-file = zoneCfg.ksk.public-key;
target-file = "/run/nsd/${zone}/${baseNameOf zoneCfg.ksk.public-key}";
};
}) (filterAttrs (_: zoneCfg: zoneCfg.ksk != null) cfg.zones);
zones = mapAttrs (zone-name: zoneCfg:
let
domainName = zoneCfg.domain;
domain = config.fudo.domains."${domainName}";
makeSrvRecord = port: host: { inherit port host; };
isPrimaryNameserver = hostname == cfg.nameservers.primary;
internalNameserverHostnames = [ cfg.nameservers.primary ]
++ cfg.nameservers.secondary;
getNsDeets = hostname:
let hostDomain = config.fudo.hosts."${hostname}".domain;
in {
ipv4-address = pkgs.lib.network.host-ipv4 config hostname;
ipv6-address = pkgs.lib.network.host-ipv6 config hostname;
description =
"${domainName} nameserver ${hostname}.${hostDomain}.";
};
nameserverDeets = (map getNsDeets internalNameserverHostnames)
++ cfg.nameservers.external;
allNameservers = listToAttrs
(imap1 (i: nsOpts: nameValuePair "ns${toString i}" nsOpts)
nameserverDeets);
dnsSrvRecords = let
nameserverSrvRecords = mapAttrsToList (hostname: hostOpts:
let
targetHost = if (hasAuthHostname hostname hostOpts) then
"${hostOpts.authoritative-hostname}"
else
"${hostname}.${domainName}";
in makeSrvRecord 53 targetHost) allNameservers;
in {
tcp.domain = nameserverSrvRecords;
udp.domain = nameserverSrvRecords;
};
mailSrvRecords = let
mkRecords = port: servers:
map (host: { inherit host port; }) servers;
in {
tcp = {
smtp = mkRecords 25 zoneCfg.mail.smtp-servers;
submission = mkRecords 587 zoneCfg.mail.smtp-servers;
submissions = mkRecords 465 zoneCfg.mail.smtp-servers;
imap = mkRecords 143 zoneCfg.mail.imap-servers;
imaps = mkRecords 993 zoneCfg.mail.imap-servers;
};
udp = {
smtp = mkRecords 25 zoneCfg.mail.smtp-servers;
submission = mkRecords 587 zoneCfg.mail.smtp-servers;
};
};
in {
gssapi-realm = mkIf (!isNull domain.gssapi-realm) domain.gssapi-realm;
hosts = allNameservers;
mx = zoneCfg.mail.smtp-servers;
dmarc-report-address = "dmarc-report@${domainName}";
default-host = zoneCfg.default-host;
nameservers = map (hostname: "${hostname}.${domainName}.")
(attrNames allNameservers);
srv-records = dnsSrvRecords // mailSrvRecords;
verbatim-dns-records = mkIf (zoneCfg.ksk != null) [
(readFile zoneCfg.ksk.public-key)
(readFile zoneCfg.ksk.ds)
];
}) cfg.zones;
};
};
}