diff --git a/docs/templates/ci-box/creds.md b/docs/templates/ci-box/creds.md index 0b93495..9afd29b 100644 --- a/docs/templates/ci-box/creds.md +++ b/docs/templates/ci-box/creds.md @@ -1,9 +1,13 @@ - **Creds-free by default.** The box starts with no Forgejo credentials and no git credentials. The runner binary is installed but **not registered**: - registration needs a short-lived token the operator mints in Forgejo - (Site Administration, org, or repo → Actions → Runners) and hands to - `sudo rig forgejo-runner install --instance `. The token is consumed at - registration and never written to disk by rig. After that, the runner's own - credential lives in `~/forgejo-runner/.runner`, mode 0600 — never copy it, - print it, or commit it. Secrets that CI jobs need belong in Forgejo's repo or - org secrets, injected per job, not on this box. + registration needs a token the operator mints in Forgejo (Site + Administration, org, or repo → Actions → Runners) and hands to + `sudo rig forgejo-runner install --instance `. rig never writes that + token to disk — but it does **not expire, and registering does not spend + it**. A Forgejo registration token stays valid until somebody mints a + replacement at that same scope, and it will register as many runners as it is + shown to. Treat a leaked one as live until it has been replaced. (GitHub's + equivalent expires in about an hour; do not carry that habit across.) After + registration the runner's own credential lives in `~/forgejo-runner/.runner`, + mode 0600 — never copy it, print it, or commit it. Secrets that CI jobs need + belong in Forgejo's repo or org secrets, injected per job, not on this box. diff --git a/docs/templates/ci-box/install.sh b/docs/templates/ci-box/install.sh index 6229b73..4210676 100755 --- a/docs/templates/ci-box/install.sh +++ b/docs/templates/ci-box/install.sh @@ -3,10 +3,20 @@ # TENANT_USER/TENANT_HOME/TENANT_GROUP/ROLE exported. # # This lands the BINARY ONLY. Registration is deliberately not here: it needs a -# short-lived token from the Forgejo instance, and a tenant install is +# registration token from the Forgejo instance, and a tenant install is # creds-free by contract — box auto-runs it at mint, holding nothing. The # operator registers afterwards, out loud: # +# Deliberately NOT described the way the GitHub sibling describes its own +# registration token, which really does expire in about an hour. That adjective +# must not cross this forge boundary — a test pins its absence from this file +# and from creds.md, so do not reintroduce it by copying from `rig runner`. +# Forgejo's ActionRunnerToken carries no expiry field at all; +# NewRunnerToken invalidates prior tokens only when a replacement is minted at +# the same scope, and Register leaves the one it was handed active. It is +# reusable until replaced, so a leak stays live. See creds.md, which is the +# copy an agent inside the box actually reads. +# # box shell ci-box # sudo rig forgejo-runner install --instance https://forgejo.example.com # diff --git a/test/cli.sh b/test/cli.sh index 45556da..46ade38 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -3190,6 +3190,18 @@ check "forgejo-runner: --instance needs a value" 2 "needs a value" "$FR" - check "forgejo-runner: unknown flag exits 2" 2 "unknown flag" "$FR" --instance https://f.example.com --nope check "forgejo-runner: empty --labels refused" 2 "must not be empty" "$FR" --instance https://f.example.com --labels '' check "forgejo-runner: the runner user is never root" 2 "must not be root" "$FR" --instance https://f.example.com --user root +# The pin rig RESOLVES was charset-checked; the pin it is HANDED was not, and +# that one is the one that reaches a URL path. Refused by name at parse time. +check "forgejo-runner: --version refuses a path, not a release number" 2 "release number like" \ + "$FR" --instance https://f.example.com --version ../../etc/passwd +check "forgejo-runner: --version refuses a non-numeric pin" 2 "release number like" \ + "$FR" --instance https://f.example.com --version latest +# Reaching the root check is the proof a good pin got THROUGH validation: this +# runs as a normal user in CI, so "must run as root" is the next gate down. +check "forgejo-runner: a plain release number passes validation" 1 "must run as root" \ + "$FR" --instance https://f.example.com --version 12.13.2 +check "forgejo-runner: a leading v is stripped before that check" 1 "must run as root" \ + "$FR" --instance https://f.example.com --version v12.13.2 # A schemeless host and a repo URL are the two ways an operator mis-states the # instance, and only one of them would fail loudly on its own — a repo URL # registers somewhere subtly wrong instead. Both refuse by name. @@ -3384,6 +3396,32 @@ check "ci-box: no warn-and-continue checksum branch either" 1 "" \ check "forgejo-runner: install routes through the shared checksum policy" 0 "fetch_and_verify_sha256" \ grep -o "fetch_and_verify_sha256 \"\$URL\"" "$FR" +# --- GitHub's token adjective must not cross the forge boundary ------------- +# Measured in Forgejo's own source, not inferred: ActionRunnerToken carries NO +# expiry field; NewRunnerToken invalidates prior tokens only when a replacement +# is minted at the same scope; Register leaves the token it was handed active. +# Reusable until replaced — where GitHub's expires in about an hour. +# +# This is pinned rather than merely fixed because creds.md is spliced into the +# ci-box's own CONTEXT.md: it is the text an AGENT INSIDE THE BOX reads about +# its own credentials. "short-lived" there makes a leaked token look +# self-expiring while it is still registering runners. The wording arrived by +# being copied from the GitHub sibling, so the same copy can bring it back. +CICREDS="$ROOT/docs/templates/ci-box/creds.md" +check "ci-box: creds.md never calls the registration token short-lived" 1 "" \ + grep -qi "short-lived" "$CICREDS" +# Plain absence, not a phrase match: the wording this replaced wrapped across +# two comment lines, so a phrase pin would have passed against the very text it +# was written to catch. The file explains the ban without spelling the word. +check "ci-box: the install header does not call it short-lived either" 1 "" \ + grep -qi "short-lived" "$CIBOX" +# ...and says the true thing, so the pin cannot be satisfied by deleting the +# claim rather than correcting it. +check "ci-box: creds.md states the token does not expire" 0 "not expire" \ + grep -o "not expire" "$CICREDS" +check "ci-box: creds.md states a leaked token stays live" 0 "leaked" \ + grep -o "leaked" "$CICREDS" + # --- --version must converge, not be swallowed (review !110) ---------------- # forgejo-runner does NOT self-update, and a ci-box's template preinstalls the # binary at mint — so a bare presence check would make --version dead on the