Skip to content

DNS & TLS

Record map

infra/terraform/dns.tf:

RecordTypePoints toServes
@ (playpalz.gg)ALoadBalancer IPweb
wwwALoadBalancer IPweb
apiALoadBalancer IPapi
socketALoadBalancer IPesu
mediaALoadBalancer IPmedia (Spaces/CDN)
wsALiveKit droplet IPLiveKit
*ALoadBalancer IPOptional wildcard
@CAALet's EncryptCertificate authority restriction

ws is the odd one out — it points at the LiveKit droplet directly, not at the cluster load balancer, because WebRTC does not go through the ingress.

Terraform-managed or manual

DNS management is opt-in:

hcl
variable "manage_dns"          { default = false }
variable "enable_wildcard_dns" { default = false }

Every record is count = var.manage_dns ? 1 : 0. With the default, Terraform creates nothing and you configure DNS at whatever registrar or provider holds the domain.

To hand DNS to Terraform, the domain must use DigitalOcean nameservers:

ns1.digitalocean.com
ns2.digitalocean.com
ns3.digitalocean.com

Then set manage_dns = true and apply.

Finding the LoadBalancer IP

bash
cd infra/terraform && terraform output loadbalancer_ip

# or directly
kubectl get service -n ingress-nginx nginx-ingress-ingress-nginx-controller

Point every A record except ws at that address.

Manual configuration

If DNS lives elsewhere (Cloudflare, GoDaddy, Namecheap):

TypeNameValueTTL
A@<loadbalancer-ip>300
Awww<loadbalancer-ip>300
Aapi<loadbalancer-ip>300
Asocket<loadbalancer-ip>300
Amedia<loadbalancer-ip>300
Aws<livekit-droplet-ip>300
CAA@0 issue "letsencrypt.org"3600

Cloudflare proxying breaks WebSockets and ACME

If you use Cloudflare, set socket and ws to DNS only (grey cloud). Proxying interferes with WebSocket upgrades and with cert-manager's HTTP-01 challenge. api and www can be proxied, but the ingress already terminates TLS, so use Full (strict) mode.

TLS

cert-manager, installed by Terraform via Helm, with two ClusterIssuers:

IssuerACME endpointUse
letsencrypt-prodacme-v02.api.letsencrypt.orgReal certificates
letsencrypt-stagingacme-staging-v02.api.letsencrypt.orgTesting — untrusted certs, generous rate limits

Ingress objects request a certificate by annotation:

yaml
metadata:
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
    - hosts: [playpalz.gg, www.playpalz.gg, api.playpalz.gg]
      secretName: playpalz-tls

cert-manager then solves an HTTP-01 challenge, obtains the certificate, stores it in the named secret, and renews it automatically at roughly 60 days.

Enforcement

yaml
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
nginx.ingress.kubernetes.io/ssl-ciphers: "HIGH:!aNULL:!MD5"

HTTP redirects to HTTPS, TLS 1.0/1.1 are refused, and weak ciphers are excluded.

Use staging first

Let's Encrypt rate-limits certificate issuance to 5 duplicate certificates per week. A misconfiguration that retries in a loop will exhaust that and lock you out for days. Test a new host with letsencrypt-staging, confirm the challenge succeeds, then switch to letsencrypt-prod.

Verifying

bash
dig playpalz.gg +short
dig api.playpalz.gg +short
dig socket.playpalz.gg +short
dig ws.playpalz.gg +short          # should differ — the droplet

kubectl get certificate
kubectl describe certificate playpalz-tls
kubectl get certificaterequest
kubectl logs -n cert-manager -l app=cert-manager --tail=50

curl -I https://api.playpalz.gg/health
echo | openssl s_client -connect api.playpalz.gg:443 2>/dev/null | openssl x509 -noout -dates

Troubleshooting

Certificate stuck in False / pending

bash
kubectl describe certificate <name>
kubectl describe challenge

Usual causes, in order of likelihood:

  1. DNS has not propagated — the ACME server cannot resolve the host to the LoadBalancer.
  2. HTTP-01 challenge blocked — Cloudflare proxying, or a firewall closing port 80. Port 80 must stay open even though everything redirects to 443.
  3. Rate limited — check the cert-manager logs for a Let's Encrypt rate-limit error.

DNS is not propagating

TTL is 300 s, so changes are usually visible in five minutes. Check against a public resolver rather than your own cache:

bash
dig @8.8.8.8 api.playpalz.gg

Adding a new subdomain

  1. Add the record — dns.tf if manage_dns = true, otherwise at your DNS provider.
  2. Add the host to the relevant Ingress rules and its tls.hosts.
  3. kubectl apply -f infra/k8s/ingress.yaml.
  4. Watch kubectl get certificate until it reports Ready.

Missing step 2's tls.hosts entry is the most common mistake — the route works over HTTP and fails over HTTPS.

Stale documentation nearby

infra/DNS_SETUP.md and infra/DOMAIN_ROUTING_SUMMARY.md reference playpalz.com. The live domain is playpalz.gg — as dns.tf, the ingress manifests, and the mobile build profiles all confirm.

Internal documentation — PlayPalz platform