Skip to main content
RunBook Academy

← All labs in Git, CI/CD & GitOps

Lab · intermediate · ~75 min

Lab 13: Configure a self-hosted runner through the supported interface

C · SimulationB · Nested virtualisation

Objectives

  • Use the documented `config.sh` interface with a one-hour repository registration token
  • Configure the runner as ephemeral and target it with an explicit repository label
  • Keep registration authorization out of generated runner files and source control
  • Separate runner registration from fleet idle-time and replacement policy
  • Document network, filesystem, secret, and workflow trust boundaries

Prerequisites

The product boundary

A repository registration token is short-lived authorization for the supported config.sh registration step. It is not the configured runner’s ongoing session credential. Never create or edit .runner, .credentials, .credentials_rsaparams, or similar internal files. Their format and lifecycle belong to the runner application.

GitHub documents --ephemeral for single-job runners. GitHub does not document --exit_after_idle_time; idle capacity and abandoned-runner cleanup belong to the fleet scheduler (for example ARC, a VM autoscaler, or a systemd timer around the process), not to an invented runner flag.

Task 1 — Create the non-secret environment contract

# check-shell-blocks: allow-invalid
LAB="$HOME/runner-lab"
mkdir -p "$LAB/.github/workflows"
cd "$LAB"

cat > runner.env.example <<'EOF'
GH_OWNER=runbook-academy
GH_REPO=runner-lab
RUNNER_NAME=runner-lab-01
RUNNER_LABEL=runbook-iac
RUNNER_GROUP=Default
RUNNER_WORK=_work
EOF

printf '%s\n' '.registration-token' 'runner.env' > .gitignore
cp runner.env.example runner.env

The file contains selection and naming data only. The registration token is created just in time and is never written to runner.env.

Task 2 — Author the supported bootstrap

# check-shell-blocks: allow-invalid
cd "$HOME/runner-lab"

cat > register-and-run.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

: "${GH_OWNER:?set GH_OWNER}"
: "${GH_REPO:?set GH_REPO}"
: "${RUNNER_NAME:?set RUNNER_NAME}"
: "${RUNNER_LABEL:?set RUNNER_LABEL}"
: "${RUNNER_GROUP:=Default}"
: "${RUNNER_WORK:=_work}"

test -x ./config.sh || {
  echo 'run this script from an extracted official actions/runner release' >&2
  exit 2
}
test -x ./run.sh || {
  echo './run.sh is missing from the runner release directory' >&2
  exit 2
}

REGISTRATION_TOKEN=$(gh api --method POST \
  -H 'Accept: application/vnd.github+json' \
  "/repos/${GH_OWNER}/${GH_REPO}/actions/runners/registration-token" \
  --jq .token)
test -n "$REGISTRATION_TOKEN"

# config.sh owns all runner identity and credential files. --ephemeral makes
# this registration accept one job; --disableupdate is appropriate only when
# the image/release is rebuilt by the fleet owner on a controlled cadence.
./config.sh \
  --url "https://github.com/${GH_OWNER}/${GH_REPO}" \
  --token "$REGISTRATION_TOKEN" \
  --name "$RUNNER_NAME" \
  --runnergroup "$RUNNER_GROUP" \
  --labels "$RUNNER_LABEL" \
  --work "$RUNNER_WORK" \
  --unattended \
  --ephemeral \
  --disableupdate

unset REGISTRATION_TOKEN
exec ./run.sh
EOF

chmod 0750 register-and-run.sh
bash -n register-and-run.sh

The script fails closed if it is not run from an official extracted runner release. It obtains the one-hour token immediately before configuration and passes it only to config.sh. An orchestrator must replace the process after one job and reap a runner that never receives a job within the fleet’s idle deadline.

Task 3 — Target the explicit label

# check-shell-blocks: allow-invalid
cd "$HOME/runner-lab"

