aboutsummaryrefslogblamecommitdiff
path: root/systems/eldiron/dns.nix
blob: 7645b69494261501914de5d40f80e977234104a8 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                                    













                                                                                        











                                                                                                



                                                                                                                    











                                                                                         



                                                   













                                                                     
                                        





















                                                                                                                        

                                                                                                                                            


                                                   

                                                                                                                                                                                                                                    


               
                                                        







                                                                        







                                                                                   






































































                                                                                                                        

                                   
                            



                               



                                 
                               













                                             
                               







                                            



                                                                                 










                                                                                                                     
                                                                                       
                                               
                                                                                     


















































                                                                                                     




                                                                             


                                                                                         
                                                                                                     

                                       






                                                                             










                                                                                               





















                                                                                             

                                        
                                              

                                        
                                             



                                   
{ 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
          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"; }) ];
          };

          # 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 = {
      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.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"];

          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];

          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);
  };
}