Gateway-published records now CNAME to wan.${BASE_DOMAIN}, which a
favonia/cloudflare-ddns Deployment keeps at the home WAN's current public
IP. Operator decision: every Gateway-published name goes public; internal
services (controller: none) remain unpublished.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
88 lines
3.5 KiB
YAML
88 lines
3.5 KiB
YAML
---
|
|
# 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
|