Skip to content

DOMAIN:NETWORKING:DNS_MANAGEMENT

OWNER: stef UPDATED: 2026-04-15 SCOPE: all DNS operations for GE and client domains AGENTS: stef (primary), karel (CDN CNAME coordination), arjan (server IPs), leon (deploy chain) PROVIDER: bunny.net DNS (Slovenia-based, EU-native) REGISTRAR: TransIP (Netherlands-based, domain registration only)


DNS:OVERVIEW

PURPOSE: manage DNS records for all GE and client domains PROVIDER: bunny.net DNS (BunnyWay/bunnynet Terraform provider) REGISTRAR: TransIP (domain registration only — no DNS management, no Terraform provider) METHOD: Terraform only — NEVER manual console changes CREDENTIALS: Vault path secret/ge/bunny (bunny.net API key) PRIMARY_DOMAIN: growing-europe.com (Bunny DNS zone) HOSTING_DOMAIN: hosting.growing-europe.com (separate Bunny DNS zone, client subdomains) NAMESERVERS: kiki.bunny.net / coco.bunny.net (set at TransIP registrar)

RULE: all DNS changes via Terraform (IaC, auditable, version-controlled) RULE: never register a new domain without human (Dirk-Jan) approval RULE: always verify DNS propagation after changes


DNS:ARCHITECTURE

Zone Layout

TransIP (registrar only)
  └── growing-europe.com NS → kiki.bunny.net / coco.bunny.net

Bunny DNS (authoritative for all zones)
  ├── growing-europe.com zone (GE operations)
  │   ├── office.growing-europe.com   → A → fort-knox-dev
  │   ├── api.growing-europe.com      → A → UpCloud LB
  │   ├── www.growing-europe.com      → CNAME → bunny.net CDN
  │   ├── auth.growing-europe.com     → A → Zitadel
  │   ├── MX, SPF, DKIM, DMARC       → email config
  │   └── hosting.growing-europe.com  → NS → (same Bunny DNS, separate zone)
  └── hosting.growing-europe.com zone (client projects)
      ├── acme-corp.hosting.growing-europe.com   → bunny.net CDN
      └── bakkerij.hosting.growing-europe.com    → bunny.net CDN

Both zones managed via the bunny-dns-zone Terraform module.


DNS:RECORD_TYPES

A RECORD

PURPOSE: maps domain to IPv4 address USE_WHEN: pointing domain to a server IP

resource "bunnynet_dns_record" "app_a" {
  zone_id = bunnynet_dns_zone.ge_root.id
  name    = var.subdomain  # e.g., "app" for app.growing-europe.com
  type    = "A"
  value   = var.server_ip  # from arjan's provisioning output
  ttl     = 300            # 5 min TTL — quick failover
}

VERIFY:

TOOL: dig
RUN: dig {domain} @kiki.bunny.net A +short
EXPECT: {server_ip}

AAAA RECORD

PURPOSE: maps domain to IPv6 address USE_WHEN: server has IPv6 (UpCloud servers typically have both IPv4 and IPv6) TTL: 300s (same as A record)

CNAME RECORD

PURPOSE: maps domain to another domain (alias) USE_WHEN: CDN domains (pointing to BunnyCDN pull zone)

resource "bunnynet_dns_record" "cdn_cname" {
  zone_id = bunnynet_dns_zone.ge_root.id
  name    = "cdn"          # cdn.growing-europe.com
  type    = "CNAME"
  value   = "${var.pullzone}.b-cdn.net."  # Karel provides pull zone name
  ttl     = 300
}

FLOW: Karel creates pull zone -> tells Stef pull zone hostname -> Stef creates CNAME

RULE: CNAME cannot coexist with other record types on the same name RULE: apex domain (@) cannot use CNAME — use A record or ALIAS (if supported)

MX RECORD

PURPOSE: mail routing USE_WHEN: client domain needs email

resource "bunnynet_dns_record" "mx" {
  zone_id  = bunnynet_dns_zone.ge_root.id
  name     = ""           # apex
  type     = "MX"
  value    = "mail.growing-europe.com."
  priority = 10
  ttl      = 86400  # 24h TTL — mail records are stable
}

TTL: 86400s (24 hours) — MX records rarely change, high TTL reduces DNS load

TXT RECORD

PURPOSE: domain verification, email authentication (SPF, DKIM, DMARC) USE_WHEN: email setup, domain ownership verification, Let's Encrypt DNS-01 challenge

# SPF record
resource "bunnynet_dns_record" "spf" {
  zone_id = bunnynet_dns_zone.ge_root.id
  name    = ""
  type    = "TXT"
  value   = "v=spf1 include:_spf.google.com ~all"
  ttl     = 86400
}

