Publishing the name was a block of printed instructions, which put the knowledge in whoever last read them. It is now a step like every other: absent records are created, correct ones are reported, and a name that points somewhere else is left alone with a pointer to public-dns.md §4. `--skip-dns` opts out. The apex takes a CNAME to the site indirection, not an A record. Cloudflare flattens it, and it buys the property §2 is about -- a WAN address change is one record rather than a hunt across zones. bansko.io already does this; the claim in public-dns.md §3 that the fleet's apexes use A records was wrong and is corrected there. Note that `dig` cannot tell you which a zone uses, because flattening makes both answer with an A; only the API can. The script lives in script/publish-dns.sh and is piped to the proxy rather than embedded as a heredoc. Quoting shell through an unquoted heredoc silently mangled two earlier versions of this same code -- once producing `tr -d "'" ")` -- and a file can be linted and run directly. It executes on the proxy because that is where the Cloudflare token is, and the token is never copied off that host: it can rewrite DNS for every zone on the account. Token parsing uses awk rather than a PCRE lookbehind. Widening the lookbehind to tolerate variable spacing made grep fail outright -- lookbehinds must be fixed-length -- so the fixed-width form would have broken silently the day someone reformatted the credentials file. Verified: first run created both records, second reported them ok, rustingface.com and www both resolve to the site address, and the Let's Encrypt chain validates against the public trust store. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZG2i4AmfSqE97EJGBVb64
77 lines
3.3 KiB
Bash
Executable File
77 lines
3.3 KiB
Bash
Executable File
#!/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 <apex> <site-indirection> <certbot-credentials>
|
|
#
|
|
# 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"
|