--- # Keeps home.kube.huskypup.net pointed at this cluster's current public IP in # Cloudflare. This is the health-check target the VPS's DNS failover watcher # (scripts/vps-dns-failover.sh) uses to decide whether home is reachable - # it only needs to run while home is up, which is exactly when it can run. # # Reuses the cloudflare-token-secret already wired via ExternalSecret for # cert-manager's DNS-01 solver (see secret-cf-token.yaml) - same zone, same # token, no new secret plumbing. apiVersion: v1 kind: ConfigMap metadata: name: home-ip-ddns-script namespace: cert-manager data: update.sh: | #!/bin/sh set -eu ZONE_NAME="huskypup.net" # Cloudflare zone is the parent domain - kube.huskypup.net is just a record within it, not its own zone RECORD_NAME="home.kube.huskypup.net" TOKEN="$(cat /etc/cf/cloudflare-token)" CURRENT_IP="$(curl -sf https://cloudflare.com/cdn-cgi/trace | grep -o '^ip=.*' | cut -d= -f2)" if [ -z "$CURRENT_IP" ]; then echo "ERROR: could not determine public IP" exit 1 fi ZONE_ID="$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ "https://api.cloudflare.com/client/v4/zones?name=${ZONE_NAME}" \ | jq -r '.result[0].id')" RECORD_JSON="$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${RECORD_NAME}&type=A")" RECORD_ID="$(echo "$RECORD_JSON" | jq -r '.result[0].id // empty')" EXISTING_IP="$(echo "$RECORD_JSON" | jq -r '.result[0].content // empty')" if [ "$EXISTING_IP" = "$CURRENT_IP" ]; then echo "home.kube.huskypup.net already up to date (${CURRENT_IP})" exit 0 fi BODY="{\"type\":\"A\",\"name\":\"${RECORD_NAME}\",\"content\":\"${CURRENT_IP}\",\"ttl\":120,\"proxied\":false}" if [ -n "$RECORD_ID" ]; then echo "Updating ${RECORD_NAME}: ${EXISTING_IP} -> ${CURRENT_IP}" curl -sf -X PATCH -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \ -d "$BODY" \ "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" >/dev/null else echo "Creating ${RECORD_NAME} -> ${CURRENT_IP}" curl -sf -X POST -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \ -d "$BODY" \ "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" >/dev/null fi echo "done" --- apiVersion: batch/v1 kind: CronJob metadata: name: home-ip-ddns namespace: cert-manager spec: schedule: "*/10 * * * *" # every 10 minutes; cheap, and only matters while home is up concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: backoffLimit: 2 template: spec: restartPolicy: Never securityContext: runAsNonRoot: true runAsUser: 65534 seccompProfile: type: RuntimeDefault containers: - name: ddns-update image: alpine/k8s:1.32.13 # already has curl + jq (see MEMORY.md kubectl image note) command: ["/bin/sh", "/scripts/update.sh"] securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumeMounts: - name: script mountPath: /scripts - name: cf-token mountPath: /etc/cf readOnly: true volumes: - name: script configMap: name: home-ip-ddns-script defaultMode: 0755 - name: cf-token secret: secretName: cloudflare-token-secret items: - key: cloudflare-token path: cloudflare-token