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:
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):¶
- List all DNS records from both Bunny DNS zones
- Check for orphan records
- Report findings
- 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