Merge pull request 'Give the privileged tier a working v1 to v2 path, and link the upgrade docs' (#22) from upgrade-path-privileged into main

Reviewed-on: #22
Reviewed-by: branderson <brad@branderson.io>
This commit was merged in pull request #22.
This commit is contained in:
2026-08-27 21:00:16 -07:00
7 changed files with 116 additions and 9 deletions
+32
View File
@@ -43,6 +43,9 @@ For *why* any of this is necessary - what a CI run adds over a sandbox, and
why no approval gate is offered - see [dejima's `docs/ci.md`](https://gitbox.branderson.io/dejima/dejima/src/branch/main/docs/ci.md). You do
not need it to follow this.
**Already have a runner?** [Updating an existing runner](#updating-an-existing-runner)
is the path forward: check its version, then walk the notes in order.
**Early development. Not ready to be depended on.** Nothing here has been
through outside review, an external audit, or adversarial testing by anyone but
the person who wrote it. Every security property this describes is a claim
@@ -55,6 +58,35 @@ where to report a vulnerability rather than in a public issue.
[![Developed in dejima](assets/dejima-badge.svg)](https://gitbox.branderson.io/dejima/dejima)
## Updating an existing runner
**Check what it is at**, from a checkout rather than the installed copy:
```sh
git -C ~/dejima-ci-hardener pull
cd ~/dejima-ci-hardener
sudo ./bin/dejima-ci-upgrade set-tier confined # or privileged, once per host
sudo ./bin/dejima-ci-upgrade status
```
That names every version this runner is missing. **Do them in order** — each
note assumes the one before it.
| | |
| --- | --- |
| [upgrades/](upgrades/) | One note per version: the console work, the guest work, and what it costs |
| [docs/upgrading.md](docs/upgrading.md) | The process, including the seven steps a guest-side change takes when a rollback is going to rebuild the machine |
**From a checkout, not the installed copy**, for two reasons: an installed copy
knows only the versions that existed when it was installed, and on a privileged
host there may be no installed copy at all — `install.sh` does not run there.
**Recording a version is itself a guest-side change**, so on a runner it is
thrown away at the next rollback unless it is baked into the snapshot. The
console work is not: hypervisor rules are in force either way. `docs/upgrading.md`
covers the choice; `status` re-runs each upgrade's check before calling its work
outstanding, so a rebuilt runner is not told to redo something already done.
## Do not install this next to dejima
This runs on a machine whose job is to execute untrusted code. dejima runs on
+11
View File
@@ -535,6 +535,17 @@ elif [ "$ROLE" = privileged ]; then
say " installed /usr/local/sbin/dejima-ci-runners"
say " installed /usr/local/sbin/dejima-ci-upgrade"
say " recorded /etc/dejima-ci/state (tier=privileged)"
# This runs under sudo, so its PATH is the one the operator's later sudo
# commands get. Without a secure_path in sudoers, sudo inherits a non-root
# PATH, and Debian leaves every sbin off that - so a correct install ends
# with every documented command reporting "command not found".
if [ "$(command -v dejima-ci-upgrade 2>/dev/null)" != /usr/local/sbin/dejima-ci-upgrade ]; then
say
say " NOTE: /usr/local/sbin is not on the PATH sudo gives you here, so"
say " 'sudo dejima-ci-upgrade ...' will not be found. Use the full path,"
say " or add a secure_path to sudoers that includes it."
fi
say
/usr/local/sbin/dejima-ci-runners --role privileged || \
say " runner setup did not finish - re-run: sudo dejima-ci-runners --role privileged"
+2 -1
View File
@@ -9,7 +9,8 @@ where the second one lives.
| [building-a-runner.md](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](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](threat-model.md) | The adversary, where each tier's boundary sits, blast radius by layer, and what was rejected |
| [upgrading.md](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 |
| [upgrading.md](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](../README.md#updating-an-existing-runner) |
| [../upgrades/](../upgrades/) | One note per version: what an operator does in the console and in the guest to reach it |
| [../proxmox/README.md](../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](../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/](../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 |
+20
View File
@@ -858,6 +858,26 @@ say "are in the host's namespace, no bridge is in front of them, and the rules"
say "installed here do not filter them. That is the privileged tier, and it"
say "belongs on its own machine - see the README."
say
# Reachability, not just existence.
#
# Everything installs to /usr/local/sbin and every instruction in this
# repository reads `sudo dejima-ci-...`. That works only where sudoers sets a
# secure_path including sbin; without one, sudo inherits the caller's PATH, and
# Debian leaves every sbin directory off a non-root PATH. The result is a
# correct install whose documented commands all report "command not found",
# which reads as a broken install rather than as a PATH.
#
# This runs under sudo, so its own PATH is the one those later commands will
# get. Asking `command -v` here is therefore the real question rather than a
# proxy for it.
if [ "$(command -v dejima-ci-allowlist 2>/dev/null)" != "$BIN_DIR/dejima-ci-allowlist" ]; then
say
say " NOTE: $BIN_DIR is not on the PATH sudo gives you here, so"
say " 'sudo dejima-ci-allowlist ...' will not be found. Either use the full"
say " path, as $BIN_DIR/dejima-ci-allowlist, or add a secure_path to"
say " sudoers that includes it. Nothing about the install is wrong."
fi
say "Files this installer owns, for later reference:"
say " config $CONFIG"
say " allowlist $out (generated - do not hand-edit)"
+5
View File
@@ -6,6 +6,11 @@ same on any hypervisor.
## The whole job
**Already have a runner?** This page builds one. To bring an existing VM
forward, start at [Updating](../README.md#updating-an-existing-runner) — the
upgrade notes name the rules to change rather than having you re-read the table
below.
Four steps, and the two pages interleave — follow this order rather than either
page top to bottom.
+15
View File
@@ -276,6 +276,21 @@ stub_check() {
|| { echo "bootstrap does not record tier=privileged"; false; }
}
@test "the hand-install recipe writes where bootstrap writes" {
# v2 gives a privileged operator three install commands, because install.sh
# does not run on that tier and re-running bootstrap on a built machine
# redoes runner setup. Those commands duplicate bootstrap's, so they can
# drift from it - and a tool installed to the wrong path is a tool that is
# not there, reported as "not installed here" three sections later.
branch=$(awk '/elif \[ "\$ROLE" = privileged \]/,/^else$/' "$REPO/bin/dejima-ci-bootstrap")
for dest in /usr/local/sbin/dejima-ci-upgrade /usr/local/share/dejima-ci/upgrades; do
printf '%s' "$branch" | grep -qF "$dest" \
|| { echo "bootstrap no longer installs to $dest"; false; }
grep -qF "$dest" "$REPO/upgrades/0002-ipv6-lan-boundary.md" \
|| { echo "v2's hand-install recipe no longer names $dest"; false; }
done
}
@test "every version from 1 to VERSION has an upgrade note" {
newest=$(tr -d '[:space:]' < "$REPO/VERSION")
for v in $(seq 1 "$newest"); do
+31 -8
View File
@@ -114,21 +114,44 @@ So there are two honest ways to do this, and no third.
If you want the record to survive, it takes the same sequence any guest-side
change takes — [the seven steps](../docs/upgrading.md#a-guest-side-upgrade-is-not-one-step).
And if you are doing the cycle anyway, **run `install.sh` while you are in
it** (confined tier only). It puts `dejima-ci-upgrade` and these notes on the
box, so later versions can be read and recorded without a checkout. Doing the
dance for `mark-node` alone and skipping the install would be the expensive half
of the work for the smaller half of the benefit.
**Install the tool in the same cycle.** A record that survives a rollback is
worth little if the program that reads it does not, and the cycle is the
expensive part either way.
**The full path, immediately after installing.** These programs go to
`/usr/local/sbin`, and `sudo` reaches that only if your `sudoers` sets a
`secure_path` including it — without one, `sudo` inherits your own PATH, and
Debian leaves every `sbin` off a non-root PATH. `sudo printenv PATH` says which
you have. Naming the path here costs nothing and works either way.
**Confined tier**, where `install.sh` does it:
```sh
# ... after rolling back to the clean snapshot, per the seven steps
cd ~/dejima-ci-hardener && git pull
sudo ./install.sh # confined tier only
sudo dejima-ci-upgrade set-tier confined
sudo dejima-ci-upgrade mark-node 2
sudo ./install.sh
sudo /usr/local/sbin/dejima-ci-upgrade set-tier confined
sudo /usr/local/sbin/dejima-ci-upgrade mark-node 2
# ... then re-snapshot and repoint the rollback job
```
**Privileged tier**, where nothing does. `install.sh` does not run here — that
is the grant the tier exists for — so the tool arrives only by
`dejima-ci-bootstrap`, and re-running that on a built machine also redoes runner
setup. The three commands it uses for this part are enough on their own:
```sh
# ... after rolling back to the clean snapshot, per the seven steps
cd ~/dejima-ci-hardener && git pull
sudo install -m 0755 bin/dejima-ci-upgrade /usr/local/sbin/dejima-ci-upgrade
sudo install -d -m 0755 /usr/local/share/dejima-ci/upgrades
sudo install -m 0644 upgrades/*.md /usr/local/share/dejima-ci/upgrades/
sudo /usr/local/sbin/dejima-ci-upgrade set-tier privileged
sudo /usr/local/sbin/dejima-ci-upgrade mark-node 2
# ... then re-snapshot and repoint the rollback job
```
### Or: skip it, and let the check answer for you
Perfectly reasonable, and probably right for v2 specifically. The console work