nixos-config/config/service/authoritative-dns.nix

339 lines
10 KiB
Nix

{ config, lib, pkgs, ... }@toplevel:
with lib;
let
hostname = config.instance.hostname;
cfg = config.fudo.services.authoritative-dns;
hostSecrets = config.fudo.secrets.host-secrets."${hostname}";
domainName = config.instance.local-domain;
# domain = config.fudo.domains."${domainName}";
# primaryNameserver = domain.primaryNameserver;
# isPrimaryNameserver = primary-nameserver == hostname;
zoneKeySecret = zone: "${zone}-ksk";
nameserverOpts = { name, ... }: {
options = with types; {
hostname = mkOption {
type = str;
description = "Hostname of the external nameserver.";
default = name;
};
ipv4-address = mkOption {
type = nullOr str;
description = "Host ipv4 address of the external nameserver.";
default = null;
};
ipv6-address = mkOption {
type = nullOr str;
description = "Host ipv6 address of the external nameserver.";
default = null;
};
authoritative-hostname = mkOption {
type = nullOr str;
description = "Authoritative hostname of this nameserver.";
default = null;
};
description = mkOption {
type = str;
description = "Description of the external nameserver.";
};
};
};
networkHostOpts = {
options = with types; {
hostname = mkOption {
type = str;
description = "Hostname.";
};
ipv4-address = mkOption {
type = nullOr str;
description = "The V4 IP of a given host, if any.";
default = null;
};
ipv6-address = mkOption {
type = nullOr str;
description = "The V6 IP of a given host, if any.";
default = null;
};
mac-address = mkOption {
type = nullOr str;
description =
"The MAC address of a given host, if desired for IP reservation.";
default = null;
};
description = mkOption {
type = nullOr str;
description = "Description of the host.";
default = null;
};
sshfp-records = mkOption {
type = listOf str;
description = "List of SSHFP records for this host.";
default = [ ];
};
};
};
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 networkHostOpts);
description =
"Host which will respond to requests for the base domain.";
default = null;
};
external-nameservers = mkOption {
type = listOf (submodule nameserverOpts);
description = "List of external nameserver clauses.";
default = [ ];
};
domain = mkOption {
type = str;
description = "Domain which this zone serves.";
default = zoneName;
};
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.";
zones = mkOption {
type = attrsOf (submodule zoneOpts);
description = "Map of served zone to extra zone details.";
default = { };
};
nameservers = {
primary = mkOption {
type = str;
description = "Hostname of the primary nameserver.";
};
secondary = mkOption {
type = listOf str;
description = "List of internal secondary nameservers.";
default = [ ];
};
};
state-directory = mkOption {
type = str;
description =
"Directory at which to store DNS state data, including keys.";
};
};
config = {
fudo = {
secrets.host-secrets."${hostname}" = mkIf cfg.enable (mapAttrs'
(zone: zoneCfg:
nameValuePair (zoneKeySecret zone) {
source-file = zoneCfg.ksk.private-key;
target-file = "/run/nsd/${baseNameOf zoneCfg.ksk.private-key}";
user = config.fudo.nsd.user;
}) (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; };
# servedDomain = domain.primary-nameserver != null;
# primaryNameserver = domain.primary-nameserver;
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 = let
internalNameservers = map getNsDeets internalNameserverHostnames;
in internalNameservers ++ zoneCfg.external-nameservers;
hasAuthHostname = nsHost: nsOpts:
(hasAttr "authoritative-hostname" nsOpts)
&& (nsOpts.authoritative-hostname != null);
allNameservers = listToAttrs
(imap1 (i: nsOpts: nameValuePair "ns${toString i}" nsOpts)
nameserverDeets);
nameserverAliases =
mapAttrs (hostname: opts: "${opts.authoritative-hostname}.")
(filterAttrs hasAuthHostname allNameservers);
nameserverHosts = mapAttrs (hostname: opts: {
inherit (opts) ipv4-address ipv6-address description;
}) (filterAttrs (hostname: opts: !hasAuthHostname hostname opts)
allNameservers);
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 = mkIf (!isNull domain.primary-mailserver) (let
mailDomainName =
config.fudo.hosts."${domain.primary-mailserver}".domain;
in {
tcp = {
smtp = [{
host = "smtp.${mailDomainName}";
port = 25;
}];
imap = [{
host = "imap.${mailDomainName}";
port = 143;
}];
imaps = [{
host = "imap.${mailDomainName}";
port = 993;
}];
submission = [{
host = "smtp.${mailDomainName}";
port = 587;
}];
submissions = [{
host = "smtp.${mailDomainName}";
port = 465;
}];
};
udp = {
smtp = [{
host = "smtp.${mailDomainName}";
port = 25;
}];
submission = [{
host = "smtp.${mailDomainName}";
port = 587;
}];
};
});
in {
gssapi-realm = mkIf (!isNull domain.gssapi-realm) domain.gssapi-realm;
hosts = nameserverHosts;
# Don't add mailservers: remember SSL!
aliases = nameserverAliases;
mx = optional (!isNull domain.primary-mailserver) (let
mailserverDomain =
config.fudo.hosts."${domain.primary-mailserver}".domain;
in "smtp.${mailserverDomain}");
dmarc-report-address = mkIf (!isNull domain.primary-mailserver)
"dmarc-report@${domainName}";
nameservers = let
directExternal = attrValues nameserverAliases;
internal = map (hostname: "${hostname}.${domainName}.")
(attrNames nameserverHosts);
in internal ++ directExternal;
srv-records = dnsSrvRecords // mailSrvRecords;
verbatim-dns-records = mkIf (zoneCfg.ksk != null) [
(readFile zoneCfg.ksk.public-key)
(readFile zoneCfg.ksk.ds)
];
}) cfg.zones;
};
services = {
authoritative-dns = {
enable = cfg.enable;
identity = "${hostname}.${domainName}";
listen-ips =
optionals cfg.enable (pkgs.lib.network.host-ips config hostname);
state-directory = cfg.state-directory;
timestamp = toString config.instance.build-timestamp;
domains = mapAttrs' (zoneName: zoneCfg:
nameValuePair zoneCfg.domain {
ksk.key-file = mkIf (hasAttr (zoneKeySecret zoneName) hostSecrets)
hostSecrets."${zoneKeySecret zoneName}".target-file;
zone = let baseZone = config.fudo.zones."${zoneName}";
in baseZone // {
# FIXME: what's up?
# default-host = baseZone.hosts."${zoneCfg.default-host}";
};
}) cfg.zones;
};
};
};
}