#!/usr/bin/env bash # # Create the public DNS records for a WAN-facing name, if they are absent. # # Runs ON THE EDGE PROXY, piped in by infra-setup.sh, because that is where the # Cloudflare credential lives. The token can rewrite DNS for every zone on the # account — including MX records for domains whose mail we host — so it is # never copied off that host (public-dns.md §1). # # sudo bash publish-dns.sh # # Idempotent: creates what is missing, confirms what is already correct, and # refuses to touch a name that points somewhere else. # # A standalone file rather than a heredoc inside infra-setup.sh: quoting a # script through a heredoc has silently mangled this project's shell twice, # and a file can be read, linted and run directly. set -euo pipefail apex=${1:?apex domain required} site=${2:?site indirection name required} credentials=${3:?path to the certbot cloudflare credentials required} # Parsed with awk rather than a PCRE lookbehind: a lookbehind must be # fixed-length, so it cannot tolerate variable spacing around the `=`, and the # fixed-width form fails silently the day someone reformats the file. token=$(awk -F= '/dns_cloudflare_api_token/ { sub(/^[^=]*=/, "", $0) gsub(/[[:space:]"'"'"']/, "", $0) print exit }' "$credentials") [ -n "$token" ] || { echo "no Cloudflare token found in $credentials" >&2; exit 1; } base=https://api.cloudflare.com/client/v4 api() { curl -sS -H "Authorization: Bearer $token" -H 'Content-Type: application/json' "$@"; } zid=$(api "$base/zones?name=$apex" | jq -r '.result[0].id // empty') [ -n "$zid" ] || { echo "$apex is not a zone on this Cloudflare account" >&2; exit 1; } # A CNAME at the apex is fine: Cloudflare flattens it. Pointing every public # name at the per-site indirection means a WAN address change is one record # rather than a hunt across zones. ensure_cname() { local name=$1 target=$2 existing # Ask for every address-type record on the name, not just the type # expected. A name that is already a CNAME does not appear in an A-record # query, and creating an A beside it is a conflict at best # (public-dns.md §4). MX and TXT are excluded on purpose: they coexist with # an address record and are never a reason to skip or to overwrite. existing=$(api "$base/zones/$zid/dns_records?name=$name" | jq -r '.result[] | select(.type == "A" or .type == "AAAA" or .type == "CNAME") | "\(.type) \(.content)"') if [ -z "$existing" ]; then api -X POST "$base/zones/$zid/dns_records" \ --data "$(jq -cn --arg n "$name" --arg c "$target" \ '{type: "CNAME", name: $n, content: $c, ttl: 1, proxied: false, comment: "rustingface"}')" | jq -r 'if .success then " created \(.result.name) -> \(.result.content)" else " FAILED \(.errors)" end' elif [ "$existing" = "CNAME $target" ]; then echo " ok $name -> $target" else echo " !! $name already points elsewhere:" >&2 echo "$existing" | sed 's/^/ /' >&2 echo " leaving it alone. Repointing a live name is deliberate work;" >&2 echo " read public-dns.md §4 first." >&2 fi } ensure_cname "$apex" "$site" ensure_cname "www.$apex" "$site"