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.
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.