{ lib, pkgs, config, dns-nix, ... }: let zonesWithDNSSec = lib.filterAttrs (k: v: v.dnssec.enable) config.myServices.dns.zones; zoneToFile = name: v: pkgs.runCommand "${name}.zone" { text = v; passAsFile = [ "text" ]; # Automatically change the increment when relevant change # happened (both serial and mta-sts) } '' mv "$textPath" $out increment=$(( 100*($(date -u +%-H) * 60 + $(date -u +%-M))/1440 )) sed -i -e "s/2022121902/$(date -u +%Y%m%d)$increment/g" $out sed -i -e "s/20200109150200Z/$(date -u +%Y%m%d%H%M%SZ)/g" $out ''; in { options.myServices.dns = { enable = lib.mkEnableOption "enable DNS resolver"; helpers = lib.mkOption { readOnly = true; description = '' Some useful constants or functions for zones definition ''; default = rec { servers = config.myEnv.servers; ips = i: { A = i.ip4; AAAA = i.ip6; }; letsencrypt = [ { tag = "issue"; value = "letsencrypt.org"; issuerCritical = false; } ]; toKV = a: let removeOrder = n: lib.last (builtins.split "__" n); in builtins.concatStringsSep ";" (builtins.attrValues (builtins.mapAttrs (n: v: "${removeOrder n}=${v}") a)); mailMX = { hasEmail = true; subdomains = let mxes = lib.filterAttrs (n: v: v ? mx && v.mx.enable) servers; in lib.mapAttrs' (n: v: lib.nameValuePair v.mx.subdomain (ips v.ips.main)) mxes; }; zoneHeader = { TTL = 3*60*60; SOA = { # yyyymmdd?? (increment ?? at each change) serial = 2022121902; # Don't change this value, it is replaced automatically! refresh = 3*60*60; retry = 60*60; expire = 14*24*60*60; minimum = 3*60*60; # negative cache ttl adminEmail = "hostmaster@immae.eu"; #email-address s/@/./ nameServer = "ns1.immae.eu."; }; }; mailSend = { # DKIM 2048b subdomains._domainkey.subdomains.eldiron2.TXT = [ (toKV config.myEnv.mail.dkim.eldiron2.public) ]; # DKIM 1024b subdomains._domainkey.subdomains.eldiron.TXT = [ (toKV config.myEnv.mail.dkim.eldiron.public) ]; # old key, may still be used by verifiers subdomains._domainkey.subdomains.immae_eu.TXT = [ (toKV config.myEnv.mail.dkim.immae_eu.public) ]; }; mailCommon = name: quarantine: { MX = let mxes = lib.filterAttrs (n: v: v ? mx && v.mx.enable) servers; in lib.mapAttrsToList (n: v: { preference = v.mx.priority; exchange = "${v.mx.subdomain}.${name}."; }) mxes; # https://tools.ietf.org/html/rfc6186 SRV = [ { service = "submission"; proto = "tcp"; priority = 0; weight = 1; port = 587; target = "smtp.immae.eu."; } { service = "submissions"; proto = "tcp"; priority = 0; weight = 1; port = 465; target = "smtp.immae.eu."; } { service = "imap"; proto = "tcp"; priority = 0; weight = 1; port = 143; target = "imap.immae.eu."; } { service = "imaps"; proto = "tcp"; priority = 0; weight = 1; port = 993; target = "imap.immae.eu."; } { service = "sieve"; proto = "tcp"; priority = 0; weight = 1; port = 4190; target = "imap.immae.eu."; } { service = "pop3"; proto = "tcp"; priority = 10; weight = 1; port = 110; target = "pop3.immae.eu."; } { service = "pop3s"; proto = "tcp"; priority = 10; weight = 1; port = 995; target = "pop3.immae.eu."; } ]; subdomains = { # MTA-STS # https://blog.delouw.ch/2018/12/16/using-mta-sts-to-enhance-email-transport-security-and-privacy/ # https://support.google.com/a/answer/9261504 _mta-sts.TXT = [ (toKV { _00__v = "STSv1"; id = "20200109150200Z"; }) ]; # Don't change this value, it is updated automatically! _tls.subdomains._smtp.TXT = [ (toKV { _00__v = "TLSRPTv1"; rua = "mailto:postmaster+mta-sts@immae.eu"; }) ]; mta-sts = ips servers.eldiron.ips.main; # DMARC # p needs to be the first tag _dmarc.TXT = [ (toKV { _00__v = "DMARC1"; _01__p = if quarantine then "quarantine" else "none"; adkim = "s"; aspf = "s"; fo = "1"; rua = "mailto:postmaster+rua@immae.eu"; ruf = "mailto:postmaster+ruf@immae.eu"; }) ]; # Autoconfiguration for Outlook autodiscover = ips servers.eldiron.ips.main; # Autoconfiguration for Mozilla autoconfig = ips servers.eldiron.ips.main; }; # SPF TXT = [ (toKV { _00__v = "spf1 mx ~all"; }) ]; }; }; }; zones = lib.mkOption { type = lib.types.attrsOf (dns-nix.lib.types.zone.substSubModules ( dns-nix.lib.types.zone.getSubModules ++ [ ({ name, ... }: { options = { dnssec = lib.mkOption { default.enable = false; type = lib.types.submodule { options = { enable = lib.mkEnableOption "Configure dnssec for this domain"; }; }; }; hasEmail = lib.mkEnableOption "This domain has e-mails configuration"; emailPolicies = lib.mkOption { default = {}; type = lib.types.attrsOf (lib.types.submodule { options = { receive = lib.mkEnableOption "Configure this domain to receive e-mail"; }; }); apply = builtins.mapAttrs (n: v: v // { domain = name; fqdn = if n == "" then name else "${n}.${name}"; }); }; extraConfig = lib.mkOption { type = lib.types.lines; description = "Extra zone configuration for bind"; example = '' notify yes; ''; default = ""; }; slaves = lib.mkOption { type = lib.types.listOf lib.types.str; description = "NS slave groups of this zone"; default = []; }; ns = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; }; }; }) ])); apply = let toNS = n: builtins.map (d: "${d}.") (builtins.concatMap (s: builtins.attrNames config.myEnv.dns.ns."${s}") n); in builtins.mapAttrs (n: v: v // { NS = v.NS or [] ++ toNS (v.ns); }); default = {}; description = '' attrset of zones to configure ''; }; }; config = let cfg = config.services.bind; in lib.mkIf config.myServices.dns.enable { myServices.chatonsProperties.hostings.dns-secondaire = { file.datetime = "2022-08-22T02:00:00"; hosting = { name = "DNS secondaire"; description = "DNS secondaire"; website = "ns1.immae.eu"; status.level = "OK"; status.description = "OK"; registration.load = "OPEN"; install.type = "PACKAGE"; }; software = { name = "bind9"; website = pkgs.bind.meta.homepage; license.url = pkgs.bind.meta.license.url; license.name = pkgs.bind.meta.license.fullName; version = pkgs.bind.version; source.url = "https://www.isc.org/download/"; }; }; myServices.dns.zones = with config.myServices.dns.helpers; { "imsite.eu" = lib.mkMerge [ zoneHeader (ips servers.eldiron.ips.main) { dnssec.enable = true; ns = [ "immae" "raito" ]; CAA = letsencrypt; extraConfig = '' notify yes; ''; slaves = [ "raito" ]; } ]; "immae.dev" = lib.mkMerge [ { dnssec.enable = true; extraConfig = '' notify yes; ''; slaves = [ "raito" ]; } zoneHeader (ips servers.eldiron.ips.integration) { ns = [ "immae" "raito" ]; CAA = letsencrypt; } ]; "immae.eu" = lib.mkMerge [ { dnssec.enable = true; extraConfig = '' notify yes; ''; slaves = [ "raito" ]; } zoneHeader (ips servers.eldiron.ips.production) { ns = [ "immae" ]; # Cannot put ns2.immae.eu as glue record as it takes ages to propagate. # And gandi only accepts NS records with glues in their interface NS = [ "kurisu.dual.lahfa.xyz." ]; CAA = letsencrypt; # ns1 has glue records in gandi.net subdomains.ns1 = ips servers.eldiron.ips.main; # raito / kurisu.dual.lahfa.xyz ; replace with eldiron in case of problem subdomains.ns2.A = builtins.map (address: { inherit address; ttl = 600; }) servers.eldiron.ips.main.ip4; subdomains.ns2.AAAA = builtins.map (address: { inherit address; ttl = 600; }) servers.eldiron.ips.main.ip6; } { # Machines local users emailPolicies.localhost.receive = false; subdomains.localhost = lib.mkMerge [ (mailCommon "immae.eu" true) mailSend ]; emailPolicies.eldiron.receive = true; subdomains.eldiron = lib.mkMerge [ (mailCommon "immae.eu" true) mailSend ]; } { # For each server "server" and each server ip group "ipgroup", # define ipgroup.server.immae.eu # "main" is set as server.immae.eu instead # if main has an "alias", it is duplicated with this alias. # If the server is a vm, use the v.immae.eu namespace (only main is created) subdomains = let vms = lib.filterAttrs (n: v: v.isVm) servers; bms = lib.filterAttrs (n: v: !v.isVm) servers; toIps = type: builtins.mapAttrs (n: v: ips v.ips."${type}"); in lib.mkMerge [ (toIps "main" bms) { v.subdomains = toIps "main" vms; } (lib.mapAttrs (_: v: { subdomains = lib.mapAttrs' (n': v': lib.nameValuePair "${if v'.alias == null then n' else v'.alias}" (ips v')) (lib.filterAttrs (n': v': n' != "main" || v'.alias != null) v.ips); }) bms) ]; } { # Outils subdomains = { status = ips servers.monitoring-1.ips.main; }; } ]; }; networking.firewall.allowedUDPPorts = [ 53 ]; networking.firewall.allowedTCPPorts = [ 53 ]; users.users.named.extraGroups = [ "keys" ]; services.bind = { package = pkgs.bind.overrideAttrs(old: { # Partially revert https://gitlab.isc.org/isc-projects/bind9/-/commit/fd96a418689593882485bb715b3cd76b9af6f968 # Some DNS server don’t sent the question section postPatch = (old.postPatch or "") + '' sed -i -e "/missing question section/{n;N;d;}" lib/dns/xfrin.c ''; }); enable = true; cacheNetworks = ["any"]; extraOptions = '' allow-recursion { 127.0.0.1; }; allow-transfer { none; }; notify-source ${lib.head config.myEnv.servers.eldiron.ips.main.ip4}; notify-source-v6 ${lib.head config.myEnv.servers.eldiron.ips.main.ip6}; version none; hostname none; server-id none; ''; zones = builtins.mapAttrs (name: v: { master = true; extraConfig = v.extraConfig + lib.optionalString v.dnssec.enable '' key-directory "/var/lib/named/dnssec_keys"; dnssec-policy default; inline-signing yes; ''; masters = []; slaves = lib.flatten (map (n: builtins.attrValues config.myEnv.dns.ns.${n}) v.slaves); file = if v.dnssec.enable then "/var/run/named/dnssec-${name}.zone" else zoneToFile name v; }) config.myServices.dns.zones; }; systemd.services.bind.serviceConfig.StateDirectory = "named"; systemd.services.bind.preStart = lib.mkAfter (builtins.concatStringsSep "\n" (lib.mapAttrsToList (name: v: '' install -m444 ${zoneToFile name v} /var/run/named/dnssec-${name}.zone '') zonesWithDNSSec) + '' install -dm755 -o named /var/lib/named/dnssec_keys ''); myServices.monitoring.fromMasterActivatedPlugins = [ "dns" ]; myServices.monitoring.fromMasterObjects.contactgroup.dns-raito = { alias = "Secondary DNS Raito"; members = "immae"; }; myServices.monitoring.fromMasterObjects.service = lib.mkMerge (lib.mapAttrsToList (name: z: lib.optional (builtins.elem "immae" z.ns) { service_description = "eldiron dns is active and authoritative for ${name}"; host_name = config.hostEnv.fqdn; use = "dns-service"; check_command = ["check_dns" name "-A"]; servicegroups = "webstatus-dns"; _webstatus_name = name; } ++ lib.optionals (builtins.elem "raito" z.ns) [ { service_description = "raito dns is active and authoritative for ${name}"; host_name = config.hostEnv.fqdn; use = "dns-service"; check_command = ["check_external_dns" "kurisu.dual.lahfa.xyz" name "-A"]; contact_groups = "dns-raito"; servicegroups = "webstatus-dns"; _webstatus_name = "${name} (Secondary DNS Raito)"; } { service_description = "raito dns is up to date for ${name}"; host_name = config.hostEnv.fqdn; use = "dns-service"; check_command = ["check_dns_soa" "kurisu.dual.lahfa.xyz" name config.hostEnv.fqdn]; contact_groups = "dns-raito"; servicegroups = "webstatus-dns"; _webstatus_name = "${name} (Secondary DNS Raito up to date)"; } ] ++ lib.optional z.dnssec.enable { service_description = "DNSSEC is active and not expired for ${name}"; host_name = config.hostEnv.fqdn; use = "dns-service"; check_command = ["check_dnssec" name]; servicegroups = "webstatus-dns"; _webstatus_name = "${name} (DNSSEC)"; } ) config.myServices.dns.zones); }; }