From a47de56e6d03de19c9f637543b9fe2cbe6b6e1a1 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Mon, 10 Aug 2026 11:58:05 -0700 Subject: [PATCH 1/2] fix: validate persisted runner registration before reusing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream myoung34 entrypoint reuses CONFIGURED_ACTIONS_RUNNER_FILES_DIR credentials unconditionally: if .runner exists it skips registration with no validation and no fallback. When the runner has been removed server-side (GitHub prunes runners offline >14 days, manual deletion, etc), the stored credentials are dead and the container crash-loops forever — even though a valid ACCESS_TOKEN is sitting in the environment the whole time. Add a preflight entrypoint that asks GitHub whether the persisted agentId still exists. Only a definitive 404 wipes the persisted registration, letting the upstream entrypoint fall through to fresh ACCESS_TOKEN registration and store the new credentials back. Transient errors, missing env, or unparseable state leave everything untouched, so behavior is never worse than upstream. Declaring ENTRYPOINT resets the CMD inherited from the base image, so the base CMD is restated verbatim. Co-Authored-By: Claude Fable 5 --- Dockerfile | 13 +++++- preflight-entrypoint.sh | 95 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 preflight-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index c448808..da406ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,17 @@ COPY cleanup.sh /usr/local/bin/cleanup.sh RUN chmod +x /usr/local/bin/cleanup.sh ENV ACTIONS_RUNNER_HOOK_JOB_COMPLETED=/usr/local/bin/cleanup.sh -# NB: there is no CMD so it will work the same as the base image. See the +# Preflight entrypoint: validates the persisted runner registration against +# GitHub before handing off to the upstream entrypoint. Upstream reuses +# CONFIGURED_ACTIONS_RUNNER_FILES_DIR credentials unconditionally, which +# crash-loops the container when the runner was removed server-side; the +# preflight wipes provably-dead registrations so a fresh ACCESS_TOKEN +# registration happens instead. See preflight-entrypoint.sh for details. +COPY preflight-entrypoint.sh /usr/local/bin/preflight-entrypoint.sh +RUN chmod +x /usr/local/bin/preflight-entrypoint.sh +ENTRYPOINT ["/usr/local/bin/preflight-entrypoint.sh"] +# Declaring ENTRYPOINT resets the CMD inherited from the base image, so the +# base image's CMD is restated here. See # https://github.com/myoung34/docker-github-actions-runner#environment-variables # for how to use the image +CMD ["./bin/Runner.Listener", "run", "--startuptype", "service"] diff --git a/preflight-entrypoint.sh b/preflight-entrypoint.sh new file mode 100644 index 0000000..f569286 --- /dev/null +++ b/preflight-entrypoint.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# Preflight wrapper around the upstream myoung34 entrypoint. +# +# When CONFIGURED_ACTIONS_RUNNER_FILES_DIR is set, the upstream entrypoint +# copies the persisted config into /actions-runner and skips registration +# whenever a .runner file exists — with no validation of the stored +# credentials and no fallback to ACCESS_TOKEN. If the runner was meanwhile +# removed server-side (pruned by GitHub after being offline too long, deleted +# manually, etc), the stored credentials are dead and the container +# crash-loops forever without ever re-registering, even though a perfectly +# good ACCESS_TOKEN is sitting in the environment. +# +# This preflight asks GitHub whether the persisted runner still exists. Only +# a definitive 404 wipes the persisted registration, which makes the upstream +# entrypoint fall through to a fresh ACCESS_TOKEN registration (and store the +# new credentials back into the persist dir). Transient errors, missing +# variables, or unparseable state leave everything untouched, so behavior is +# never worse than upstream's. +# +# Intentionally not using set -e: preflight must never block runner startup. + +log() { echo "[preflight] $*"; } + +preflight() { + local dir="${CONFIGURED_ACTIONS_RUNNER_FILES_DIR:-}" + [ -n "$dir" ] || return 0 + [ -f "$dir/.runner" ] || return 0 + if [ -z "${ACCESS_TOKEN:-}" ]; then + log "ACCESS_TOKEN not set; cannot validate persisted registration, leaving as-is" + return 0 + fi + + # .runner is JSON written by the runner itself; it may carry a UTF-8 BOM. + local agent_id + agent_id=$(sed '1s/^\xEF\xBB\xBF//' "$dir/.runner" | jq -r '.agentId // empty' 2>/dev/null) + if [ -z "$agent_id" ]; then + log "could not read agentId from $dir/.runner; leaving as-is" + return 0 + fi + + local github_host="${GITHUB_HOST:-github.com}" + local api_base + if [ "$github_host" = "github.com" ]; then + api_base="https://api.github.com" + else + api_base="https://${github_host}/api/v3" + fi + + local api_path + case "${RUNNER_SCOPE:-repo}" in + org) + if [ -z "${ORG_NAME:-}" ]; then + log "RUNNER_SCOPE=org but ORG_NAME unset; leaving as-is" + return 0 + fi + api_path="orgs/${ORG_NAME}/actions/runners/${agent_id}" + ;; + ent*) + log "enterprise scope validation not implemented; leaving as-is" + return 0 + ;; + *) + # repo scope: REPO_URL looks like https://// + local repo_path="${REPO_URL#*://*/}" + if [ -z "$repo_path" ] || [ "$repo_path" = "${REPO_URL:-}" ]; then + log "cannot parse REPO_URL; leaving as-is" + return 0 + fi + api_path="repos/${repo_path}/actions/runners/${agent_id}" + ;; + esac + + local status + status=$(curl -sS -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${ACCESS_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + "${api_base}/${api_path}" 2>/dev/null) + + case "$status" in + 200) + log "persisted runner agentId=${agent_id} still registered; reusing stored credentials" + ;; + 404) + log "persisted runner agentId=${agent_id} no longer exists on ${github_host};" \ + "wiping persisted registration to force fresh ACCESS_TOKEN registration" + rm -f "$dir/.runner" "$dir/.credentials" "$dir/.credentials_rsaparams" + ;; + *) + log "could not validate persisted runner (HTTP ${status:-none}); leaving as-is" + ;; + esac +} + +preflight +exec /entrypoint.sh "$@" From 577c054bd37fb7ea673b03c9b7e95416769122b6 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Mon, 10 Aug 2026 12:11:05 -0700 Subject: [PATCH 2/2] fix: bound the validation curl so network stalls can't block startup Co-Authored-By: Claude Fable 5 --- preflight-entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/preflight-entrypoint.sh b/preflight-entrypoint.sh index f569286..40fecb7 100644 --- a/preflight-entrypoint.sh +++ b/preflight-entrypoint.sh @@ -70,8 +70,11 @@ preflight() { ;; esac + # Bounded timeouts so a DNS/TLS/network stall can't block runner startup; + # a timeout falls into the default case below and leaves the config as-is. local status status=$(curl -sS -o /dev/null -w '%{http_code}' \ + --connect-timeout 5 --max-time 15 \ -H "Authorization: token ${ACCESS_TOKEN}" \ -H "Accept: application/vnd.github+json" \ "${api_base}/${api_path}" 2>/dev/null)