diff --git a/mapping.nix b/mapping.nix index e9c044c..65d0f77 100644 --- a/mapping.nix +++ b/mapping.nix @@ -15,6 +15,18 @@ ]; }; + dns = rec { + master = "master-dns-aur-lasuite-federez"; + secondary = [ + "dns-mtz-lasuite-federez" + "dns-ren-lasuite-federez" + ]; + hosts = [ master ] ++ secondary; + _inherit = [ + ./shared/dns.nix + ]; + }; + # For instance: # psql = rec { # master = "some-node-1"; diff --git a/secrets/dns/tsig.age b/secrets/dns/tsig.age new file mode 100644 index 0000000..7aef06a Binary files /dev/null and b/secrets/dns/tsig.age differ diff --git a/secrets/mail/dkim.age b/secrets/mail/dkim.age new file mode 100644 index 0000000..fdeb75e Binary files /dev/null and b/secrets/mail/dkim.age differ diff --git a/secrets/secrets.nix b/secrets/secrets.nix index abbd35d..0d1d7b7 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -35,6 +35,17 @@ let "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN3YPa6rCr7re5CmZ1T4Zh6k9U9E6eVs7KgLpOEKT+Kx root@bastion-aur-lasuite-federez" ]; + system-dns = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOCkwjv+R6OgPdcv+4aaymDHnHgWKA4Ez2h0D2dRD0Jr root@dns-mtz-lasuite-federez" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFOARZoBC95SxV2tL5hbE20vnOS7VIyn4/ACeVdpIZ2D root@dns-ren-lasuite-federez" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHb5RKh1JEfStK1ZlJnvw9SH+GgZYJVC6GebbWWgtvwW root@master-dns-aur-lasuite-federez" + ]; + + system-mail = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIv9JPqMuWRU1tQ5R8rMcxfv5etDwuvGH4LzOWx98v0t root@mail-mtz-lasuite-federez" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJQ24UID4nGKru/to8wSoJ6LMcKwP3d9kZG7dlew0g9S root@mail-ren-lasuite-federez" + ]; + asyncnomi = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIENo/g3BZ1bJViYE6EY4VZO96a4q8U4nWKjTprQJtjEH asyncnomi" ]; gamma = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKhuKmuBPLAtQSjy4E4UaEmf8Qj56414r+adAJ6BgmO8 gamma" ]; jeltz = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHabXDr/vrx361yaxKK58jHJB77TNVZvqhkIiaTB7ECI jeltz" ]; @@ -82,4 +93,12 @@ in "bastion/wg-private-zone-1.age".publicKeys = system-bastion ++ users; "bastion/wg-private-zone-2.age".publicKeys = system-bastion ++ users; "bastion/wg-private-zone-3.age".publicKeys = system-bastion ++ users; + + # DNS secrets + + "dns/tsig.age".publicKeys = system-dns ++ users; + + # Mail secrets + + "mail/dkim.age".publicKeys = system-mail ++ users; } \ No newline at end of file diff --git a/shared/dns.nix b/shared/dns.nix new file mode 100644 index 0000000..eb12ff5 --- /dev/null +++ b/shared/dns.nix @@ -0,0 +1,7 @@ +{ ... }: +{ + # Import dependencies + imports = [ + ./dns/knot.nix + ]; +} \ No newline at end of file diff --git a/shared/dns/dkim.nix b/shared/dns/dkim.nix new file mode 100644 index 0000000..e7fcfc5 --- /dev/null +++ b/shared/dns/dkim.nix @@ -0,0 +1,3 @@ +{ + dkim_pub = "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0ki3RIIQaUhimeaE6AATJCA6l9Wh9Qi+RZppylD/Ivi53z0ikp07oc3o31HCbK/mSj7ewD+WUxhfytAkyPsZn1q4WNLuxXcE+cto9IbLWxzc3uKpxmY8VEOL5xWQaiiCW4GE6wzOSLaD5w6+x9I54U3JEEOiheFBJCLwpjoCv+osoSYKuQUnYMQPfz6m4gYS9kfhbSo1Xg+GxwwfpzpXcnStBZQfAl7L3w4TeftpDIUV3Xmn2kud84ldhnOMSAWelApvzSGmeiXRbozeWWTdxbw3UU1QL/h7kjEW2TkNXINHhHRvmoqyjzzZzeQ5DN7ETMEHpFsqSWdBnW2MM/VaTaUrcOuXjN6U2QymRA01byybROLxkdeV2PtPpZnCwzBbYuB2qFqFhQKEep9X6OYdUlVXZOlcfGPY3dj2sYwV045TcWMMSiAaKqF+jVILxXa3RqaeGsMEe52QLtxLvqLbPmdlTg7J8+jShJngzB1qne0vvMZT7tu7ez4lJsua+H5kqpuLIxSjQ1ea+XzWApr1JzktCMluh7SaY1AWFp/epCGhtjByy2dzgYYc/Ij1ieaAa2XdR57WZx4G9WaKCpSc8SOWn+TxCBRssd6fIAOe69WYD5Db35xoQFXPL9wRSgw5GzkBkHzHYwu+IjORjZNtB1/mMQWsnRl/QPW1rGQZeikCAwEAAQ=="; +} \ No newline at end of file diff --git a/shared/dns/knot.nix b/shared/dns/knot.nix new file mode 100644 index 0000000..9a2d561 --- /dev/null +++ b/shared/dns/knot.nix @@ -0,0 +1,210 @@ +{ config, lib, pkgs, ... }: + +let + # Import nodes + nodes = import ./../../nodes.nix; + myNode = nodes."${config.hostName}"; + + # And mapping + mapping = import ./../../mapping.nix; + + dkim = import ./dkim.nix; + + supportsIPv4 = nd: lib.hasAttr "ip4" nd; + supportsIPv6 = nd: lib.hasAttr "ip6" nd; + + timestampDerivation = pkgs.runCommand "timestamp" {} '' + echo -n $(date +%s) > $out + ''; + timestamp = builtins.readFile timestampDerivation; + + # Domain key + domainkey = '' + v=DKIM1; k=rsa; p=${dkim.dkim_pub}''; + segments = ((lib.stringLength domainkey) / 255); + domainkeySplitted = map (x: lib.substring (x*255) 255 domainkey) (lib.range 0 segments); + + + ##### + ## Knot specific + ##### + + # Define remote based on current role + remotes = if myNode == mapping.dns.master then + map (hostname: { + id = hostname; + address = "172.19.${toString nodes.${hostname}.zone}.${toString nodes.${hostname}.id}@53"; + }) mapping.dns.secondary + else + [{ + id = mapping.dns.master; + address = "172.19.${toString nodes.${mapping.dns.master}.zone}.${toString nodes.${mapping.dns.master}.id}@53"; + }]; + + remotesNames = map (remote: remote.id) remotes; + + # Remotes ACL + remotesACL = if myNode == mapping.dns.master then + map (hostname: { + id = "acl_${hostname}"; + address = "172.19.${toString nodes.${hostname}.zone}.${toString nodes.${hostname}.id}@53"; + action = "transfer"; + }) mapping.dns.secondary + else + [{ + id = "acl_${mapping.dns.master}"; + address = "172.19.${toString nodes.${mapping.dns.master}.zone}.${toString nodes.${mapping.dns.master}.id}@53"; + action = "notify"; + }]; + + remotesACLNames = map (remote: remote.id) remotesACL; + + # Other ACL + letsencryptACL = if myNode == mapping.dns.master then + [{ + id = "acl_le_challenge"; + address = [ + "172.19.0.0/16" + "fc00::/96" + ]; + action = "update"; + update-type = "TXT"; + key = "letsencrypt"; + }] + else []; + + ##### + ## Zone specific + ##### + + # host to dn + hostToDomain = hostname: builtins.replaceStrings ["-"] ["."] hostname; + + # Gen NS + toNSRecord = host: "IN NS ${hostToDomain host}.net."; + nsRecords = map toNSRecord mapping.dns.secondary; + + dnsSecondaryConfigs = lib.filterAttrs (peerName: _peerConfig: lib.elem peerName mapping.dns.secondary) nodes; + + # Gen A NS + nsARecords = lib.flatten (lib.mapAttrsToList (hostname: node: + lib.optional (supportsIPv4 node) "${hostToDomain hostname}.net. IN A ${node.ip4}" + ) dnsSecondaryConfigs); + + # Gen AAAA NS + nsAAAARecords = lib.flatten (lib.mapAttrsToList (hostname: node: + lib.optional (supportsIPv6 node) "${hostToDomain hostname}.net. IN AAAA ${node.ip6}" + ) dnsSecondaryConfigs); + + # Zone conf + zoneFilePath = "/var/lib/knot/zones/zone-lasuite-federez-net"; + zone-lasuite-federez-net = pkgs.writeText "zone-lasuite-federez-net" '' + $ORIGIN lasuite.federez.net. + $TTL 60 + @ IN SOA ${builtins.head nsRecords}. monitoring.lasuite.federez.net. ( + ${timestamp} ; serial + 60 ; refresh + 60 ; retry + 60 ; expire + 60 ) ; minimum TTL + + IN TXT "v=spf1 a:lasuite.federez.net ~all" + + ${builtins.concatStringsSep "\n" nsRecords} + + ${builtins.concatStringsSep "\n" nsARecords} + ${builtins.concatStringsSep "\n" nsAAAARecords} + + _dmarc IN TXT "v=DMARC1; p=quarantine; ruf=mailto:postmaster@lasuite.federez.net" + _mta-sts IN TXT "v=STSv1; id=1" + _smtp._tls IN TXT "v=TLSRPTv1;rua=mailto:postmaster@lasuite.federez.net" + default._domainkey IN TXT "${lib.concatStringsSep "\" \"" domainkeySplitted}" + cause-toujours IN TXT "v=spf1 a:lasuite.federez.net ~all" + ''; +in +{ + age.secrets = { + "tsig" = { + file = ./../../secrets/dns/tsig.age; + owner = "knot"; + group = "knot"; + }; + }; + + # Ensure the directory exists and is writable + systemd.tmpfiles.rules = [ + "d /var/lib/knot/zones 0755 knot knot -" + "f /var/lib/knot/zones/zone-lasuite-federez-net 0644 knot knot -" + ]; + + # Knot messes with resolvd + services.resolved.enable = false; + networking.resolvconf.extraConfig = '' + name_servers="1.1.1.1 1.0.0.1 2606:4700:4700::1111" + ''; + + # Attach knot to writeZoneFile to force knot to restart after rebuild (otherwise no changes will be detected) + systemd.services.knot = { + partOf = [ "writeZoneFile.service" ]; + restartTriggers = [ zone-lasuite-federez-net ]; + reloadIfChanged = lib.mkForce false; + }; + services.knot = { + enable = true; + package = pkgs.knot-dns; + + # To regenerate the secret use: + # nix-shell -p knot-dns + # keymgr tsig generate -t letsencrypt hmac-sha512 + keyFiles = [ + config.age.secrets.tsig.path + ]; + + settings = { + server = { + listen = [ + "0.0.0.0@53" + "::@53" + ]; + }; + + remote = remotes; + + acl = remotesACL ++ otherACL; + + zone = if myNode == mapping.dns.master then [ + { + domain = "lasuite.federez.net"; + file = zoneFilePath; + notify = remotesNames; + acl = remotesACLNames ++ [ + "acl_le_challenge" + ]; + } + ] else [ + { + domain = "lasuite.federez.net"; + master = builtins.head remotesNames; + acl = remotesACLNames; + } + ]; + + log = [ + { + target = "syslog"; + any = "debug"; + } + ]; + }; + }; + + # Write the generated zone file to the writable path + systemd.services.writeZoneFile = { + before = [ "knot.service" ]; + description = "Write initial zone file for lasuite.federez.net"; + serviceConfig = { + ExecStart = "${pkgs.coreutils}/bin/cp '${zone-lasuite-federez-net}' ${zoneFilePath}"; + }; + wantedBy = [ "multi-user.target" ]; + }; +} \ No newline at end of file