hearth/scripts/deploy-powerdns.sh
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

352 lines
9 KiB
Bash
Executable file

#!/bin/bash
set -euo pipefail
# Deploy PowerDNS on k3s
# Usage: sudo ./deploy-powerdns.sh <PUBLIC_IP>
# Example: sudo ./deploy-powerdns.sh 3.218.167.115
if [ $# -lt 1 ]; then
echo "Usage: $0 <PUBLIC_IP>"
echo "Example: $0 3.218.167.115"
exit 1
fi
PUBLIC_IP="$1"
echo "=========================================="
echo "Deploying PowerDNS with IP: $PUBLIC_IP"
echo "=========================================="
# Create data directory
echo "Creating data directory..."
mkdir -p /data/powerdns
chown 953:953 /data/powerdns
# Generate API key
PDNS_API_KEY=$(openssl rand -hex 32)
echo "$PDNS_API_KEY" > /root/.pdns-api-key
chmod 600 /root/.pdns-api-key
echo "API key saved to /root/.pdns-api-key"
# Deploy PowerDNS
echo "Deploying PowerDNS to k3s..."
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: dns
---
apiVersion: v1
kind: Secret
metadata:
name: powerdns-api-key
namespace: dns
type: Opaque
stringData:
api-key: "$PDNS_API_KEY"
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: powerdns-data
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /data/powerdns
storageClassName: local
persistentVolumeReclaimPolicy: Retain
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: powerdns-data
namespace: dns
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: local
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: powerdns
namespace: dns
spec:
replicas: 1
selector:
matchLabels:
app: powerdns
strategy:
type: Recreate
template:
metadata:
labels:
app: powerdns
spec:
securityContext:
fsGroup: 953
initContainers:
- name: init-db
image: powerdns/pdns-auth-49:4.9.2
command:
- /bin/sh
- -c
- |
if [ ! -f /var/lib/powerdns/pdns.sqlite3 ]; then
sqlite3 /var/lib/powerdns/pdns.sqlite3 < /usr/local/share/doc/pdns/schema.sqlite3.sql
chown 953:953 /var/lib/powerdns/pdns.sqlite3
fi
volumeMounts:
- name: data
mountPath: /var/lib/powerdns
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
command:
- pdns_server
args:
- --api=yes
- --api-key=\$(PDNS_AUTH_API_KEY)
- --webserver=yes
- --webserver-address=0.0.0.0
- --webserver-port=8081
- --webserver-allow-from=0.0.0.0/0
- --launch=gsqlite3
- --gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
- --local-address=0.0.0.0
- --local-port=53
volumeMounts:
- name: data
mountPath: /var/lib/powerdns
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: data
persistentVolumeClaim:
claimName: powerdns-data
---
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
---
apiVersion: v1
kind: Service
metadata:
name: powerdns-api
namespace: dns
spec:
type: ClusterIP
selector:
app: powerdns
ports:
- name: api
port: 8081
targetPort: 8081
protocol: TCP
EOF
echo "Waiting for PowerDNS pod to be ready..."
kubectl wait --for=condition=ready pod -l app=powerdns -n dns --timeout=120s
# Get PowerDNS API service IP
PDNS_HOST="http://$(kubectl get svc powerdns-api -n dns -o jsonpath='{.spec.clusterIP}'):8081"
echo "PowerDNS API: $PDNS_HOST"
# Wait for API to be ready
echo "Waiting for PowerDNS API..."
until curl -sf -H "X-API-Key: $PDNS_API_KEY" "$PDNS_HOST/api/v1/servers/localhost" > /dev/null 2>&1; do
sleep 2
done
echo "PowerDNS API is ready!"
# Function to create zone
create_zone() {
local DOMAIN=$1
echo "Creating zone: $DOMAIN"
curl -sf -X POST "$PDNS_HOST/api/v1/servers/localhost/zones" \
-H "X-API-Key: $PDNS_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$DOMAIN.\",
\"kind\": \"Native\",
\"nameservers\": [\"ns1.$DOMAIN.\", \"ns2.$DOMAIN.\"]
}" 2>/dev/null || echo " (zone may already exist)"
}
# Function to setup records
setup_records() {
local DOMAIN=$1
echo "Setting up records for: $DOMAIN"
curl -sf -X PATCH "$PDNS_HOST/api/v1/servers/localhost/zones/$DOMAIN." \
-H "X-API-Key: $PDNS_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"rrsets\": [
{
\"name\": \"$DOMAIN.\",
\"type\": \"NS\",
\"ttl\": 86400,
\"changetype\": \"REPLACE\",
\"records\": [
{\"content\": \"ns1.$DOMAIN.\", \"disabled\": false},
{\"content\": \"ns2.$DOMAIN.\", \"disabled\": false}
]
},
{
\"name\": \"ns1.$DOMAIN.\",
\"type\": \"A\",
\"ttl\": 3600,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"ns2.$DOMAIN.\",
\"type\": \"A\",
\"ttl\": 3600,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"$DOMAIN.\",
\"type\": \"A\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"www.$DOMAIN.\",
\"type\": \"CNAME\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$DOMAIN.\", \"disabled\": false}]
}
]
}"
}
echo ""
echo "Creating DNS zones..."
for domain in superviber.com muffinlabs.ai letemcook.com appbasecamp.com thanksforborrowing.com alignment.coop; do
create_zone $domain
setup_records $domain
done
# Setup superviber.com beyondtheuniverse services
echo ""
echo "Setting up beyondtheuniverse.superviber.com services..."
curl -sf -X PATCH "$PDNS_HOST/api/v1/servers/localhost/zones/superviber.com." \
-H "X-API-Key: $PDNS_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"rrsets\": [
{
\"name\": \"beyondtheuniverse.superviber.com.\",
\"type\": \"A\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"git.beyondtheuniverse.superviber.com.\",
\"type\": \"A\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"mail.beyondtheuniverse.superviber.com.\",
\"type\": \"A\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"auth.beyondtheuniverse.superviber.com.\",
\"type\": \"A\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"vault.beyondtheuniverse.superviber.com.\",
\"type\": \"A\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
},
{
\"name\": \"grafana.beyondtheuniverse.superviber.com.\",
\"type\": \"A\",
\"ttl\": 300,
\"changetype\": \"REPLACE\",
\"records\": [{\"content\": \"$PUBLIC_IP\", \"disabled\": false}]
}
]
}"
echo ""
echo "=========================================="
echo "PowerDNS deployment complete!"
echo "=========================================="
echo ""
echo "Verification:"
echo " dig @$PUBLIC_IP superviber.com NS"
echo " dig @$PUBLIC_IP git.beyondtheuniverse.superviber.com A"
echo ""
echo "Next steps:"
echo "1. Update GoDaddy glue records for each domain:"
echo " - ns1.<domain> -> $PUBLIC_IP"
echo " - ns2.<domain> -> $PUBLIC_IP"
echo ""
echo "2. Update nameservers at GoDaddy:"
echo " - ns1.<domain>"
echo " - ns2.<domain>"
echo ""
echo "3. Wait for DNS propagation (up to 48 hours)"
echo "=========================================="