From 0c6ee4921d853052c8764fae2fa3e86a598b2f3c Mon Sep 17 00:00:00 2001 From: Keith Jackson Date: Fri, 30 Oct 2026 01:55:39 -0500 Subject: [PATCH] fix(renovate): document and probe the five token scopes Renovate needs; lint daemon-bound actions Run 350 cleared platform init (/user 200, /version 200, /repos 200), then stopped at GET /api/v1/orgs/Home-Network with Gitea's own 403: token does not have at least one of required scope(s), required=[read:organization], token scope=read:package,write:repository,read:user The workflow's header listed only those three scopes, and the operator minted exactly that. Renovate's initRepo -> isOrg needs read:organization, and Gitea files /issues and /labels under the issue scope, which the dashboard and PR labels need: write:issue. - renovate.yaml: corrects the scope list (read:user, read:organization, write:repository, write:issue, read:package), with the call each serves. A new preflight step probes them with GETs, the token passed to curl on stdin, and names a missing scope instead of Renovate's bare "Authentication failure". - ci/lint.sh: rejects workflow `uses:` lines that need a Docker daemon (docker://, renovatebot/github-action, docker/*-action), the class fixed in #84. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/renovate.yaml | 95 ++++++++++++++++++++-- ci/lint.sh | 16 ++++ decisions/2026-10-30-renovate-auth.md | 108 ++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 decisions/2026-10-30-renovate-auth.md diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml index 3e984e3..50d57d6 100644 --- a/.gitea/workflows/renovate.yaml +++ b/.gitea/workflows/renovate.yaml @@ -60,9 +60,21 @@ # entry's "What I did NOT verify" — that gap is stated, not papered over. # # OPERATOR PREREQUISITE — ONE SECRET, NOT COMMITTED HERE: -# `RENOVATE_TOKEN` — a Gitea access token for the bot account, scopes `write:repository`, -# `read:user` and (since 2026-10-29) `read:package`. Set it as an Actions secret on this -# repository. Nothing in this repo mints, +# `RENOVATE_TOKEN` — a Gitea access token for the bot account (`renovate-bot`) with FIVE scopes. +# Each is required by a call Renovate 44.x makes against Gitea 1.24.3, and Gitea refuses a +# token missing one with `403 token does not have at least one of required scope(s)`: +# read:user GET /user (platform init: initPlatform) +# read:organization GET /orgs/{owner}, /orgs/{o}/labels (initRepo -> isOrg, then org labels) +# write:repository clone/push branches, /pulls, /statuses +# write:issue /issues, /labels, /issues/{n}/labels (dependency dashboard, PR labels; +# Gitea files all of these under the ISSUE scope, not repository) +# read:package the Gitea container registry (RENOVATE_HOST_RULES below) +# CORRECTED 2026-10-30. This list used to name only write:repository + read:user (+ read:package), +# and the operator minted exactly that. Run 350 then failed at +# `GET /api/v1/orgs/Home-Network 403 ... required=[read:organization], token scope=read:package, +# write:repository,read:user`; write:issue would have been the next wall. The preflight below now +# PROBES these scopes, so a short token fails in seconds with the missing scope named. +# Set it as an Actions secret on this repository. Nothing in this repo mints, # stores or references its value; it is consumed by name only. # Until it exists, this workflow FAILS LOUDLY on every scheduled run. That is intended: a # visible red is the correct state for "automation configured but not credentialed". @@ -161,10 +173,10 @@ jobs: That is the failure mode that let renovate.json sit inert in this repo for months while seven files claimed "Renovate maintains this pin". - TO FIX: create a Gitea access token for the bot account with scopes - `write:repository`, `read:user` and `read:package` (the last one lets Renovate read - this repo's own images in the Gitea registry), then add it to this repository as the - Actions secret RENOVATE_TOKEN. Do not commit it anywhere. + TO FIX: create a Gitea access token for the bot account (renovate-bot) with scopes + read:user, read:organization, write:repository, write:issue and read:package (the + header of .gitea/workflows/renovate.yaml says which call needs which), then add it to + this repository as the Actions secret RENOVATE_TOKEN. Do not commit it anywhere. MSG exit 1 fi @@ -172,6 +184,75 @@ jobs: # Proves the step is running INSIDE the pinned Renovate image, not the default job image. echo "renovate $(renovate --version)" + - name: Preflight - the token has every scope Renovate needs + shell: bash + env: + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} + # ADDED 2026-10-30. Renovate reports EVERY platform-init failure as the bare + # "Authentication failure" (lib/modules/platform/gitea/index.ts initPlatform swallows the + # HTTP error), and a later scope gap surfaces as "Repository has unknown error". Two + # dispatches were spent learning one missing scope at a time. These probes are GETs only, + # and they name the missing scope. The token never reaches argv: curl reads it from stdin + # (`--config -`), so it is not in `ps` output or in the log. + run: | + set -uo pipefail + GITEA=https://gitea.home.keithjackson.dev + REPO=Home-Network/home-network-k3s-impl + fail=0 + body=$(mktemp) + probe() { # + local code + if [ "$3" = basic ]; then + code=$(printf 'user = "renovate:%s"\n' "$RENOVATE_TOKEN" \ + | curl -sS --config - -o "$body" -w '%{http_code}' "$GITEA$2") || code=000 + else + code=$(printf 'header = "Authorization: Bearer %s"\n' "$RENOVATE_TOKEN" \ + | curl -sS --config - -o "$body" -w '%{http_code}' "$GITEA$2") || code=000 + fi + if [ "$code" = 200 ]; then + echo "ok $1 (GET ${2%%\?*} -> 200)" + return + fi + fail=1 + echo "::error::scope $1: GET ${2%%\?*} -> $code" + case "$code" in + 401) echo " 401 = Gitea rejected the token VALUE (wrong, expired or revoked)." ;; + 403) if grep -q 'Access denied' "$body"; then + echo " 403 'Access denied' = a Cilium/Envoy policy denial, not Gitea." + else + echo " Gitea: $(jq -r '.message // empty' "$body" 2>/dev/null | head -c 300)" + fi ;; + *) echo " body: $(head -c 200 "$body")" ;; + esac + } + probe read:user "/api/v1/user" bearer + probe read:organization "/api/v1/orgs/${REPO%%/*}" bearer + probe read:repository "/api/v1/repos/$REPO" bearer + probe read:issue "/api/v1/repos/$REPO/labels?limit=1" bearer + probe read:package "/v2/token?service=container_registry&scope=repository:home-network/toolbox:pull" basic + # WRITE scopes cannot be proven by a GET. Gitea's scope-refusal message, however, lists + # the token's scopes (names only, never the value): ask for an admin route this bot + # cannot have, and read `token scope=` from the 403. Advisory if the format changes. + printf 'header = "Authorization: Bearer %s"\n' "$RENOVATE_TOKEN" \ + | curl -sS --config - -o "$body" "$GITEA/api/v1/admin/orgs?limit=1" || true + scopes=$(jq -r '.message // empty' "$body" 2>/dev/null | sed -n 's/.*token scope=//p') + if [ -n "$scopes" ]; then + echo "token scopes (from Gitea): $scopes" + for want in write:repository write:issue; do + case ",$scopes," in + *",$want,"*|*",all,"*) echo "ok $want" ;; + *) echo "::error::scope $want is missing (token scope=$scopes)"; fail=1 ;; + esac + done + else + echo "::warning::could not read the token's scope list; write:repository and write:issue are UNVERIFIED" + fi + rm -f "$body" + if [ "$fail" -ne 0 ]; then + echo "::error::RENOVATE_TOKEN failed the checks above (a 401 is the value, a 403 is a scope). Re-mint it for renovate-bot with read:user, read:organization, write:repository, write:issue, read:package and overwrite the secret." + exit 1 + fi + - name: Run Renovate shell: bash # The repo config is discovered by Renovate from the cloned repo (renovate.json5, diff --git a/ci/lint.sh b/ci/lint.sh index e5307fe..39aaf86 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -42,3 +42,19 @@ while IFS=: read -r wf _ value; do done < <(grep -nE '^[[:space:]]*configurationFile:' .gitea/workflows/*.y*ml 2>/dev/null || true) [ "$missing" -eq 0 ] || exit 1 ok "workflow configurationFile references resolve" + +# A workflow must not use an action that needs a Docker daemon: jobs on this runner get NONE, by +# decision (decisions/2026-10-29-image-pipeline.md; act_runner maps docker_host to `-`). Such an +# action is valid YAML and passes every other stage, then dies at runtime with +# "failed to connect to the docker API at unix:///var/run/docker.sock". renovatebot/github-action +# did exactly that on every Renovate run until 2026-10-30 (run 343). Run the tool's image as the +# job `container:` instead (see .gitea/workflows/renovate.yaml). Container (`docker://`) actions +# and Docker's own daemon-bound actions are rejected for the same reason. Comments are ignored. +daemon_uses='uses:[[:space:]]*["'\'']?(docker://|renovatebot/github-action|docker/(build-push|setup-buildx|setup-qemu|login)-action)' +bad=0 +while IFS= read -r hit; do + err "${hit%%:*}: needs a Docker daemon, which jobs here do not get: $(echo "${hit#*:*:}" | sed -E 's/^[[:space:]]+//')" + bad=1 +done < <(grep -HnE "^[^#]*${daemon_uses}" .gitea/workflows/*.y*ml 2>/dev/null || true) +[ "$bad" -eq 0 ] || exit 1 +ok "no workflow uses a Docker-daemon-bound action" diff --git a/decisions/2026-10-30-renovate-auth.md b/decisions/2026-10-30-renovate-auth.md new file mode 100644 index 0000000..b03b2c3 --- /dev/null +++ b/decisions/2026-10-30-renovate-auth.md @@ -0,0 +1,108 @@ +--- +date: 2026-10-30 +branch: fix/renovate-auth +title: Renovate's token needs five scopes, not three; the workflow documented the wrong list, and now probes it +--- + +## What changed + +**The fix is operator-side.** `RENOVATE_TOKEN` lacks `read:organization`, and it will next lack +`write:issue`. The operator must re-mint it with five scopes: + +- `read:user` +- `read:organization` +- `write:repository` +- `write:issue` +- `read:package` + +The git half fixes the two things that sent the operator round this loop twice. + +**`.gitea/workflows/renovate.yaml`**: +- **The header's scope list is corrected**, with the call each scope serves. The old list named + only `write:repository`, `read:user` and `read:package`, and the operator minted exactly that. + The preflight's "TO FIX" text is corrected the same way. +- **A new step, "Preflight - the token has every scope Renovate needs".** + - It probes read-only GETs: `/user` for read:user, `/orgs/Home-Network` for read:organization, + `/repos/{repo}` for read:repository, `/repos/{repo}/labels` for read:issue, and registry + `/v2/token` for read:package. + - It reads the token's own scope list out of Gitea's 403 from `/admin/orgs` (names only), and + requires `write:repository` and `write:issue` in it. + - It says what a failure means: 401 is the value; a Gitea 403 is a scope, and the probe prints + Gitea's message; an "Access denied" 403 is a Cilium/Envoy denial. + - The token is fed to curl on stdin (`--config -`), never in argv. + +**`ci/lint.sh`**: a new guard rejects any non-comment `uses:` in `.gitea/workflows` that needs a +Docker daemon: `docker://`, `renovatebot/github-action` and Docker's daemon-bound +`docker/*-action`s. That is the class of bug fixed in #84. + +### Root cause, with evidence + +Run 350 (task 567, 06:49 UTC) **got further than 348 and 349.** Gitea's router log, filtered to +the act-runner pod `10.0.0.38`: + + 06:49:31 GET /api/v1/user 200 OK + 06:49:31 GET /api/v1/version 200 OK + 06:49:31 GET /api/v1/repos/Home-Network/home-network-k3s-impl 200 OK + 06:49:31 GET /api/v1/orgs/Home-Network 403 Forbidden + +There were no `/v2/` requests: Renovate stopped before extraction. From the job log, verbatim: + + ERROR: Repository has unknown error (repository=Home-Network/home-network-k3s-impl) + "url": "https://gitea.home.keithjackson.dev/api/v1/orgs/Home-Network", + "message": "token does not have at least one of required scope(s), required=[read:organization], token scope=read:package,write:repository,read:user" + "x-envoy-upstream-service-time": "27", "server": "envoy" + INFO: Repository finished ... "result": "unknown-error", "exitCode": 8 + +**This is not Cilium.** The body is Gitea's JSON scope message, Gitea logged the request, and +`x-envoy-upstream-service-time` shows that Envoy forwarded it upstream. + +**The secret WAS updated.** Runs 348 and 349 got `GET /user 403` from `tokenRequiresScopes`. Run 350 +got `/user 200`, and its token scope list now includes `read:user`. + +**Why `read:organization` is needed.** Renovate 44.167.0, `lib/modules/platform/gitea/index.ts` +`initRepo`, calls `helper.isOrg` → `GET /orgs/{owner}` and rethrows on error. Gitea 1.24.3, +`routers/api/v1/api.go`, puts `/orgs/{org}` behind +`tokenRequiresScopes(AccessTokenScopeCategoryOrganization)`. + +**Why `write:issue` is needed next.** In the same `api.go`, `/repos/{o}/{r}/issues…`, `/labels` +and `/milestones` sit in a group with `tokenRequiresScopes(AccessTokenScopeCategoryIssue)` +(lines 1466–1582). Renovate's gitea-helper uses `/issues`, `/issues/{n}/labels`, `/labels` and +`/issues/{n}/comments`, for the dependency dashboard (`:dependencyDashboard` is extended) and for +PR labels. A dry run only reads them; a scheduled run writes them. + +The other causes the coordinator listed are ruled out: +- **Schedule window and dry-run.** The run never reached branch processing. +- **Repository config.** The run never reached the clone (`"cloned": undefined`). +- **The Summary step.** It is not involved; the failure is Renovate's own exit 8. + +## What I verified + +- I read the whole run 350 job log (204 lines), and the router lines above. Runs 348 and 349 are + as recorded in `decisions/2026-10-30-renovate-first-run.md`'s follow-up: `/user` got 403 from + `tokenRequiresScopes`. +- **The probe step, in the pinned image.** I extracted the step body from the YAML and ran it in + `ghcr.io/renovatebot/renovate:44.167.0` (`curl` 8.5.0 and `jq` are present) with a deliberately + **bogus** token. Every probe reported 401 with the "token VALUE" diagnosis, the scope-list read + fell back to a warning, and the step exited 1. +- **The scope-list parser.** Against run 350's exact message format it reports `write:issue` + MISSING. Against a five-scope list it reports both OK. Against `all` it reports both OK. +- **The lint guard.** It flags `renovatebot/github-action` in the pre-#84 `renovate.yaml` + (dfc432c, line 152). It reports nothing on the current workflows, and it ignores commented + lines. `bash -n ci/lint.sh` passes. +- **Account facts, read-only.** `renovate-bot` is a collaborator with `write`, and the org is + public. + +## What I did NOT verify + +- **The probe with a real token. UNVERIFIED.** I never had the token. In particular, I have not + observed that `GET /api/v1/admin/orgs` gives renovate-bot's token a 403 with a `token scope=` + list. The source says the scope check runs before `reqSiteAdmin`. If the message format + differs, the step warns and does not fail. +- **That five scopes are enough for a full non-dry run.** They cover every helper endpoint Renovate + 44.167.0 calls, mapped to Gitea 1.24.3's route categories by reading source. Branch pushes go + over git-over-HTTPS with the token; I believe that needs write:repository, but I have not + observed it. +- **Registry reads as renovate-bot.** `read:package` on a public org is expected to work. The new + probe is the first real test of it. +- **Sensitive paths.** `.gitea/**` and `ci/**` are both sensitive. CI's `approval-policy` will block + until the operator posts `/approve-override` as a PR review. -- 2.54.0