From d415126536bc96815bc1a7de47df834842062a4f Mon Sep 17 00:00:00 2001 From: prasadvamer Date: Mon, 30 Mar 2026 20:09:58 +0900 Subject: [PATCH 1/3] fix: ensure runner deregisters on shutdown regardless of token source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the cleanup function could only deregister the runner when RUNNER_TOKEN_FILE was used. When passing RUNNER_TOKEN as a plain env var (the most common usage), the token was unset after registration and cleanup could never read it back — leaving ghost runners in GitHub. Now the token is saved to /home/runner/.runner-token-cleanup (mode 600) before being unset, so cleanup always has access. The file is deleted after deregistration and lives inside the container filesystem (never bind-mounted), avoiding conflicts with shared volumes. --- entrypoint.sh | 14 ++++---- tests/test_token_cleanup.sh | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) create mode 100755 tests/test_token_cleanup.sh diff --git a/entrypoint.sh b/entrypoint.sh index 0f0bb58..ab5bc32 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -140,19 +140,21 @@ cd /actions-runner --work "${WORK_DIR}" \ --replace -# Clear token from environment after registration -RUNNER_TOKEN_FILE_PATH="${RUNNER_TOKEN_FILE:-}" +# Save token for deregistration, then clear from environment +RUNNER_TOKEN_CLEANUP_FILE="/home/runner/.runner-token-cleanup" +printf '%s' "${RUNNER_TOKEN}" > "$RUNNER_TOKEN_CLEANUP_FILE" +chmod 600 "$RUNNER_TOKEN_CLEANUP_FILE" unset RUNNER_TOKEN cleanup() { echo "Unregistering runner..." - if [ -n "${RUNNER_TOKEN_FILE_PATH}" ] && [ -f "${RUNNER_TOKEN_FILE_PATH}" ]; then + if [ -f "$RUNNER_TOKEN_CLEANUP_FILE" ]; then local token - token="$(cat "$RUNNER_TOKEN_FILE_PATH")" + token="$(cat "$RUNNER_TOKEN_CLEANUP_FILE")" ./config.sh remove --unattended --token "$token" || true + rm -f "$RUNNER_TOKEN_CLEANUP_FILE" else - echo "WARNING: Cannot unregister -- RUNNER_TOKEN already cleared from environment." - echo "Use RUNNER_TOKEN_FILE for automatic deregistration on shutdown." + echo "WARNING: Cannot unregister -- cleanup token file not found." fi } diff --git a/tests/test_token_cleanup.sh b/tests/test_token_cleanup.sh new file mode 100755 index 0000000..19b409d --- /dev/null +++ b/tests/test_token_cleanup.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/helpers.sh" + +test_header "Token Cleanup File for Deregistration" + +CLEANUP_FILE="/home/runner/.runner-token-cleanup" + +# Token file is created with correct permissions and content +output=$(run_in_image " + TOKEN='test-secret-token-abc123' + printf '%s' \"\$TOKEN\" > $CLEANUP_FILE + chmod 600 $CLEANUP_FILE + + # Verify file exists + [ -f $CLEANUP_FILE ] && echo 'EXISTS' || echo 'MISSING' +") +assert_eq "$output" "EXISTS" "Cleanup token file is created at $CLEANUP_FILE" + +# Token file has mode 600 (owner read/write only) +output=$(run_in_image " + printf '%s' 'test-token' > $CLEANUP_FILE + chmod 600 $CLEANUP_FILE + stat -c '%a' $CLEANUP_FILE +") +assert_eq "$output" "600" "Cleanup token file has mode 600" + +# Token content is preserved correctly +output=$(run_in_image " + printf '%s' 'my-secret-runner-token' > $CLEANUP_FILE + chmod 600 $CLEANUP_FILE + cat $CLEANUP_FILE +") +assert_eq "$output" "my-secret-runner-token" "Token content is read back correctly" + +# Token file is owned by runner user +output=$(run_in_image " + gosu runner bash -c \"printf '%s' 'test-token' > $CLEANUP_FILE && chmod 600 $CLEANUP_FILE\" + stat -c '%U' $CLEANUP_FILE +") +assert_eq "$output" "runner" "Cleanup token file is owned by runner user" + +# Token file is removed after cleanup reads it +output=$(run_in_image " + printf '%s' 'test-token' > $CLEANUP_FILE + chmod 600 $CLEANUP_FILE + # Simulate what cleanup() does after reading + cat $CLEANUP_FILE >/dev/null + rm -f $CLEANUP_FILE + [ -f $CLEANUP_FILE ] && echo 'STILL_EXISTS' || echo 'REMOVED' +") +assert_eq "$output" "REMOVED" "Cleanup token file is deleted after use" + +# Token file is not visible to other users +output=$(run_in_image " + printf '%s' 'test-token' > $CLEANUP_FILE + chmod 600 $CLEANUP_FILE + # Try reading as nobody (should fail) + su -s /bin/bash nobody -c 'cat $CLEANUP_FILE 2>&1' || echo 'PERMISSION_DENIED' +") +assert_contains "$output" "PERMISSION_DENIED" "Token file is not readable by other users" + +# Cleanup path in entrypoint.sh matches expected location +output=$(run_in_image "grep -c 'RUNNER_TOKEN_CLEANUP_FILE=\"$CLEANUP_FILE\"' /entrypoint.sh || echo 0") +assert_eq "$output" "1" "Entrypoint uses $CLEANUP_FILE as cleanup path" + +test_summary From fd45e481b3027baa09c43166182e9b9945ca46c8 Mon Sep 17 00:00:00 2001 From: prasadvamer Date: Tue, 31 Mar 2026 00:14:14 +0900 Subject: [PATCH 2/3] security: build containerd, dockerd, and gosu from source to fix CVEs Upstream containerd 2.2.2 and Docker 29.3.1 ship with google.golang.org/grpc < 1.79.3 and gosu 1.19 ships with Go < 1.24.13, both flagged by Docker Hub security scanning with no upstream fix available. Introduce a multi-stage build that compiles all affected binaries from source using Go 1.24.13 and grpc v1.79.3, then overlays them onto the apt-installed packages in the final image: - containerd, containerd-shim-runc-v2, ctr (grpc >= 1.79.3) - dockerd (grpc >= 1.79.3) - gosu (Go >= 1.24.13) Affected paths: /usr/bin/containerd, /usr/bin/containerd-shim-runc-v2, /usr/bin/ctr, /usr/bin/dockerd, /usr/sbin/gosu --- Dockerfile | 67 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index d07cefc..c776be6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,46 @@ # syntax=docker/dockerfile:1.5 + +# --------------------------------------------------------------------------- +# Stage 1: Builder – compile containerd, dockerd, and gosu from source +# with google.golang.org/grpc >= 1.79.3 (fixes CVE in grpc < 1.79.3) +# and Go >= 1.24.13 (fixes CVE in Go stdlib < 1.24.13) +# --------------------------------------------------------------------------- +FROM --platform=$TARGETPLATFORM golang:1.24.13 AS builder + +ARG CONTAINERD_VERSION_TAG=v2.2.2 +ARG MOBY_VERSION_TAG=v29.3.1 +ARG GOSU_VERSION=1.19 +ARG GRPC_FIX_VERSION=1.79.3 + +# Build gosu +RUN set -eux; \ + CGO_ENABLED=0 go install -ldflags '-s -w' \ + "github.com/tianon/gosu@${GOSU_VERSION}"; \ + cp /go/bin/gosu /usr/local/bin/gosu + +# Build containerd binaries (containerd, ctr, containerd-shim-runc-v2) +RUN set -eux; \ + git clone --depth 1 --branch "${CONTAINERD_VERSION_TAG}" \ + https://github.com/containerd/containerd.git /build/containerd; \ + cd /build/containerd; \ + go get "google.golang.org/grpc@v${GRPC_FIX_VERSION}"; \ + go mod tidy; \ + make STATIC=1 binaries; \ + cp bin/containerd bin/ctr bin/containerd-shim-runc-v2 /usr/local/bin/ + +# Build dockerd (moby engine) +RUN set -eux; \ + git clone --depth 1 --branch "${MOBY_VERSION_TAG}" \ + https://github.com/moby/moby.git /build/moby; \ + cd /build/moby; \ + go get "google.golang.org/grpc@v${GRPC_FIX_VERSION}"; \ + go mod tidy; \ + CGO_ENABLED=0 go build -o /usr/local/bin/dockerd \ + -ldflags '-s -w' ./cmd/dockerd + +# --------------------------------------------------------------------------- +# Stage 2: Final image +# --------------------------------------------------------------------------- FROM --platform=$TARGETPLATFORM ubuntu:25.10 ARG RUNNER_VERSION=2.333.1 @@ -15,15 +57,10 @@ ARG COMPOSE_VERSION=2.40.3 ARG COMPOSE_SHA256_AMD64=dba9d98e1ba5bfe11d88c99b9bd32fc4a0624a30fafe68eea34d61a3e42fd372 ARG COMPOSE_SHA256_ARM64=d26373b19e89160546d15407516cc59f453030d9bc5b43ba7faf16f7b4980137 -# Docker Engine + containerd pinned versions (fixes CVE in Go dependency <1.79.3) +# Docker Engine + containerd apt versions (binaries overridden by source-built in builder stage) ARG DOCKER_VERSION=5:29.3.1-1~ubuntu.25.10~questing ARG CONTAINERD_VERSION=2.2.2-1~ubuntu.25.10~questing -# Gosu checksums from: https://github.com/tianon/gosu/releases/tag/1.19 -ARG GOSU_VERSION=1.19 -ARG GOSU_SHA256_AMD64=52c8749d0142edd234e9d6bd5237dff2d81e71f43537e2f4f66f75dd4b243dd0 -ARG GOSU_SHA256_ARM64=3a8ef022d82c0bc4a98bcb144e77da714c25fcfa64dccc57f6aba7ae47ff1a44 - # Node.js LTS pinned version ARG NODE_VERSION=22 @@ -58,18 +95,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ docker-ce-cli="${DOCKER_VERSION}" \ containerd.io="${CONTAINERD_VERSION}" -# Install gosu from official release with checksum verification (apt version ships vulnerable Go stdlib) -RUN set -eux; \ - case "${TARGETARCH}" in \ - arm64) CHECKSUM="${GOSU_SHA256_ARM64}" ;; \ - amd64) CHECKSUM="${GOSU_SHA256_AMD64}" ;; \ - *) echo "Unsupported: ${TARGETARCH}" >&2; exit 1 ;; \ - esac; \ - curl -fL "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-${TARGETARCH}" \ - -o /usr/sbin/gosu; \ - echo "${CHECKSUM} /usr/sbin/gosu" | sha256sum -c -; \ - chmod +x /usr/sbin/gosu; \ - gosu --version +# Override apt-installed binaries with source-built versions (fixes CVE in grpc < 1.79.3 and Go < 1.24.13) +COPY --from=builder /usr/local/bin/gosu /usr/sbin/gosu +COPY --from=builder /usr/local/bin/containerd /usr/bin/containerd +COPY --from=builder /usr/local/bin/containerd-shim-runc-v2 /usr/bin/containerd-shim-runc-v2 +COPY --from=builder /usr/local/bin/ctr /usr/bin/ctr +COPY --from=builder /usr/local/bin/dockerd /usr/bin/dockerd # Create runner user WITHOUT blanket sudo access RUN useradd -m runner From 25c56a0e1641e79270cddd7138a39ef8ff3d8990 Mon Sep 17 00:00:00 2001 From: prasadvamer Date: Tue, 31 Mar 2026 00:41:57 +0900 Subject: [PATCH 3/3] security: split builder stages for containerd and dockerd with Go version optimization - Split builder stage into builder-containerd (Go 1.24.13) and builder-moby (Go 1.25.8) to match moby v29.3.1 requirements - Update moby version tag from v29.3.1 to docker-v29.3.1 for correct git reference - Add go mod vendor step to both builder stages for reproducible builds - Implement build cache mounts for /go/pkg/mod and /root/.cache/go-build to improve build performance - Update COPY directives to reference correct builder stages (builder-containerd and builder-moby) - Ensures grpc >= 1.79.3 and appropriate Go versions are used in each build stage to fix CVEs --- Dockerfile | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index c776be6..be57b1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,12 @@ # syntax=docker/dockerfile:1.5 # --------------------------------------------------------------------------- -# Stage 1: Builder – compile containerd, dockerd, and gosu from source -# with google.golang.org/grpc >= 1.79.3 (fixes CVE in grpc < 1.79.3) -# and Go >= 1.24.13 (fixes CVE in Go stdlib < 1.24.13) +# Stage 1a: Build gosu and containerd from source (Go 1.24.x) +# with google.golang.org/grpc >= 1.79.3 and Go >= 1.24.13 # --------------------------------------------------------------------------- -FROM --platform=$TARGETPLATFORM golang:1.24.13 AS builder +FROM --platform=$TARGETPLATFORM golang:1.24.13 AS builder-containerd ARG CONTAINERD_VERSION_TAG=v2.2.2 -ARG MOBY_VERSION_TAG=v29.3.1 ARG GOSU_VERSION=1.19 ARG GRPC_FIX_VERSION=1.79.3 @@ -19,22 +17,37 @@ RUN set -eux; \ cp /go/bin/gosu /usr/local/bin/gosu # Build containerd binaries (containerd, ctr, containerd-shim-runc-v2) -RUN set -eux; \ +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + set -eux; \ git clone --depth 1 --branch "${CONTAINERD_VERSION_TAG}" \ https://github.com/containerd/containerd.git /build/containerd; \ cd /build/containerd; \ go get "google.golang.org/grpc@v${GRPC_FIX_VERSION}"; \ go mod tidy; \ + go mod vendor; \ make STATIC=1 binaries; \ cp bin/containerd bin/ctr bin/containerd-shim-runc-v2 /usr/local/bin/ +# --------------------------------------------------------------------------- +# Stage 1b: Build dockerd from source (Go 1.25.x – moby v29.3.1 requires >= 1.25.5) +# with google.golang.org/grpc >= 1.79.3 +# --------------------------------------------------------------------------- +FROM --platform=$TARGETPLATFORM golang:1.25.8 AS builder-moby + +ARG MOBY_VERSION_TAG=docker-v29.3.1 +ARG GRPC_FIX_VERSION=1.79.3 + # Build dockerd (moby engine) -RUN set -eux; \ +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + set -eux; \ git clone --depth 1 --branch "${MOBY_VERSION_TAG}" \ https://github.com/moby/moby.git /build/moby; \ cd /build/moby; \ go get "google.golang.org/grpc@v${GRPC_FIX_VERSION}"; \ go mod tidy; \ + go mod vendor; \ CGO_ENABLED=0 go build -o /usr/local/bin/dockerd \ -ldflags '-s -w' ./cmd/dockerd @@ -96,11 +109,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ containerd.io="${CONTAINERD_VERSION}" # Override apt-installed binaries with source-built versions (fixes CVE in grpc < 1.79.3 and Go < 1.24.13) -COPY --from=builder /usr/local/bin/gosu /usr/sbin/gosu -COPY --from=builder /usr/local/bin/containerd /usr/bin/containerd -COPY --from=builder /usr/local/bin/containerd-shim-runc-v2 /usr/bin/containerd-shim-runc-v2 -COPY --from=builder /usr/local/bin/ctr /usr/bin/ctr -COPY --from=builder /usr/local/bin/dockerd /usr/bin/dockerd +COPY --from=builder-containerd /usr/local/bin/gosu /usr/sbin/gosu +COPY --from=builder-containerd /usr/local/bin/containerd /usr/bin/containerd +COPY --from=builder-containerd /usr/local/bin/containerd-shim-runc-v2 /usr/bin/containerd-shim-runc-v2 +COPY --from=builder-containerd /usr/local/bin/ctr /usr/bin/ctr +COPY --from=builder-moby /usr/local/bin/dockerd /usr/bin/dockerd # Create runner user WITHOUT blanket sudo access RUN useradd -m runner