# RFC 0003: PowerDNS Self-Hosted DNS | | | |---|---| | **Status** | Implemented | | **Created** | 2026-01-24 | | **Related** | [coherence-mcp RFC 0046](../../coherence-mcp/docs/rfcs/0046-domain-email-migration.md) | --- ## Problem We have 5 domains registered at GoDaddy that need DNS management: - superviber.com - muffinlabs.ai - letemcook.com - appbasecamp.com - thanksforborrowing.com Current state: 1. **Glue records exist** at GoDaddy pointing ns1/ns2.superviber.com to IPs 3.12.26.86 and 3.140.40.205 2. **No DNS server running** at those IPs (planned EKS infrastructure never deployed) 3. **Forgejo running** at 3.218.167.115 on k3s (hearth minimal), but DNS not configured 4. **Cloudflare partially configured** for superviber.com but nameservers not switched We want self-hosted DNS for: - Control over all records without third-party dashboard - Integration with other self-hosted services (mail, git, auth) - Single source of truth in infrastructure-as-code ## Goals 1. **Deploy PowerDNS** on existing k3s infrastructure (hearth) 2. **Configure zones** for all 5 managed domains 3. **Expose DNS** via port 53 UDP/TCP 4. **Update glue records** at GoDaddy to point to working DNS servers 5. **Add git.beyondtheuniverse.superviber.com** record pointing to Forgejo ## Non-Goals - HA DNS with 3 separate IPs (future scope, current setup is single-user) - DNSSEC (future scope) - Secondary/slave DNS (future scope) - DNS-over-HTTPS or DNS-over-TLS (future scope) --- ## Proposal ### Architecture Deploy PowerDNS on the existing hearth k3s instance (3.218.167.115) as a single authoritative DNS server. ``` ┌─────────────────────────────────────┐ │ EC2: 3.218.167.115 │ │ (hearth-forgejo) │ │ │ ┌─────────────┐ │ ┌─────────────────────────────┐ │ │ GoDaddy │ │ │ k3s │ │ │ Glue: │ │ │ │ │ │ ns1/ns2 ────┼────────►│ │ ┌─────────┐ ┌──────────┐ │ │ │ .superviber │ :53 │ │ │PowerDNS │ │ Forgejo │ │ │ │ .com │ │ │ │ :53 │ │ :3000 │ │ │ └─────────────┘ │ │ └─────────┘ └──────────┘ │ │ │ │ │ │ │ │ Traefik (ingress) │ │ │ │ :80, :443, :22 │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘ ``` ### Why Single Instance For a ~1 user personal infrastructure: - Simpler ops (one machine to manage) - Lower cost (~$7.50/month total) - Acceptable availability (spot instance with persistent EIP) - GoDaddy remains as backup (can revert NS records if needed) ### DNS Records Initial zones will include: **superviber.com:** ``` @ A 3.218.167.115 ns1 A 3.218.167.115 ns2 A 3.218.167.115 beyondtheuniverse A 3.218.167.115 git.beyondtheuniverse A 3.218.167.115 mail.beyondtheuniverse A 3.218.167.115 auth.beyondtheuniverse A 3.218.167.115 @ NS ns1.superviber.com. @ NS ns2.superviber.com. ``` Other domains (muffinlabs.ai, etc.) will have minimal records pointing to the same infrastructure. --- ## Implementation ### Phase 1: Deploy PowerDNS on k3s Add to hearth user-data.sh or apply directly: ```yaml apiVersion: v1 kind: Namespace metadata: name: dns --- apiVersion: v1 kind: Secret metadata: name: powerdns-api-key namespace: dns type: Opaque stringData: api-key: "GENERATED_SECRET_KEY" --- apiVersion: apps/v1 kind: Deployment metadata: name: powerdns namespace: dns spec: replicas: 1 selector: matchLabels: app: powerdns template: metadata: labels: app: powerdns spec: containers: - name: powerdns image: powerdns/pdns-auth-49:4.9.2 ports: - name: dns-udp containerPort: 53 protocol: UDP - name: dns-tcp containerPort: 53 protocol: TCP - name: api containerPort: 8081 protocol: TCP env: - name: PDNS_AUTH_API_KEY valueFrom: secretKeyRef: name: powerdns-api-key key: api-key args: - --api=yes - --api-key=$(PDNS_AUTH_API_KEY) - --webserver=yes - --webserver-address=0.0.0.0 - --webserver-port=8081 - --launch=gsqlite3 - --gsqlite3-database=/var/lib/powerdns/pdns.sqlite3 volumeMounts: - name: data mountPath: /var/lib/powerdns volumes: - name: data hostPath: path: /data/powerdns type: DirectoryOrCreate --- apiVersion: v1 kind: Service metadata: name: powerdns namespace: dns spec: type: LoadBalancer externalTrafficPolicy: Local selector: app: powerdns ports: - name: dns-udp port: 53 targetPort: 53 protocol: UDP - name: dns-tcp port: 53 targetPort: 53 protocol: TCP - name: api port: 8081 targetPort: 8081 protocol: TCP ``` ### Phase 2: Update Security Group Add to hearth main.tf security group: ```hcl # DNS (UDP) ingress { description = "DNS UDP" from_port = 53 to_port = 53 protocol = "udp" cidr_blocks = ["0.0.0.0/0"] } # DNS (TCP) ingress { description = "DNS TCP" from_port = 53 to_port = 53 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ``` ### Phase 3: Configure Zones Use PowerDNS API to create zones: ```bash # Create zone curl -X POST http://localhost:8081/api/v1/servers/localhost/zones \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "superviber.com.", "kind": "Native", "nameservers": ["ns1.superviber.com.", "ns2.superviber.com."] }' # Add records curl -X PATCH http://localhost:8081/api/v1/servers/localhost/zones/superviber.com. \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "rrsets": [ { "name": "superviber.com.", "type": "A", "ttl": 300, "changetype": "REPLACE", "records": [{"content": "3.218.167.115", "disabled": false}] }, { "name": "ns1.superviber.com.", "type": "A", "ttl": 3600, "changetype": "REPLACE", "records": [{"content": "3.218.167.115", "disabled": false}] }, { "name": "ns2.superviber.com.", "type": "A", "ttl": 3600, "changetype": "REPLACE", "records": [{"content": "3.218.167.115", "disabled": false}] }, { "name": "git.beyondtheuniverse.superviber.com.", "type": "A", "ttl": 300, "changetype": "REPLACE", "records": [{"content": "3.218.167.115", "disabled": false}] } ] }' ``` ### Phase 4: Update GoDaddy Glue Records At GoDaddy for superviber.com: 1. Update glue records to point ns1 and ns2 to 3.218.167.115 2. Set nameservers to ns1.superviber.com and ns2.superviber.com ### Phase 5: Verify ```bash # Test directly against PowerDNS dig @3.218.167.115 git.beyondtheuniverse.superviber.com A # Test via public DNS (after propagation) dig git.beyondtheuniverse.superviber.com A ``` --- ## File Changes | File | Change | Status | |------|--------|--------| | `hearth/terraform/minimal/main.tf` | Add DNS ports 53 UDP/TCP to security group | Done | | `hearth/scripts/deploy-powerdns.sh` | Standalone PowerDNS deployment script | Done | | `hearth/docs/rfcs/0003-powerdns-self-hosted.md` | This RFC | Done | --- ## Rollback Plan If PowerDNS fails: 1. Update GoDaddy to use its default nameservers (restore from backup) 2. Or: Use Cloudflare (already partially configured) The Elastic IP remains stable, so only NS records need updating. --- ## Test Plan - [x] PowerDNS pod running in k3s - [x] Port 53 UDP/TCP accessible from internet - [x] `dig @3.218.167.115 superviber.com NS` returns ns1/ns2 - [x] `dig @3.218.167.115 git.beyondtheuniverse.superviber.com A` returns 3.218.167.115 - [x] GoDaddy glue records updated for all 5 domains (2026-01-24) - [x] `dig @8.8.8.8 git.beyondtheuniverse.superviber.com A` resolves to 3.218.167.115 - [x] Forgejo accessible at https://git.beyondtheuniverse.superviber.com with valid TLS --- ## Future Work 1. **HA DNS** - Deploy on separate instance with its own EIP for redundancy 2. **DNSSEC** - Sign zones for security 3. **Backup zones** - Export zone files to S3 with daily backups 4. **DNS UI** - PowerDNS-Admin web interface for zone management 5. **Integration with Blue** - Manage DNS records as code via Blue --- ## References - [PowerDNS Documentation](https://doc.powerdns.com/authoritative/) - [coherence-mcp RFC 0046](../../coherence-mcp/docs/rfcs/0046-domain-email-migration.md) - [coherence-mcp dns-zones-rfc0046.yaml](../../coherence-mcp/infra/kubernetes/powerdns/dns-zones-rfc0046.yaml)