CAA RECORD

PURPOSE: Certificate Authority Authorization — controls which CAs can issue certs for domain USE_WHEN: always for production domains

resource "bunnynet_dns_record" "caa" {
  zone_id = bunnynet_dns_zone.ge_root.id
  name    = ""
  type    = "CAA"
  value   = "0 issue \"letsencrypt.org\""
  ttl     = 86400
}

RULE: CAA records should restrict issuance to Let's Encrypt only (GE standard CA) EXCEPTION: client-provided certs from other CAs — add their CA to CAA


DNS:TTL_STRATEGY

Record Type TTL Rationale
A / AAAA 300s (5 min) Quick failover during incidents or migrations
CNAME 300s (5 min) CDN changes need fast propagation
MX 86400s (24h) Mail servers rarely change
TXT (SPF/DKIM/DMARC) 86400s (24h) Email auth records are stable
TXT (verification) 300s (5 min) Temporary records, remove after verification
CAA 86400s (24h) CA policy rarely changes
NS 86400s (24h) Nameserver delegation is stable

PRE_MIGRATION_TTL: - Before planned migration, reduce A/CNAME TTL to 60s (24 hours in advance) - After migration complete and verified, restore to standard TTL - REASON: low TTL ensures clients resolve to new IP quickly during cutover


DNS:PROPAGATION

CHECKING_PROPAGATION

TOOL: dig
RUN: dig {domain} @kiki.bunny.net       # authoritative nameserver
RUN: dig {domain} @8.8.8.8              # Google Public DNS
RUN: dig {domain} @1.1.1.1              # Cloudflare DNS
RUN: dig {domain} @9.9.9.9              # Quad9 DNS

IF_STALE_CACHE: - TTL determines how long resolvers cache records - After updating, wait at least 1x TTL for most resolvers to refresh - Some ISP resolvers ignore TTL (violation of RFC) — nothing GE can do about that - For critical cutover, monitor propagation across multiple resolvers

PROPAGATION_TIMELINE

TTL ~50% propagated ~95% propagated
60s ~1 minute ~5 minutes
300s ~5 minutes ~15 minutes
3600s ~1 hour ~4 hours
86400s ~12 hours ~48 hours

DNS:MULTI_DOMAIN_MANAGEMENT

DOMAIN_ORGANIZATION

growing-europe.com                    # GE corporate domain (Bunny DNS root zone)
  office.growing-europe.com           # admin-ui
  wiki.growing-europe.com             # wiki brain
  auth.growing-europe.com             # Zitadel
  hosting.growing-europe.com          # client hosting base domain (Bunny DNS hosting zone)
    {client}.hosting.growing-europe.com  # per-client subdomain

{client-domain}.com                   # client's own domain (managed by stef)
  www.{client-domain}.com             # client website
  app.{client-domain}.com             # client application
  api.{client-domain}.com             # client API
  cdn.{client-domain}.com             # CDN (CNAME to BunnyCDN)

PER_CLIENT_DNS_SETUP

ON_NEW_CLIENT_DOMAIN:
1. Client provides domain name
2. Stef verifies domain ownership (TXT record challenge)
3. For hosting.growing-europe.com subdomains:
   → Stef creates record in Bunny DNS hosting zone via Terraform
4. For client's own domain:
   → Client points nameservers to Bunny DNS (kiki.bunny.net / coco.bunny.net)
   OR client creates CNAME to {client}.hosting.growing-europe.com
5. Stef creates standard record set:
   → A record: server IP (from arjan/rutger)
   → CNAME: cdn.{domain} -> {pullzone}.b-cdn.net (from karel)
   → TXT: SPF, DKIM, DMARC (if email needed)
   → CAA: restrict to letsencrypt.org
6. Stef triggers cert-manager for TLS certificate (DNS-01 challenge)
7. Verify all records resolving correctly

DNS:LET_S_ENCRYPT_VALIDATION

DNS_01_CHALLENGE

PURPOSE: prove domain ownership for TLS certificate issuance METHOD: cert-manager creates _acme-challenge TXT record via Bunny DNS API ADVANTAGE: works for wildcard certificates, does not require inbound HTTP

FLOW:
1. cert-manager requests certificate from Let's Encrypt
2. Let's Encrypt returns challenge token
3. cert-manager webhook creates TXT record via Bunny DNS API:
   _acme-challenge.{domain} TXT "{token}"
4. Let's Encrypt verifies TXT record exists
5. Certificate issued
6. cert-manager removes challenge TXT record

CREDENTIAL: Bunny API key in Vault (secret/ge/bunny)

