Skip to content

Phase 4: bootstrap hardening — non-root runner user, --ephemeral, configurable runner version #10

@kurok

Description

@kurok

Part of plan #15. Phase 4 — Runner Bootstrap Hardening.

Problems with current bootstrap (src/aws.js)

'export RUNNER_ALLOW_RUNASROOT=1',
...
'./run.sh',
  1. Runner runs as root. Every step in every consumer workflow has unrestricted filesystem + network + device access on the EC2 instance.
  2. Runner version hardcoded in the action source. Each bump is a new action release (we've done this twice already — feat: bump actions/runner to v2.333.1 for node24 support #3 and future bumps).
  3. Not ephemeral. A runner that hits an unhandled error mid-bootstrap could linger and accept a second job it shouldn't.
  4. No pre-flight checks. If config.sh fails (expired token, network block), the EC2 instance stays up burning cost.

Target bootstrap

#!/bin/bash
set -euo pipefail

RUNNER_VERSION="${RUNNER_VERSION:-2.333.1}"
RUNNER_USER="runner"

useradd -m -s /bin/bash "$RUNNER_USER"

sudo -u "$RUNNER_USER" bash -s <<EOF
set -euo pipefail
cd /home/$RUNNER_USER
mkdir actions-runner && cd actions-runner

case \$(uname -m) in
  aarch64) ARCH="arm64" ;;
  amd64|x86_64) ARCH="x64" ;;
esac

curl -fsSLo runner.tar.gz \\
  https://github.com/actions/runner/releases/download/v\${RUNNER_VERSION}/actions-runner-linux-\${ARCH}-\${RUNNER_VERSION}.tar.gz

# Verify SHA-256 against actions/runner published checksum
expected_sha=\$(curl -fsSL https://github.com/actions/runner/releases/download/v\${RUNNER_VERSION}/actions-runner-linux-\${ARCH}-\${RUNNER_VERSION}.tar.gz.sha256 | awk '{print \$1}')
echo "\$expected_sha  runner.tar.gz" | sha256sum -c -

tar xzf runner.tar.gz

./config.sh \\
  --url ${GITHUB_URL} \\
  --token ${TOKEN} \\
  --labels ${LABEL} \\
  --ephemeral \\
  --unattended \\
  --disableupdate

./run.sh
EOF

Changes proposed

  • Dedicated runner user. Drops RUNNER_ALLOW_RUNASROOT=1.
  • --ephemeral flag. Runner processes exactly one job and exits. Eliminates the "stale runner picks up an unintended job" class of bug. GitHub auto-deregisters — the stop-runner step's config.sh remove call becomes redundant (keep it as belt-and-braces for the EC2-termination path).
  • Configurable RUNNER_VERSION. New optional action input runner-version defaulting to the pinned-in-source value (2.333.1). Consumers can override for canary testing without cutting a new action release.
  • Checksum verification of the runner tarball — same pattern landed in terraform-provider-namecheap#160 for Go/Terraform downloads.
  • set -euo pipefail throughout so a silent useradd or tar failure kills the bootstrap instead of proceeding to ./run.sh against a broken install.
  • --disableupdate keeps the runner binary stable during the short-lived ephemeral session.

Compatibility impact on consumers (specifically terraform-provider-namecheap)

  • make testacc is plain go test. No root required.
  • Setup steps in the provider's acceptance_test job write to the workspace (.go-instance/, .terraform-bin/, go-env.sh). All workspace-local. No root required.
  • actions/checkout@v6 writes to the workspace. No root required.
  • Any Docker / containerd / iptables / sysctl changes in a consumer workflow would break. None in the provider repo today.

This is the phase with the highest compatibility risk. Recommendation: land it on a branch and dogfood-test on terraform-provider-namecheap by rotating the SHA pin on a throwaway branch before merging to feat/al2023-support.

Acceptance criteria

  • Bootstrap user-data creates a dedicated runner user; runner never runs as root.
  • --ephemeral passed to config.sh; stale-runner behavior cannot occur.
  • New runner-version input (optional, defaults to current pin) lets consumers override.
  • Runner tarball checksum verified before extraction.
  • README.md notes the non-root constraint; any consumer script that relied on root is called out.
  • Dogfood test: rotate SHA pin in terraform-provider-namecheap on a throwaway branch; full acctest cycle (start → acceptance → stop) passes end-to-end.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions