Add the audit timer, and the guard against naming a path twice

The timer exists because the rules it checks are edited on the hypervisor, in a console, possibly by someone not thinking about this VM at the time. A rule dragged below the DROPs or a Firewall checkbox cleared while debugging something else produces no symptom here at all: the box keeps working, it is simply no longer contained. Hourly means nobody has to remember.

The service runs reach and not all. That is a security decision rather than a tidiness one: the radius report inventories credentials by path, and a timer would write that into the journal on a schedule, where it outlives the run and is readable by anything that later gets root.

units.bats is not a straight copy. It read BIN_DIR out of install.sh, and this repository has none by design, so it now reads the one entry point. Copied unchanged it would have passed by reading a file that does not exist. Its first test also passed vacuously with no units present, because grep failing to open a file gives the same non-zero status the test was looking for; it now asserts there is something to check before checking it.
This commit is contained in:
2026-08-09 23:18:46 -07:00
parent be59f607d2
commit 38a8df5d5a
3 changed files with 104 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
[Unit]
Description=Check that the hypervisor firewall is still applied to this VM
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
# `reach`, not `all`, and that is a security decision rather than a tidiness
# one. The radius report inventories credentials by path, and a timer running
# it would write that inventory into the journal on a schedule, where it
# outlives the session that produced it and is readable by anything that later
# gets root. The reachability half has no sensitive output at all, and it is
# also the only half that can change without anyone touching this VM.
ExecStart=@@BIN_DIR@@/dejima-devbox-audit reach
# The path above is a marker. bootstrap substitutes it and then checks that
# what it wrote is executable, because a unit naming a program that is not
# there fails with 203/EXEC on every start and says nothing anyone reads.
User=root
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
+23
View File
@@ -0,0 +1,23 @@
[Unit]
Description=Re-check the hypervisor firewall periodically
[Timer]
# The rules this checks live on the hypervisor, where they are edited by hand
# in a console, by a person who may not be thinking about this VM at the time.
# A rule dragged below the DROPs, a security group detached, a Firewall
# checkbox cleared while debugging something else: none of those produce any
# symptom on this VM. The box keeps working. It is simply no longer contained.
#
# So the point of a schedule here is not freshness, it is that nobody has to
# remember. Hourly catches it the same day, and three TCP probes to your own
# gateway is not traffic worth noticing.
OnBootSec=5min
OnUnitActiveSec=1h
RandomizedDelaySec=5min
# Catch up after the VM has been off. A missed window is exactly when a change
# went in unobserved.
Persistent=true
[Install]
WantedBy=timers.target
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bats
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2026 Brad Anderson
#
# The systemd units, checked against where bootstrap actually puts things.
#
# These exist because the failure they guard has no symptom worth the name. A
# unit naming a program that is not there fails with 203/EXEC on every start,
# and for a timer that means the firewall silently stops being re-checked: no
# error anyone sees, nothing different on the box, and a host that looks
# configured while verifying nothing at all.
#
# In the sister repository a timer shipped that way and had never once worked
# on any host. The unit said /usr/local/bin and the installer used
# /usr/local/sbin, and nothing compared the two.
setup() {
REPO="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
T="$BATS_TEST_TMPDIR"
}
@test "no unit hard-codes a path to a program bootstrap places" {
# Assert there is something to check first. Without this the test passes
# when systemd/ is empty or absent, because grep's own failure to open a
# file also produces a non-zero status - a green test asserting nothing.
units=("$REPO"/systemd/*.service)
[ -f "${units[0]}" ] || { echo "no units to check"; false; }
# The path belongs to bootstrap. Written down a second time it can only
# agree by luck, and in the sister repository it did not.
run grep -nE '^Exec(Start|Stop)=/(usr|opt|bin|sbin)' "${units[@]}"
[ "$status" -ne 0 ]
}
@test "every unit's ExecStart resolves to the directory bootstrap uses" {
# This repository has no install.sh by design, so the value is read from
# the one entry point instead. Copying this test unchanged would have made
# it pass by reading a file that does not exist.
bin=$(sed -n 's|^BIN_DIR=${BIN_DIR:-\(.*\)}|\1|p' "$REPO/bin/dejima-devbox-bootstrap")
[ -n "$bin" ]
for u in "$REPO"/systemd/*.service; do
exe=$(sed -n 's|^ExecStart=\([^ ]*\).*|\1|p' "$u" | head -1)
[ -n "$exe" ]
# After substitution it must name a program this repository ships.
resolved="${exe/@@BIN_DIR@@/$bin}"
[ "$resolved" != "$exe" ] || { echo "$u: ExecStart is not templated: $exe"; false; }
prog="${resolved##*/}"
[ -f "$REPO/bin/$prog" ] || { echo "$u: names $prog, which bin/ does not ship"; false; }
done
}
@test "bootstrap substitutes the marker rather than copying it through" {
grep -q '@@BIN_DIR@@' "$REPO/bin/dejima-devbox-bootstrap"
# And refuses to finish if what it wrote is not executable.
grep -q 'which is not executable' "$REPO/bin/dejima-devbox-bootstrap"
}