IF_CHALLENGE_FAILS: 1. CHECK: is Bunny API key valid? (Vault path secret/ge/bunny) 2. CHECK: does the domain use Bunny DNS nameservers (kiki.bunny.net / coco.bunny.net)? 3. CHECK: is there a CAA record blocking Let's Encrypt? 4. CHECK: cert-manager logs for specific error

TOOL: kubectl
RUN: kubectl logs -l app=cert-manager -n cert-manager --tail=50
RUN: kubectl get challenges -A
RUN: kubectl describe challenge {name} -n {namespace}

DNS:DNSSEC

STATUS

CURRENT: DNSSEC is NOT automatically managed by Bunny DNS (unlike TransIP's former auto-DNSSEC) GE_POLICY: evaluate DNSSEC setup via Bunny DNS for production domains NOTE: TransIP previously auto-managed DNSSEC when using their nameservers. With Bunny DNS as authoritative, DNSSEC requires manual DS record configuration if needed.

WHAT_DNSSEC_DOES

  • Signs DNS responses with cryptographic keys
  • Prevents DNS spoofing and cache poisoning
  • Chain of trust from root DNS servers to domain
  • Does NOT encrypt queries (that is DNS-over-HTTPS/TLS)

DNS:ORPHAN_RECORD_CHECK

WEEKLY_AUDIT (Stef, Monday 7am)

PURPOSE: detect DNS records pointing to decommissioned servers (dangling DNS)

CHECK:
1. List all A records from Bunny DNS zones
2. For each A record, verify server is responsive:
   TOOL: curl
   RUN: curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://{domain}
3. For each CNAME, verify target exists:
   TOOL: dig
   RUN: dig {cname_target} +short
4. FLAG: any record pointing to unreachable IP or non-existent CNAME
5. ALERT: orphan records are a security risk (subdomain takeover)

SECURITY_RISK: dangling CNAME to deprovisioned CDN/cloud service = subdomain takeover vulnerability VICTORIA: notified of any dangling records for security assessment


DNS:DEPLOY_CHAIN_POSITION

STEF'S ROLE IN DEPLOY CHAIN

... → Rutger (prod k8s applied) →
  STEF:
  1. Create/update DNS records for new deployment
  2. Verify TLS certificate valid and terminating
  3. Apply/verify production firewall rules
  4. Confirm network ready to Leon
→ Karel (CDN purge + edge config) →
→ Leon (cutover confirmed)

DNS:AGENT_WORKFLOW

FOR_STEF

ON_DNS_TASK: 1. READ this page for DNS standards 2. DETERMINE record type needed 3. WRITE Terraform config (never manual console) 4. RUN terraform plan — verify TTL, record value 5. RUN terraform apply 6. VERIFY: dig {domain} @kiki.bunny.net 7. VERIFY: dig {domain} @8.8.8.8 (propagation) 8. HAND OFF to leon (deploy chain) or confirmation to requesting agent

ON_WEEKLY_AUDIT (Monday 7am):

  1. List all DNS records from both Bunny DNS zones
  2. Check for orphan records
  3. Report findings
  4. Remove orphans (with approval)

DNS:ANTI_PATTERNS

BEFORE_EVERY_DNS_ACTION: 1. Am I making manual Bunny console changes? (NEVER — Terraform only) 2. Am I registering a new domain without human approval? (NEVER) 3. Am I creating wildcard DNS? (check with victoria first) 4. Am I setting TTL too high for records that may need quick changes? (A/CNAME: 300s max) 5. Am I setting TTL too low for stable records? (MX/TXT: 86400s appropriate) 6. Am I leaving dangling CNAME records? (security risk — subdomain takeover) 7. Am I skipping propagation verification? (NEVER — always verify with dig) 8. Am I touching CDN configuration? (Karel's domain, Stef only manages origin DNS) 9. Am I trying to manage DNS via TransIP? (NEVER — TransIP is registrar only, all DNS is in Bunny)


DNS:CROSS_REFERENCES

TLS_CERTIFICATES: domains/networking/tls-certificates.md — DNS-01 challenge for cert issuance CDN_EDGE: domains/networking/cdn-edge.md — CNAME records for BunnyCDN pull zones NETWORK_SECURITY: domains/networking/network-security.md — firewall and network configuration TERRAFORM_PATTERNS: domains/infrastructure/terraform-patterns.md — Bunny DNS provider config DEPLOYMENT_STRATEGIES: domains/infrastructure/deployment-strategies.md — DNS changes during deploy TRANSIP_INTEGRATION: domains/infrastructure/transip-integration.md — domain registration only