This repository had no .dejima.md. The first rule in it is the one the v3 migration paid for: every step that asked an operator to type something was a step that went wrong on a real host, and every fix was to stop asking rather than to ask more clearly. --network and --dir became --pool; the count, the label and the group name became things read from the project already in the directory. Three more, all from the same week and all invisible from reading the code: run a migration before shipping the note describing it, since the suite passed throughout while every step of that note failed; the scoping is the registration and never the label, because a branch writes its own runs-on; and a set is addresses with no memory of what put them there, which is why revocation has to flush and why scoping the resolver alone would have bought nothing.
4.1 KiB
Working on dejima-ci-hardener
Notes for an agent working on this repo. Not a description of what the tool
does: README.md and docs/threat-model.md are that.
Remove the value an operator would type, rather than documenting it better
When a step needs a path, a name or a number typed in, the fix is almost never a clearer instruction. It is to take the value from wherever it is already recorded, and to have the command name the thing rather than describe it.
This repository is where the lesson was learned. The v3 migration asked for a network and a project directory that had to agree with each other and with a third file, once per pool:
sudo dejima-ci-runners --role confined --network dejima-ci-1 --dir ~/runners/one
Run a second time it gets copied, the path gets changed because the path is the part that obviously differs, and the network does not. Both pools then run on the first one's network, which is one pool holding another's allowlist and looks exactly like it worked. The instruction was accurate. The shape was wrong.
It became --pool <scope>, taking both values from the file that already knew
them. The same pass removed three more: the runner count, the label and the
group name now come from the compose project that is already in the directory,
because an operator re-running the generator was reading their own count out of
a compose file in another terminal to answer a prompt.
Where a value genuinely has to be given, make the wrong one refuse rather than
proceed. --network still exists, and pointing a project at a different pool's
network now stops with both names in the message. A check that catches the
mistake is worth more than a paragraph describing it: the paragraph is read
once, the check runs every time.
Run a migration before shipping the note that describes it
Every step of the v3 note failed on a real host, and none of it was visible from reading the code. The suite passed throughout.
- The installer's runner prompt defaulted to yes, so pressing enter through a re-run walked into replacing existing pools.
- The generator offered built-in defaults rather than the pool's own values.
- A documented command omitted a required
--role, and answered with usage. - The group name defaulted from the label, which is shared between pools, so accepting it would have orphaned the running containers and volumes.
- The runners came up reading a stale
config.yaml, because it is bind-mounted as a file andwrite_if_changedreplaces the inode by rename, and compose will not recreate a container whose service definition did not change.
Before shipping a note, run its commands against a fixture in the state the note targets, which for an upgrade means the already-configured state rather than a fresh one. Where a step cannot be run here, say so in the pull request rather than presenting the note as tested.
The scoping is the registration, never the label
A pool's firewall set holds the union of every declaration it could be made to run, which is decided by what its runners are registered to: one repository, or an owner.
Labels cannot carry this. A branch writes its own runs-on, so a label is a
request the job makes rather than a property the operator sets. Anything that
starts to route by label for a security reason is heading somewhere that does
not hold.
The corollary is that an owner-scoped pool holding its whole owner's names is correct rather than a compromise. That union is what the pool can already be asked to run.
A set is addresses, with no memory of what put them there
ipset holds resolved addresses. It cannot be asked which name added one, which
decides more than it looks like it does:
- removing a name from a declaration revokes nothing on its own, so a render that drops a name flushes the sets and lets the next lookup refill them;
- scoping the resolver alone would have bought nothing, because a job that cannot resolve a name can still reach the address another pool's lookup put in a shared set.
Anything reasoning about "which project does this address belong to" is reasoning about something the kernel is not storing.