cat > .github/workflows/test-workflow.yml <<'EOF'
name: verify-ephemeral-runner
on:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  verify:
    runs-on: [self-hosted, runbook-iac]
    timeout-minutes: 10
    steps:
      - name: Record runner context
        shell: bash
        run: |
          set -euo pipefail
          printf 'name=%s os=%s arch=%s\n' "$RUNNER_NAME" "$RUNNER_OS" "$RUNNER_ARCH"
          test ! -S /var/run/docker.sock
          grep -E '^(NoNewPrivs|CapEff):' /proc/self/status
EOF

The workflow opts into both self-hosted and runbook-iac. The label is a routing control, not a security boundary: repository access, workflow-change review, runner groups, environment approvals, network policy, and ephemeral replacement remain necessary.

Task 4 — Write the threat model and phase-two evidence contract

# check-shell-blocks: allow-invalid
cd "$HOME/runner-lab"

cat > runner-threat-model.md <<'EOF'
# Runner threat model

## Trust boundary
- Any workflow selected by the label executes arbitrary code as the runner user.
- The runner must not mount the host Docker socket or broad host paths.
- Egress is allowlisted to GitHub and explicitly required internal services.
- Cloud access uses job-scoped OIDC; no static cloud key is stored on disk.
- Secrets exposed to a job are assumed recoverable by that job.

## Lifecycle ownership
- The fleet scheduler creates an unregistered instance from a pinned image.
- Startup invokes the supported config.sh interface with --ephemeral.
- The process accepts one job; the scheduler destroys the instance afterwards.
- The scheduler terminates unclaimed instances after the documented idle limit.
- Runner logs are forwarded externally before instance destruction.

## Prohibited designs
- Hand-authored .runner or .credentials files.
- Docker socket, privileged mode, host PID, or host network without an approved exception.
- Organisation-wide runner access when one repository is sufficient.
- Registration-token rotation presented as configured-runner credential rotation.
EOF

cat > phase-two-evidence.md <<'EOF'
# Phase-two execution evidence

- [ ] Exact runner release and archive checksum recorded.
- [ ] Disposable repository URL and runner-group policy recorded.
- [ ] config.sh exits successfully and creates the server-side runner record.
- [ ] Workflow is accepted only by [self-hosted, runbook-iac].
- [ ] Job log proves no Docker socket and records NoNewPrivs/CapEff.
- [ ] Runner accepts exactly one job and does not accept a second.
- [ ] Idle reaper behavior is measured separately from runner CLI behavior.
- [ ] Runner record and disposable repository are removed after capture.
- [ ] Controller/runner diagnostic logs are archived with secrets redacted.
EOF

Task 5 — Perform the local phase-one checks

cd "$HOME/runner-lab"

bash -n register-and-run.sh
grep -F -- '--ephemeral' register-and-run.sh
grep -F './config.sh' register-and-run.sh
if grep -Eq '\.(runner|credentials)|exit_after_idle_time' register-and-run.sh; then
  echo 'FAIL: bootstrap uses runner internals or an unsupported idle flag' >&2
  exit 1
fi
grep -F 'runs-on: [self-hosted, runbook-iac]' .github/workflows/test-workflow.yml
grep -F 'Do not' runner-threat-model.md

Validation

  • The bootstrap parses with bash -n.
  • Registration is performed only through config.sh.
  • The one-hour token is fetched just in time and is not persisted.
  • --ephemeral is present; --exit_after_idle_time and internal credential files are absent.
  • The workflow uses an explicit label intersection and least-privilege permissions.
  • The evidence file states precisely what phase two must prove on GitHub.

Expected outcome

You have a locally validated, supportable runner bootstrap and an honest execution contract. You do not yet have evidence of a working runner. That operational claim is earned only when the phase-two checklist is completed against a disposable repository.

Deliverables

  • · runner.env.example — non-secret repository, runner-name, and label settings
  • · register-and-run.sh — supported unattended registration and runner startup
  • · test-workflow.yml — label-targeted runner verification workflow
  • · runner-threat-model.md — trust boundaries and lifecycle ownership
  • · phase-two-evidence.md — the exact live evidence still required

Verification status

Last reviewed
2026-08-25
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.