hearth/docs/rfcs/0003-powerdns-self-hosted.md
Eric Garcia da40273177 feat(dns): Add self-hosted PowerDNS for 5 managed domains
- Deploy PowerDNS on k3s with SQLite backend
- Add DNS ports 53 UDP/TCP to security group
- Configure zones for superviber.com, muffinlabs.ai, letemcook.com,
  appbasecamp.com, thanksforborrowing.com
- Add deploy-powerdns.sh standalone deployment script
- Document in RFC 0003

Glue records updated at GoDaddy to point ns1/ns2 to 3.218.167.115.
DNS verified working via Google DNS (8.8.8.8).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 07:42:48 -05:00

9.7 KiB

RFC 0003: PowerDNS Self-Hosted DNS

Status Implemented
Created 2026-01-24
Related coherence-mcp RFC 0046

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:

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:

# 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:

# 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

# 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

  • PowerDNS pod running in k3s
  • Port 53 UDP/TCP accessible from internet
  • dig @3.218.167.115 superviber.com NS returns ns1/ns2
  • dig @3.218.167.115 git.beyondtheuniverse.superviber.com A returns 3.218.167.115
  • GoDaddy glue records updated for all 5 domains (2026-01-24)
  • dig @8.8.8.8 git.beyondtheuniverse.superviber.com A resolves to 3.218.167.115
  • 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