diff --git a/terraform/README.md b/terraform/README.md index 21e1749..072ac06 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -27,6 +27,18 @@ day-to-day configuration is in `../ansible`. - asxpio-backups-repl (fsn1) +## DNS + +`dns.tf` holds the zones asxp.io and czsk.it (Hetzner DNS, this project) +and every record set except SOA and NS. Our servers' records reference +`hcloud_server`, so moving a service is one edit. + +All record sets are change-protected except the test XMPP servers' +(`prosody`, `ejabberd`, `anotherim`); both zones are delete-protected. +To change a protected record set, apply twice: first with the set +excluded from protection, then with the new value. Hand edits in the +console show up as drift in `terraform plan`. + ## Network - VPC: 10.10.0.0/20 diff --git a/terraform/dns.tf b/terraform/dns.tf index 97cfbd9..b9d6331 100644 --- a/terraform/dns.tf +++ b/terraform/dns.tf @@ -1,34 +1,161 @@ -# Hetzner Cloud DNS Management -# ================================= +# DNS for asxp.io and czsk.it (Hetzner DNS, project asxpio). Every record +# set of both zones is here except SOA and NS, which Hetzner manages. # -# DNS zones and records are now managed by Terraform. -# -# Existing Zones: -# - asxp.io (ID: 61604) - 34 records -# - czsk.it (ID: 61606) - 7 records -# - childfr.ee (ID: 61605) - 13 records -# -# To manage DNS records: -# Use the 'hcloud_record' resource in Terraform. -# -# Key records to update during migration: -# - asxp.io: @, git, builds, upload, onitama, proxy, xmpp, turn, photos -# - czsk.it: @ -# - childfr.ee: @ +# Keys are "/"; "@" is the apex. Records of our own servers +# point at the server resources, so moving a service is one edit here. +# Every record set has Hetzner's change protection except those of the +# test servers in local.unprotected: a protected set cannot be changed or +# deleted until protection is lifted, so a change to one is two applies +# (set protection off for it, then edit). Both zones are delete-protected. -#resource "hcloud_zone" "czsk_it" { -# name = "czsk.it" -# mode = "primary" -# labels = { -# managed_by = "terraform" -# } -#} +locals { + ip = { + ops_v4 = hcloud_server.ops.ipv4_address + ops_v6 = hcloud_server.ops.ipv6_address + services_v4 = hcloud_server.services.ipv4_address + services_v6 = hcloud_server.services.ipv6_address + } -#resource "hcloud_zone_rrset" "apex_a" { -# zone = hcloud_zone.czsk_it.name -# name = "test" -# type = "A" -# records = [ -# { value = "127.0.0.1", comment = "test" }, -# ] -#} + zones = { + "asxp.io" = { ttl = 3600, delete_protection = true } + "czsk.it" = { ttl = 86400, delete_protection = true } + } + + # Test XMPP servers (prosody, ejabberd, anotherim) and their subdomains + unprotected = "(^|\\.)(prosody|ejabberd|anotherim)$" + + dns = { + "asxp.io" = { + "@/A" = { records = [local.ip.ops_v4] } + "anotherim/A" = { records = ["178.105.241.125"] } + "blog/A" = { records = [local.ip.ops_v4], ttl = 86400 } + "conference/A" = { records = [local.ip.services_v4], ttl = 300 } + "conference.anotherim/A" = { records = ["178.105.241.125"] } + "conference.ejabberd/A" = { records = ["178.105.240.144"] } + "conference.prosody/A" = { records = ["178.105.250.162"] } + "conference.xmpp/A" = { records = [local.ip.services_v4], ttl = 300 } + "ejabberd/A" = { records = ["178.105.240.144"] } + "mc/A" = { records = [local.ip.services_v4] } + "prosody/A" = { records = ["178.105.250.162"] } + "proxy.anotherim/A" = { records = ["178.105.241.125"] } + "proxy.ejabberd/A" = { records = ["178.105.240.144"] } + "proxy.prosody/A" = { records = ["178.105.250.162"] } + "pubsub.anotherim/A" = { records = ["178.105.241.125"] } + "pubsub.ejabberd/A" = { records = ["178.105.240.144"] } + "pubsub.prosody/A" = { records = ["178.105.250.162"] } + "s3/A" = { records = [local.ip.ops_v4] } + "traefik/A" = { records = [local.ip.ops_v4] } + "upload.ejabberd/A" = { records = ["178.105.240.144"] } + "upload.prosody/A" = { records = ["178.105.250.162"] } + "www/A" = { records = [local.ip.ops_v4] } + "xmpp/A" = { records = [local.ip.services_v4], ttl = 300 } + "@/AAAA" = { records = [local.ip.ops_v6] } + "anotherim/AAAA" = { records = ["2a01:4f8:c01e:27d9::1"] } + "blog/AAAA" = { records = [local.ip.ops_v6], ttl = 86400 } + "conference/AAAA" = { records = [local.ip.services_v6], ttl = 300 } + "conference.anotherim/AAAA" = { records = ["2a01:4f8:c01e:27d9::1"] } + "conference.ejabberd/AAAA" = { records = ["2a01:4f8:c01e:11b6::1"] } + "conference.prosody/AAAA" = { records = ["2a01:4f8:c01e:2891::1"] } + "conference.xmpp/AAAA" = { records = [local.ip.services_v6], ttl = 300 } + "ejabberd/AAAA" = { records = ["2a01:4f8:c01e:11b6::1"] } + "prosody/AAAA" = { records = ["2a01:4f8:c01e:2891::1"] } + "proxy.anotherim/AAAA" = { records = ["2a01:4f8:c01e:27d9::1"] } + "proxy.ejabberd/AAAA" = { records = ["2a01:4f8:c01e:11b6::1"] } + "proxy.prosody/AAAA" = { records = ["2a01:4f8:c01e:2891::1"] } + "pubsub.anotherim/AAAA" = { records = ["2a01:4f8:c01e:27d9::1"] } + "pubsub.ejabberd/AAAA" = { records = ["2a01:4f8:c01e:11b6::1"] } + "pubsub.prosody/AAAA" = { records = ["2a01:4f8:c01e:2891::1"] } + "s3/AAAA" = { records = [local.ip.ops_v6] } + "upload.ejabberd/AAAA" = { records = ["2a01:4f8:c01e:11b6::1"] } + "upload.prosody/AAAA" = { records = ["2a01:4f8:c01e:2891::1"] } + "www/AAAA" = { records = [local.ip.ops_v6] } + "xmpp/AAAA" = { records = [local.ip.services_v6], ttl = 300 } + "fm1._domainkey/CNAME" = { records = ["fm1.asxp.io.dkim.fmhosted.com."] } + "fm2._domainkey/CNAME" = { records = ["fm2.asxp.io.dkim.fmhosted.com."] } + "fm3._domainkey/CNAME" = { records = ["fm3.asxp.io.dkim.fmhosted.com."] } + "git/CNAME" = { records = ["git.czsk.it."], ttl = 86400 } + "mail/CNAME" = { records = ["mail.fastmail.com."] } + "@/MX" = { records = ["10 in1-smtp.messagingengine.com.", "20 in2-smtp.messagingengine.com."] } + "@/TXT" = { records = ["\"google-site-verification=H1sj22-DZ-4uW_Ri7z18RDKXo50bPwJXqIT1SyjthZg\"", "\"nfd=5777245\"", "\"v=spf1 include:spf.messagingengine.com ?all\""] } + "_discord/TXT" = { records = ["\"dh=8d39ced1e23afda4492219ee2456a81d209fecaa\""] } + "_dmarc/TXT" = { records = ["\"v=DMARC1;p=reject;rua=mailto:tr@asxp.io;ruf=mailto:tr@asxp.io;rf=afrf;pct=100\""] } + "mail._domainkey/TXT" = { records = ["\"k=rsa; t=s; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0X0brpvueSc7IiGZA2Qx zu12CWjeSN7/SquxrZM+afjU2If9BL5MHiE9iza/k6HgVnvc7exfMUdCHeLPUJJr WUIgCpYUR6ogJF0pe402zltYBExxjJFIMASAXuhTi0Gs4nc7fcfM56zSLckmJpO9 6jMrZPM7x3BxCeJt79j+4NLegClP8d5eRk1aByz9rujRYE\" \" rSJzaUcHuR47uEE5wz bSbY64hUadaSduBuD/OnAy0F3iqMSzVV5KNzDnAyWD4V7QcEWK7GAF9nuj3KhhS9 QLSHydPw/CCrhrXVjcprDDpfDG8dQw1JPfYPKs8h+Z44SGFw1ZxvqYIUmQ6mYrxv uwIDAQAB\""] } + "_stun._tcp/SRV" = { records = ["0 5 3478 xmpp.asxp.io."] } + "_stun._tcp.anotherim/SRV" = { records = ["0 5 3478 anotherim.asxp.io."] } + "_stun._tcp.ejabberd/SRV" = { records = ["0 5 3478 ejabberd.asxp.io."] } + "_stun._udp/SRV" = { records = ["0 5 3478 xmpp.asxp.io."] } + "_stun._udp.anotherim/SRV" = { records = ["0 5 3478 anotherim.asxp.io."] } + "_stun._udp.ejabberd/SRV" = { records = ["0 5 3478 ejabberd.asxp.io."] } + "_turn._tcp/SRV" = { records = ["0 5 3478 xmpp.asxp.io."] } + "_turn._tcp.anotherim/SRV" = { records = ["0 5 3478 anotherim.asxp.io."] } + "_turn._tcp.ejabberd/SRV" = { records = ["0 5 3478 ejabberd.asxp.io."] } + "_turn._udp/SRV" = { records = ["0 5 3478 xmpp.asxp.io."] } + "_turn._udp.anotherim/SRV" = { records = ["0 5 3478 anotherim.asxp.io."] } + "_turn._udp.ejabberd/SRV" = { records = ["0 5 3478 ejabberd.asxp.io."] } + "_xmpp-client._tcp/SRV" = { records = ["0 5 5222 xmpp.asxp.io."], ttl = 3600 } + "_xmpp-client._tcp.anotherim/SRV" = { records = ["0 5 5222 anotherim.asxp.io."] } + "_xmpp-client._tcp.ejabberd/SRV" = { records = ["0 5 5222 ejabberd.asxp.io."] } + "_xmpp-client._tcp.prosody/SRV" = { records = ["0 5 5222 prosody.asxp.io."] } + "_xmpp-server._tcp/SRV" = { records = ["0 5 5269 xmpp.asxp.io."], ttl = 3600 } + "_xmpp-server._tcp.anotherim/SRV" = { records = ["0 5 5269 anotherim.asxp.io."] } + "_xmpp-server._tcp.conference/SRV" = { records = ["0 5 5269 xmpp.asxp.io."], ttl = 3600 } + "_xmpp-server._tcp.conference.anotherim/SRV" = { records = ["0 5 5269 anotherim.asxp.io."] } + "_xmpp-server._tcp.conference.ejabberd/SRV" = { records = ["0 5 5269 ejabberd.asxp.io."] } + "_xmpp-server._tcp.conference.prosody/SRV" = { records = ["0 5 5269 prosody.asxp.io."] } + "_xmpp-server._tcp.ejabberd/SRV" = { records = ["0 5 5269 ejabberd.asxp.io."] } + "_xmpp-server._tcp.prosody/SRV" = { records = ["0 5 5269 prosody.asxp.io."] } + "_xmpps-client._tcp/SRV" = { records = ["0 5 5223 xmpp.asxp.io."], ttl = 3600 } + "_xmpps-client._tcp.anotherim/SRV" = { records = ["0 5 5223 anotherim.asxp.io."] } + "_xmpps-client._tcp.ejabberd/SRV" = { records = ["0 5 5223 ejabberd.asxp.io."] } + "_xmpps-client._tcp.prosody/SRV" = { records = ["0 5 5223 prosody.asxp.io."] } + }, + "czsk.it" = { + "@/A" = { records = [local.ip.ops_v4] } + "www/A" = { records = [local.ip.ops_v4] } + "@/AAAA" = { records = [local.ip.ops_v6] } + } + } + + # "//" => record set; the key is also the import ID + rrsets = merge([ + for zone, sets in local.dns : { + for key, set in sets : "${zone}/${key}" => merge(set, { + zone = zone + name = split("/", key)[0] + type = split("/", key)[1] + }) + } + ]...) +} + +resource "hcloud_zone" "zone" { + for_each = local.zones + name = each.key + mode = "primary" + ttl = each.value.ttl + delete_protection = each.value.delete_protection +} + +resource "hcloud_zone_rrset" "rr" { + for_each = local.rrsets + zone = hcloud_zone.zone[each.value.zone].name + name = each.value.name + type = each.value.type + ttl = try(each.value.ttl, null) + change_protection = !can(regex(local.unprotected, each.value.name)) + records = [for value in each.value.records : { value = value }] +} + +# Adopted from the existing zones on 2026-11-12. Harmless once in state; +# remove after the first apply if you like. +import { + for_each = local.zones + to = hcloud_zone.zone[each.key] + id = each.key +} + +import { + for_each = local.rrsets + to = hcloud_zone_rrset.rr[each.key] + id = each.key +}