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
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
`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.
This commit is contained in:
parent
486a194086
commit
d6553a7a05
1 changed files with 37 additions and 1 deletions
|
|
@ -110,12 +110,48 @@ jobs:
|
||||||
|
|
||||||
- name: Log in to the Forgejo registry
|
- name: Log in to the Forgejo registry
|
||||||
if: steps.image.outputs.changed == 'true'
|
if: steps.image.outputs.changed == 'true'
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# NOT secrets.GITHUB_TOKEN -- Forgejo's per-job auto-injected token can
|
# NOT secrets.GITHUB_TOKEN -- Forgejo's per-job auto-injected token can
|
||||||
# never push to the container registry (a known Forgejo limitation,
|
# never push to the container registry (a known Forgejo limitation,
|
||||||
# independent of any permission setting). Needs a manually provisioned
|
# independent of any permission setting). Needs a manually provisioned
|
||||||
# PAT with write:package scope, stored as REGISTRY_TOKEN.
|
# PAT with write:package scope, stored as REGISTRY_TOKEN.
|
||||||
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ steps.image.outputs.host }}" \
|
#
|
||||||
|
# WHITESPACE IS THE FAILURE THIS GUARDS AGAINST, and it is a nasty one.
|
||||||
|
# `docker login --password-stdin` strips exactly ONE trailing newline.
|
||||||
|
# `echo "$T"` adds one. So a secret pasted WITH a trailing newline --
|
||||||
|
# which is what happens if you copy from a terminal or an editor that
|
||||||
|
# ends files with one -- arrives as "<token>\n" and the registry answers
|
||||||
|
#
|
||||||
|
# Error response from daemon: Get "https://.../v2/": denied:
|
||||||
|
#
|
||||||
|
# with no further detail. That is indistinguishable from a revoked or
|
||||||
|
# wrong token, and it cost an afternoon on 2026-08-01 chasing a
|
||||||
|
# credential that was in fact valid. Strip first, echo never.
|
||||||
|
#
|
||||||
|
# The token also goes 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 newline would change
|
||||||
|
# the shape of the command itself rather than just its arguments.
|
||||||
|
tok=$(printf '%s' "$REGISTRY_TOKEN" | tr -d ' \t\r\n')
|
||||||
|
if [ -z "$tok" ]; then
|
||||||
|
echo "::error::REGISTRY_TOKEN is empty or unset. Set it at" \
|
||||||
|
"Settings -> Actions -> Secrets on this repository."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Shape check is a WARNING, not a failure: a hard assertion on the token
|
||||||
|
# format would block every build the day Forgejo changes it. But saying
|
||||||
|
# so up front turns the registry's opaque "denied:" into a diagnosis.
|
||||||
|
# Length and character class only -- the value is never printed.
|
||||||
|
case "$tok" in
|
||||||
|
*[!0-9a-f]*) echo "::warning::REGISTRY_TOKEN contains characters outside" \
|
||||||
|
"[0-9a-f]; a Forgejo PAT is 40 hex characters. If the" \
|
||||||
|
"login below fails, re-paste the secret." ;;
|
||||||
|
esac
|
||||||
|
[ "${#tok}" -eq 40 ] || echo "::warning::REGISTRY_TOKEN is ${#tok} characters," \
|
||||||
|
"expected 40. If the login below fails, re-paste the secret."
|
||||||
|
printf '%s' "$tok" | docker login "${{ steps.image.outputs.host }}" \
|
||||||
--username admin_emi --password-stdin
|
--username admin_emi --password-stdin
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue