diff --git a/Dockerfile b/Dockerfile index d07cefc..be57b1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,59 @@ # syntax=docker/dockerfile:1.5 + +# --------------------------------------------------------------------------- +# 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-containerd + +ARG CONTAINERD_VERSION_TAG=v2.2.2 +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 --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 --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 + +# --------------------------------------------------------------------------- +# Stage 2: Final image +# --------------------------------------------------------------------------- FROM --platform=$TARGETPLATFORM ubuntu:25.10 ARG RUNNER_VERSION=2.333.1 @@ -15,15 +70,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 +108,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-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 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