build-push: stop a whitespace-tainted REGISTRY_TOKEN failing as "denied" #154

Merged
emig merged 1 commit from fix/registry-login-whitespace into dev 2026-08-01 17:17:55 +00:00
Owner

docker login --password-stdin strips exactly one trailing newline. echo "$T" adds one. So a secret pasted with a trailing newline — what you get copying from a terminal, or from an editor that terminates files with one — arrives as "<token>\n" and the registry answers:

Error response from daemon: Get "https://git.thermograph.org/v2/": denied:

with no further detail. That is indistinguishable from a revoked or wrong token.

On 2026-08-01 this stopped every build and deploy in the estate, and cost an afternoon diagnosing a credential that was in fact valid. Tested by hand against the live registry, the same token returned 200 on /v2/ and was issued a pull,push-scoped registry token for jinemi/thermograph/backend.

Changes

  • Strip leading/trailing whitespace and CR/LF before the pipe; never echo a credential into stdin.
  • The token travels via the environment instead of being interpolated into the script text. ${{ }} is substituted before bash parses the line, so a value containing a quote or newline changes the shape of the command itself, not merely its arguments.
  • Two diagnostics, deliberately asymmetric:
    • empty/unset → hard failure, naming where to set it. There is no case where proceeding helps.
    • wrong shape → warning only (length, and whether it falls outside [0-9a-f]), then attempt the login anyway. A hard assertion on token format would block every build the day Forgejo changes that format — a worse failure than the one being prevented. The warning is enough to turn the registry's opaque denied: into a diagnosis.
  • Neither diagnostic prints the value, only its length and character class.

Verified

The cleaning logic was exercised against a clean token, one with a trailing newline, one with surrounding spaces, an empty value, and a malformed value: the first three all normalise to a valid 40-character token, empty fails fast, malformed warns with its length.

Not the username

Worth recording, because it looked like a candidate. Tested against the live registry, all of admin_emi, emi and jinemi authenticate identically with a valid token, and each is issued a push-scoped token. Forgejo's container registry authenticates on the token, not the username — so the --username value in this file (and in deploy.sh, deploy-stack.sh, and the Terraform module, which still say emi) is cosmetic for auth purposes.

This does not fix the current outage on its ownREGISTRY_TOKEN still has to hold a valid token. It makes the next occurrence diagnose itself instead of presenting as a revoked credential.

`docker login --password-stdin` strips exactly **one** trailing newline. `echo "$T"` adds one. So a secret pasted **with** a trailing newline — what you get copying from a terminal, or from an editor that terminates files with one — arrives as `"<token>\n"` and the registry answers: ``` Error response from daemon: Get "https://git.thermograph.org/v2/": denied: ``` with no further detail. That is indistinguishable from a revoked or wrong token. On 2026-08-01 this stopped **every build and deploy in the estate**, and cost an afternoon diagnosing a credential that was in fact valid. Tested by hand against the live registry, the same token returned `200` on `/v2/` and was issued a `pull,push`-scoped registry token for `jinemi/thermograph/backend`. ## Changes - Strip leading/trailing whitespace and CR/LF before the pipe; never `echo` a credential into stdin. - The token travels via the **environment** instead of being interpolated into the script text. `${{ }}` is substituted before bash parses the line, so a value containing a quote or newline changes the shape of the command itself, not merely its arguments. - Two diagnostics, deliberately asymmetric: - **empty/unset → hard failure**, naming where to set it. There is no case where proceeding helps. - **wrong shape → warning only** (length, and whether it falls outside `[0-9a-f]`), then attempt the login anyway. A hard assertion on token format would block every build the day Forgejo changes that format — a worse failure than the one being prevented. The warning is enough to turn the registry's opaque `denied:` into a diagnosis. - Neither diagnostic prints the value, only its length and character class. ## Verified The cleaning logic was exercised against a clean token, one with a trailing newline, one with surrounding spaces, an empty value, and a malformed value: the first three all normalise to a valid 40-character token, empty fails fast, malformed warns with its length. ## Not the username Worth recording, because it looked like a candidate. Tested against the live registry, **all of `admin_emi`, `emi` and `jinemi` authenticate identically** with a valid token, and each is issued a push-scoped token. Forgejo's container registry authenticates on the token, not the username — so the `--username` value in this file (and in `deploy.sh`, `deploy-stack.sh`, and the Terraform module, which still say `emi`) is cosmetic for auth purposes. **This does not fix the current outage on its own** — `REGISTRY_TOKEN` still has to hold a valid token. It makes the next occurrence diagnose itself instead of presenting as a revoked credential.
emig added 1 commit 2026-08-01 17:16:57 +00:00
build-push: stop a whitespace-tainted REGISTRY_TOKEN failing as "denied"
All checks were successful
shell-lint / shellcheck (pull_request) Successful in 15s
PR build (required check) / changes (pull_request) Successful in 20s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
secrets-guard / encrypted (pull_request) Successful in 27s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 4s
shell-lint / shellcheck (push) Successful in 7s
secrets-guard / encrypted (push) Successful in 15s
d6553a7a05
`docker login --password-stdin` strips exactly ONE trailing newline. `echo
"$T"` adds one. So a secret pasted WITH a trailing newline -- which is what
copying from a terminal, or an editor that terminates files with one, produces
-- arrives at the registry as "<token>\n" and gets back:

    Error response from daemon: Get "https://.../v2/": denied:

with no further detail. That is indistinguishable from a revoked or wrong
token. On 2026-08-01 it stopped every build and deploy in the estate and cost
an afternoon of diagnosis on a credential that was in fact valid: the same
token, tested by hand, returned 200 on /v2/ and was issued a pull,push-scoped
registry token for jinemi/thermograph/backend.

So: strip leading/trailing whitespace and CR/LF before the pipe, and never
`echo` a credential into stdin.

The token now travels through the ENVIRONMENT rather than being interpolated
into the script text. `${{ }}` is substituted before bash parses the line, so a
value containing a quote or a newline changes the shape of the command itself,
not merely its arguments.

Two diagnostics, deliberately asymmetric:

  * empty/unset  -> HARD FAILURE naming where to set it. There is no case where
    proceeding helps.
  * wrong shape  -> WARNING only (length, and whether it is outside [0-9a-f]),
    then attempt the login anyway. A hard assertion on token format would block
    every build the day Forgejo changes that format, which is a worse failure
    than the one being prevented. The warning is enough to turn the registry's
    opaque "denied:" into a diagnosis.

Neither diagnostic prints the value; only its length and character class.

Note the username is not a factor: tested against the live registry, all of
admin_emi, emi and jinemi authenticate identically with a valid token and are
each issued a push-scoped token. Forgejo's container registry authenticates on
the token, not the username.
emig merged commit d6553a7a05 into dev 2026-08-01 17:17:55 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Jinemi/thermograph#154
No description provided.