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.
--ephemeralis present;--exit_after_idle_timeand 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.