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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions