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:
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¶
- Mint the replacement at the account, with the same permissions and a fresh TTL.
nerthus secrets set CLOUDFLARE_API_TOKENwith the new value, on each holder.- 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. - Then revoke the old one at the account, and only then.
Rotate secrets owns the rest of the estate's clocks.