For DNS Managers. It covers the one time setup and the things that go wrong.
Do these in order. The order matters twice over. A required check that has
never run on main blocks every merge. And the zone files do not yet hold what
Cloudflare holds, so the drift check fails until the first sync pull request
lands.
The zone files are nearly empty and Cloudflare is not. As of this writing,
witcc.devhas 14 records in Cloudflare and one in the zone file.hackwit.orghas 4 records in Cloudflare and none in the zone file. Those include theMX, SPF, DMARC and DKIM records for club email. Step 3 is what fixes this. Do not run./bin/sync --forcebefore it.
| Secret | What it is | Used by |
|---|---|---|
CLOUDFLARE_TOKEN |
Cloudflare API token, edit DNS for both zones | deploy |
CLOUDFLARE_TOKEN_READ_ONLY |
Cloudflare API token, read DNS for both zones | plan, sync-from-cloudflare |
DNS_BOT_PRIVATE_KEY |
Private key of the WITCC DNS Bot GitHub App, the whole .pem |
sync-from-cloudflare |
There is also one repository variable, not a secret:
| Variable | What it is |
|---|---|
DNS_BOT_CLIENT_ID |
Client ID of the same App. An identifier, not a credential |
The two Cloudflare tokens already exist. Create them at
Cloudflare > My Profile > API Tokens with the Edit zone DNS template, and
scope each one to witcc.dev and hackwit.org only.
Rotate
CLOUDFLARE_TOKEN_READ_ONLYonce. The workflow it replaces ran scripts from a pull request while holding it, so anybody who opened a pull request could have read it. SeeSECURITY.md.
The nightly sync opens its pull request as a GitHub App. Two reasons, and both of them rule out the simpler options:
- Not
GITHUB_TOKEN. GitHub does not start workflows for commits pushed with the built in token, so that pull request would get no checks and could never satisfy a required check. - Not a person's token. Nobody can approve their own pull request. If the sync ran on a DNS Manager's personal access token, every sync pull request would be authored by that person and they could never review it. On a two person team that leaves exactly one possible reviewer.
An App is neither. It triggers checks, and it is not a person, so anybody on the team can approve its pull requests. It also has no expiry to forget.
-
Go to https://github.com/organizations/WITCodingClub/settings/apps/new.
-
Name:
WITCC DNS Bot. Homepage URL: this repository is fine. -
Turn Webhook > Active off. The App never receives events.
-
Repository permissions:
Contents: Read and write,Pull requests: Read and write,Issues: Read and write. Nothing else. -
Where can this App be installed: only this account.
-
Create it, then Generate a private key. A
.pemdownloads. -
Install App on the left, install it on
WITCodingClub/dnsonly. The App can do nothing until this step. -
Record the client id and the key:
$ gh variable set DNS_BOT_CLIENT_ID --repo WITCodingClub/dns --body 'Iv23...' $ gh secret set DNS_BOT_PRIVATE_KEY --repo WITCodingClub/dns < ~/Downloads/witcc-dns-bot.*.pem
The client id is on the App's settings page. The private key is the whole
.pemfile,BEGINandENDlines included. -
Delete the
.pemfrom your Downloads folder.
To check that it worked:
$ gh api orgs/WITCodingClub/installations --jq '.installations[].app_slug'
$ gh workflow run sync-from-cloudflare.yml && gh run watchThe dns-managers team needs write access or better on this repository.
GitHub ignores a CODEOWNERS entry for a team that cannot write.
$ gh api orgs/WITCodingClub/teams/dns-managers/repos/WITCodingClub/dns \
--jq .permissionsRun the nightly sync by hand, then review and merge the pull request it opens. Expect it to be large. Cloudflare holds the real records today.
$ gh workflow run sync-from-cloudflare.yml
$ gh run watchGive every record marked TODO owner unknown an owner before you merge.
Once it has merged, confirm that Cloudflare and main agree:
$ export CLOUDFLARE_TOKEN=... # the read-only token
$ ./bin/planIt should print ## No changes were planned. Until it does, the
cloudflare in sync check fails on every pull request, which is the point.
This is already applied. Ruleset 23783065 on main requires one
approving review from a code owner, squash merge, resolved review threads, and
these five checks:
yaml syntax, dns records, tools tests, plan the change,
cloudflare in sync.
docs/ruleset-main.json is a copy of it. Restore it with:
$ gh api -X PUT repos/WITCodingClub/dns/rulesets/23783065 \
--input docs/ruleset-main.jsonRead it back with:
$ gh api repos/WITCodingClub/dns/rulesets/23783065 --jq '.rules[] | select(.type=="pull_request" or .type=="required_status_checks")'The ruleset went on before the workflows did.
plan.ymlruns onpull_request_target, which reads the workflow frommain. Until it is onmain,plan the changeandcloudflare in syncnever start, so any pull request open right now waits on checks that cannot run. A repository admin hasbypass_mode: alwaysand can merge the first one anyway. Every pull request after that gets real checks.
The nightly schedule starts by itself once the workflow is on main. There is
nothing else to turn on.
Cloudflare does not match main. Somebody changed DNS in the dashboard, or a
deploy failed.
-
Look at the failed check. It prints the difference.
-
If there is an open
cloudflare-syncpull request, review and merge it. -
If there is not, make one:
$ gh workflow run sync-from-cloudflare.yml -
Re-run the failed check on the blocked pull request.
Do not merge past this check. Merging undoes whatever is in Cloudflare and not
in main.
Read the workflow log. The usual causes:
- A record is not valid. octoDNS names the record and says why.
- The Cloudflare token expired. The log shows a 401 or 403 from the API.
- A root
NSchange. octoDNS refuses these without--force. Cloudflare owns the nameservers for a zone. The zone files must not contain rootNSrecords.tools/merge_live.pyleaves them out for this reason.
The zone files and Cloudflare now disagree. Fix it, do not leave it.
-
Too many deletes. The deploy refuses a plan that deletes more thanMAX_DELETESrecords. Read the plan in the job summary. If every delete is correct, run the deploy by hand:$ gh workflow run deploy.yml -f allow_mass_delete=trueThis guard exists because octoDNS's own guard has a hole. octoDNS refuses a plan that updates or deletes more than 30% of a zone, but only for a zone that already has at least 10 records.
MIN_EXISTING_RECORDSis a constant in octoDNS and cannot be configured.hackwit.orghas fewer records than that, so octoDNS would delete every one of them without complaining. -
TooMuchChange. This is octoDNS's own guard, for a zone with 10 records or more. Read the plan. If the change really is correct, apply it by hand:$ export CLOUDFLARE_TOKEN=... # the edit token $ ./bin/plan # read this first $ ./bin/sync --force
-
Rate limited. octoDNS retries five times and waits ten minutes. Re-run the workflow.
-
The apply half finished. Run the
deployworkflow again. octoDNS works out what is left to do.
- Create the new token in Cloudflare.
- Update the repository secret.
- Run
gh workflow run deploy.ymland confirm it passes. - Delete the old token in Cloudflare.
Add them to the dns-managers team. CODEOWNERS points at the team, so that
is the only step. Check that the team still has write access:
$ gh api orgs/WITCodingClub/teams/dns-managers/repos/WITCodingClub/dns \
--jq .permissionsDelete the record from the zone file in a pull request. The deploy removes it from Cloudflare. Tell the owner first.
$ dig +short TXT octodns-meta.witcc.devThe time= value is when octoDNS last changed that zone.
$ export CLOUDFLARE_TOKEN=... # the read-only token is enough
$ ./bin/dump .live
$ cat .live/witcc.dev.yaml.live/ is ignored by git. It never touches the zone files.
DNS for both zones is in Cloudflare. Cloudflare is the live system. This repository is how we change it, not how it serves.
- Fix the record in the Cloudflare dashboard. The site comes back.
- Post in the club Discord that you did it.
- Run
gh workflow run sync-from-cloudflare.ymland merge the pull request it opens, so the repository catches up.