Security
What you are agreeing to
Section titled “What you are agreeing to”The runner mounts /var/run/docker.sock. That is root on the host.
Any workflow this runner executes can start a privileged container and from there read or write anything on the machine: the other stacks, their volumes, their secrets, the host filesystem. There is no sandbox, and none is intended — the design assumes you trust every workflow in the repository you point it at.
“Every workflow” is broader than it sounds. It means every workflow file, on every branch, written by anyone who can push to the repository or open a pull request against it.
Where the trade stops being acceptable
Section titled “Where the trade stops being acceptable”| situation | why it breaks | instead |
|---|---|---|
| the repository is public, or accepts pull requests from forks | a stranger’s pull request runs as root on your host | don’t. If you must: RUNNER_EPHEMERAL=true, a throwaway VM, and read GitHub’s own warning |
RUNNER_SCOPE=org | the runner accepts jobs from every repository in the organisation, including ones somebody adds later | scope it to one repository |
| the host runs production or another project’s data | one runaway job reaches it | give the runner its own machine |
GitHub’s own documentation puts it briefly and correctly: “We recommend that you only use self-hosted runners with private repositories.” The reason they give is the first row of that table — a fork’s pull request can execute code on your runner.
If you need isolation rather than trust
Section titled “If you need isolation rather than trust”This is the wrong tool, and that is not a criticism of either side. Use:
- actions-runner-controller for ephemeral, isolated runners,
- rootless Docker,
- or a per-job throwaway VM.
A half-sandbox that reads as safe is worse than an honest warning, which is why this stack does not pretend to offer one.
Smaller things worth doing anyway
Section titled “Smaller things worth doing anyway”Use a specific label. With only self-hosted, any job in scope lands on
this runner — including jobs the host cannot serve, which then fail in
confusing ways. RUNNER_LABELS=e2e and runs-on: [self-hosted, linux, x64, e2e].
Keep the registration token out of everything. It is single-use and expires in an hour, so it is nearly worthless after the deploy — but there is no reason for it to sit in a repository, a chat log or an issue. Portainer’s environment block is the right place.
Opt out of your host’s auto-updater. Both services carry
com.centurylinklabs.watchtower.enable=false. Many hosts run a Watchtower that
watches every container by default, and one that restarts the runner mid-job
loses the job and reports it as cancelled. Any service you add to this stack
needs the same label.
Bump the runner image deliberately. It is pinned, because a runner that pulls a new image mid-job loses the job — which also means you do not get the runner’s security updates automatically. GitHub force-upgrades the runner binary when it falls far enough behind, but do not rely on that as your update strategy.
Reporting something
Section titled “Reporting something”A private security
advisory,
not a public issue. The repository’s SECURITY.md lists what is in scope — and
it explicitly includes documentation that understates a risk.