--- # In-cluster dynamic DNS for the home WAN (2026-10-03). # # Keeps ONE record, `wan.${BASE_DOMAIN}` (A, IPv4 only, not proxied), pointed at the home WAN's # current public IP. Every public name external-dns publishes from the shared Gateway is a CNAME to # it (platform/gateway/gateway.yaml `external-dns.alpha.kubernetes.io/target`), so a WAN IP change # moves all of them through this one record, with no Git change and no external-dns involvement. # # OWNERSHIP SPLIT WITH EXTERNAL-DNS. This record carries no external-dns TXT registry entry, so # external-dns (policy: sync, owner `prod`) never treats it as its own and never deletes it. # Conversely this updater manages only the names in DOMAINS and never touches external-dns's records. # # IP DETECTION. Default `cloudflare.trace`: an HTTPS request to Cloudflare's trace endpoint, which # reports the address the request arrived from. Correct here only because this pod is NOT in the # `vpn` egress class — the CiliumEgressGatewayPolicy (platform/vpn-egress) selects pods by # `homelab.keithjackson.dev/egress-class: vpn` alone, cluster-wide. Labelling this pod `vpn` would # publish the VPN exit IP instead of the WAN IP. # # TOKEN. Reuses this namespace's `cloudflare-api-token` (same Zone:DNS:Edit token as external-dns # and cert-manager) — no new secret. # # TIER NOTE. This lives in the gating `platform` Flux Kustomization (wait: true) next to # external-dns. If it ever crashloops, `platform` goes not-Ready and apps stop reconciling (no # outage). Kept here rather than in its own non-gating Kustomization to avoid a clusters/** change. apiVersion: apps/v1 kind: Deployment metadata: name: cloudflare-ddns namespace: external-dns labels: app.kubernetes.io/name: cloudflare-ddns spec: replicas: 1 # exactly one writer for the record revisionHistoryLimit: 3 strategy: type: Recreate # never two updaters at once selector: matchLabels: app.kubernetes.io/name: cloudflare-ddns template: metadata: labels: app.kubernetes.io/name: cloudflare-ddns spec: automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: cloudflare-ddns image: favonia/cloudflare-ddns:1.17.1 imagePullPolicy: IfNotPresent env: - name: CLOUDFLARE_API_TOKEN valueFrom: secretKeyRef: name: cloudflare-api-token key: api-token - name: DOMAINS value: "wan.${BASE_DOMAIN}" - name: IP6_PROVIDER value: "none" # IPv4 WAN only; leaves any AAAA alone - name: PROXIED value: "false" # DNS-only: Plex/Gitea/etc. terminate TLS on the Gateway - name: UPDATE_CRON value: "@every 5m" - name: DELETE_ON_STOP value: "false" # a pod restart must not blank the record every CNAME depends on - name: RECORD_COMMENT value: "managed by k3s cloudflare-ddns (platform/externaldns/ddns.yaml)" - name: EMOJI value: "false" resources: requests: cpu: 5m memory: 16Mi limits: memory: 64Mi securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL