Files
branderson e4e0816f6f
tests / runner-env (confined) (pull_request_target) Successful in 41s
tests / tests (pull_request_target) Successful in 1m31s
tests / compose-schema (pull_request_target) Successful in 49s
tests / runner-env (privileged) (pull_request_target) Successful in 38s
Give each runner pool the allowlist it could actually run
Every job on the confined tier resolved against one list and was filtered by one
ipset, so a name any trusted repository declared was reachable from every
repository's jobs. One project's declaration widened all of them.

THE SET IS WHAT HAD TO BE SCOPED, not the resolver. It holds addresses with no
memory of which name put them there, and every bridge matched the same one, so
the union was at the address layer: a job that could not resolve another
project's name could still connect to the address that name resolved to, and
declarations are public.

A SET PER RUNNER POOL, holding the union of every declaration that pool could be
made to run. That is the only union that is safe. A pool takes jobs from the
repositories it is registered for, so a hostile branch in one of them can
already put a job on it, and its names being in that pool's set gives away
nothing the branch did not already have. What the set leaves out is every
repository the pool cannot run at all.

An earlier draft scoped per project and leaned on labels to route jobs, which is
not a boundary: a branch writes its own runs-on, so a label is a request the job
makes rather than a property the operator sets. What a runner may take is
decided by its registration, which a branch cannot change. So the set is built
from the registration - one repository, or an owner - and labels are left alone.
Pools may share them.

Registering a pool to one repository is how its union gets small, and is the
recommendation rather than the mechanism. An owner-scoped pool is not a
misconfiguration: it is a pool that genuinely can run any of those
repositories' jobs, holding exactly what that implies. So the dejima
organisation's repositories can keep one org-scoped pool and one union, a
user-scoped pool keeps a small one, and a repository needing an unusual domain
can have a pool of its own without that domain reaching anybody else's.

No shared fallback. A declaration no pool can run refuses the render, naming
what is missing: that is almost always a repository trusted to widen the
allowlist and never given a runner, which is a job queueing rather than a
security decision, and rendering its names into nothing would hide it. Refusing
installs nothing, so the allowlist already in force stays in force.

dnsmasq places a name's addresses in several sets at once, so one resolver fills
each pool's set and no others. The firewall matches each bridge against its
pool's set and a base set every pool shares, and takes rules for a pool's bridge
whether or not the config's bridge list knew about it.

Three things caught while doing it. The name count was lines minus the header,
which doubled the moment a name emitted two lines. A guest-layer note must
require a snapshot, or this tier's scheduled rollback quietly undoes the
install. And the revocation flush from the previous change flushed the set named
by the config key, which is a prefix now and holds nothing - it would have
emptied an unused set and reported a revocation that had not happened.

Capturing fetch_repo into a variable also dropped its exit status, so a forge
that rejected the token became a repository that declared nothing. It was the
last command of a pipeline before, where pipefail carried the failure out.
2026-09-01 13:34:15 -07:00
..

Documentation

The README is the map: what this is, which tier you want, and how to operate one once it exists. The build itself is two pages, and this is where the second one lives.

building-a-runner.md The build, inside the VM: the Debian install, addressing, one bootstrap command, install.sh's settings, and the runners. The same on any hypervisor and for both tiers. Start at your hypervisor's page and it sends you here
reference.md The two settings named docker.sock, CONFIG_FILE and why an edit can appear to do nothing, the /data volume, compose networks against job networks, whether several runners should share a job network, why the privileged runner is separate, why public egress is not restricted on the node, why the resolver is set per-runner, the cache server
threat-model.md The adversary, where each tier's boundary sits, blast radius by layer, and what was rejected
upgrading.md Moving a runner that already exists to a newer version, why the two halves are counted separately, and the seven steps a guest-side change takes when a rollback is going to rebuild the machine. Start at Updating
../upgrades/ One note per version: what an operator does in the console and in the guest to reach it
../proxmox/README.md The node-side half on Proxmox: the VM, the datacenter firewall, the security group and the order its rules go in, and all three snapshot moments including the scheduled rollback
../proxmox/firewall.fw.template The same node firewall as one annotated file, easier to review in one piece than as a table. Reference, not something to install
../examples/ The annotated templates dejima-ci-runners renders into ~/runners. Every setting carries the reasoning for its value, which the generated copies do not - read them here rather than reading what lands on the host

One document lives outside this repository: dejima's docs/ci.md, which covers why a CI run needs any of this over a sandbox, and why no approval gate is offered. You do not need it to follow the README.