Skip to content

Mint the Cloudflare token

nerthus-config reads and writes Cloudflare through one scoped API token: named tunnels, DNS records, Access applications and R2 buckets, all of it, and nothing else. Minting it is a root act — it happens at the Cloudflare account, by a person, at a browser — so it is a runbook rather than a command. The life of a credential owns the model this row belongs to.

Why one token, and why not the account token

The estate holds the smallest set of credentials that separates read from write and machine from root. A token per repository, per zone or per job is a set nobody can audit; one token for everything is one blast radius. So: one token per purpose, and Cloudflare is one purpose.

The account-level token that can mint other tokens is root and nerthus-config never reads it. It lives in a person's keychain or hardware key, outside the vault, off every host and out of CI, and it rotates by hand on its own ceiling. The token below is what it mints.

What the token carries

Create it at My Profile → API Tokens → Create Token → Create Custom Token.

Permission Scope Why
Account → Cloudflare Tunnel → Edit the estate's account create, read and delete named tunnels and their ingress
Account → Access: Apps and Policies → Edit the estate's account the admin application in front of each host's admin path
Account → Workers R2 Storage → Edit the estate's account the backup bucket and its lifecycle
Account → Account Settings → Read the estate's account resolve which account, so nothing has to be configured
Zone → DNS → Edit the estate's zones the records that point at the tunnels
Zone → Zone → Read the estate's zones find the zone a record is in, by name

Two of those are reads that exist so the tool needs no configuration: with Account Settings: Read it asks which account it can see and uses that one, and with Zone: Read it finds the zone a <type>:<name> record belongs to instead of being told. Leave either out and the tool still works — it says which permission it lacked, and NERTHUS_CONFIG_CLOUDFLARE_ACCOUNT names the account by hand — but every account-scoped resource reads unknown until you do.

Set Client IP Address Filtering only if every host that will hold this token has a fixed address; a plan from a laptop then fails from anywhere else, which is a good trade for a token that can delete DNS and a bad one for a token a rented box needs.

Set a TTL. It is the credential's ceiling, and a ceiling is what makes rotation a date rather than a decision.

Where it goes

The value is stored in the instance's own sops file, under the name CLOUDFLARE_API_TOKEN:

printf '%s' "$TOKEN" | nerthus secrets set CLOUDFLARE_API_TOKEN

Nothing prints it back. nerthus-config reads it from there, registers it with the redaction filter before the first call, and hands it to the provider per call; the provider's own log lines print <credential cloudflare-scoped-token> and never the value.

It never goes into a CI variable. CI plans and never applies, and the two read-only tokens CI may hold are separate and read-only — this one can write.

Its inventory row

Every access-granting value in the estate has a row saying who issued it, who holds it, when it expires and what a leak costs. This token's:

Field Value
name cloudflare-scoped-token
issuer the Cloudflare account token (root, outside the vault)
holder kind instance — every instance that runs plan, apply or the drift job
ceiling the TTL set at creation
leak class rotate-and-done — revoking it ends the incident; it grants no data that leaves with the reader

What plan does without it

Nothing silently. A provider is registered only for a credential the machine holds, so with no token every Cloudflare resource reads unknown naming what is missing and nerthus-config plan --against live exits 1. A plan that reported agreement because it asked nobody is the failure this rule exists against — the estate once ran a stale contract for a day and a half while every guard was green.

plan --against state and plan --against recorded answer without any token at all.

Rotating it

  1. Mint the replacement at the account, with the same permissions and a fresh TTL.
  2. nerthus secrets set CLOUDFLARE_API_TOKEN with the new value, on each holder.
  3. Prove it by using it: nerthus-config plan --against live --only 'cloudflare_*' reads the estate and changes nothing. A token is never proved by reading its expiry date — the estate has been told a dead token was good for another 364 days.
  4. Then revoke the old one at the account, and only then.

Rotate secrets owns the rest of the estate's clocks.