v2.13.9 build progressed past the kiosk assertion and Step 7 rsync.
But then Step 7 (Pi bootloader config) failed :
grep: /tmp/.../mnt/boot/firmware/config.txt: No such file or directory
The tmpfs at /boot/firmware (PR #438) was the wrong primitive. Files
that raspi-firmware writes during postinst (config.txt, vmlinuz, initrd,
dtbs) go to the tmpfs — and are DESTROYED when Step 6.5 umounts it.
Step 7 then rsyncs ${ROOTFS}/boot/firmware/ (empty) to the loop-mounted
real BOOT partition, then tries to read config.txt which doesn't exist.
Replace `mount -t tmpfs` with `mount --bind ${ROOTFS}/boot/firmware
${ROOTFS}/boot/firmware`. The directory becomes a mountpoint
(raspi-firmware's `mountpoint -q` check passes), AND writes go to the
underlying directory on the ROOTFS filesystem (preserved after umount).
Step 7's rsync of ${ROOTFS}/boot/firmware/ to the real BOOT partition
now carries config.txt + kernel + initrd + dtbs that raspi-firmware
populated during build.
This should finally close#436. Chain :
PR #437 — chroot safety net (systemctl wrapper, policy-rc.d, tmpfs)
PR #438 — bump tmpfs 64M→512M + bind /proc + /sys
PR #439 — force WantedBy symlink (absolute, broken host check)
PR #440 — relative symlink (assertion passes)
PR #441 — tmpfs → bind-mount (config.txt persists)
v2.13.8 still failed the kiosk assertion. PR #439's `ln -sf
"/etc/systemd/system/secubox-kiosk.service"` created an *absolute*
symlink that, from the host's perspective, points to
`/etc/systemd/system/secubox-kiosk.service` on the HOST filesystem
(which doesn't exist there). `[[ -e symlink ]]` follows the target,
so the check returned false even though the symlink itself was
present at the right path.
Switch to a *relative* target (`../secubox-kiosk.service`) :
- matches systemctl's own convention for WantedBy symlinks
- resolves correctly inside the chroot at runtime
- resolves correctly from the host's assertion check at build time
This should be the actual last piece. Build chain: chroot safety net
(PR #437) → bigger tmpfs + /proc/sys (PR #438) → WantedBy symlink
(PR #439 absolute, this PR relative) → assertion passes → image ships.
Follow-up to PR #438. v2.13.7 build progressed all the way to the
build-time assertion, which then failed with:
[WARN] secubox-kiosk.service not in graphical.target.wants/
[FAIL] Kiosk artefacts incomplete — refusing to publish a broken image
Under SYSTEMD_OFFLINE=1 inside a qemu-arm64 chroot, `systemctl enable
secubox-kiosk.service` returns 0 but doesn't always materialise the
WantedBy symlink. Create it explicitly right after the enable call
(belt-and-suspenders against any quirk of offline systemctl behaviour).
This is the last piece: chroot safety net (PR #437), bigger tmpfs +
/proc + /sys (PR #438), and now the WantedBy symlink. Expected v2.13.8:
the assertion finally passes, Step 7 rsyncs, image ships.
Chain: #423 → #433 (PR #435) → #436 (PRs #437, #438, this).
Follow-up to PR #437. v2.13.6 build hit two new failures inside the
safety net :
1. **ENOSPC on tmpfs /boot/firmware** — 64M was too small. The
raspi-firmware post-update.d hook copies kernel (~25M) + initrd
(~30M arm64) + dtbs + overlays. Bumped to 512M to cover all
firmware artefacts comfortably.
2. **/proc + /sys not bind-mounted into the chroot** — raspi-firmware's
hook uses `findmnt /boot/firmware` which reads /proc/mounts. Without
/proc mounted in the chroot it errored
`findmnt: can't read /proc/mounts`. Now bound at safety-net install
(matches the pattern build-live-usb.sh uses at lines 277-278) and
unmounted at teardown.
Both errors surface during the apt --fix-broken install OR the apt-get
install kiosk stack, where postinsts trigger update-initramfs which
runs the raspi-firmware hook. The systemctl wrapper + policy-rc.d
parts of the safety net (PR #437) were already working — these two
extras finish the job.
Expected v2.13.7: kiosk install completes cleanly through to the
build-time assertion, image rsyncs successfully.
Refs: #423 → #433 → #436 (this and PR #437 close together).
v2.13.5 rpi400 build aborted on apt --fix-broken install. Root cause:
in a qemu-arm64 chroot, /run/systemd/system is bind-mounted from the
host so systemctl thinks systemd is running, but it can't reach dbus.
Every postinst calling `systemctl enable/start` fails with
"Failed to connect to bus: Host is down", leaving secubox-core (and
the cascade of secubox-* depending on it) installed-but-not-configured.
Three-part safety net installed right after debootstrap --second-stage
(Step 1.5) and torn down before Step 7's image creation (Step 6.5):
1. **systemctl wrapper** — dpkg-divert /bin/systemctl to
/bin/systemctl.distrib, install a thin shim that exports
SYSTEMD_OFFLINE=1 before exec'ing the real binary. Postinsts
that call `systemctl enable X.service` now succeed (filesystem-only
operation) instead of failing on dbus.
2. **policy-rc.d** — /usr/sbin/policy-rc.d returns 101 to block
invoke-rc.d from starting daemons during apt operations.
3. **tmpfs at /boot/firmware** — raspi-firmware's postinst checks
`mountpoint -q /boot/firmware`. The real BOOT partition is only
loop-mounted in Step 7, so we bind a 64M tmpfs at that path during
build; raspi-firmware writes its files there (discarded on umount,
regenerated by Step 7's actual firmware copy). Without this,
apt --fix-broken install fails on raspi-firmware before reaching
the kiosk packages.
Also softens the apt --fix-broken to best-effort (warn instead of
err) — the kiosk install below is now the gate, with `-f` to satisfy
deps. The pre-rsync assertion from #433 stays as the safety net.
Tested locally on v2.13.5 sources : apt --fix-broken now succeeds
(secubox-core configures, cascades complete), kiosk packages install
cleanly, all artefacts present in ROOTFS before rsync.
Expected v2.13.6 outcome: rpi400 image actually ships with kiosk
working out of the box, AND the dpkg state is clean so apt operations
on the running device don't hit broken-deps surprises.
Refs: #423 (kiosk install original) → #433 (silent fail, fail-loud) →
#436 (this, root cause fix).
Three changes to Step 5.4 of image/build-rpi-usb.sh:
1. **apt --fix-broken install FIRST** — the secubox-* .debs installed
via dpkg -i in Step 5 (no dep resolution) leave the rootfs with
broken deps (lxc, debootstrap, python3-cryptography, certbot,
netdata, ... all unmet). apt refuses any new install while that
state exists, so chromium/xserver-xorg/openbox installs silently
failed. fix-broken clears the state (~500 MB extra) BEFORE the
kiosk install. This is the root cause of #433.
2. **Fail-loud on every step** — was `chroot apt-get ... 2>/dev/null
|| warn "Some kiosk packages may have failed (continuing)"`. The
`|| warn` swallowed apt failures and the script printed
`[OK] Kiosk mode installed and enabled` even when nothing was
actually installed. Now apt errors abort the build (err exits
non-zero). Silently shipping a no-kiosk image is worse than no
image — if --kiosk is passed, the user expects kiosk.
3. **Build-time assertion before Step 7 (rsync)** — verifies that
EVERY kiosk artefact made it into ${ROOTFS}:
- chromium / startx / openbox binaries
- secubox-kiosk.sh launcher + .xinitrc
- /etc/systemd/system/secubox-kiosk.service file
- /var/lib/secubox/boot-mode == "kiosk"
- default.target → graphical.target symlink
- secubox-kiosk.service enabled (graphical.target.wants symlink)
Any missing artefact aborts the build. No more discovering after
flash that the .img has no kiosk despite a green CI run.
Also drops the `2>/dev/null` masking on systemctl set-default and
enable — these now fail loud too (and shouldn't fail anymore since
the X stack is properly installed at that point).
Observed v2.13.4: image v2.13.4 shipped with boot-mode="normal",
no chromium, no secubox-kiosk.service, default.target=multi-user.
The CI log claimed "[OK] Kiosk mode installed and enabled". The
apt step had failed loud with "apt --fix-broken install to correct
these" but the warn swallowed it.
Expected v2.13.5: rpi400 image properly ships kiosk-by-default, OR
the CI fails clearly with "kiosk artefacts incomplete — refusing
to publish a broken image" if any prerequisite is missing.
The --kiosk flag in image/build-rpi-usb.sh was parsed but never acted on
(INCLUDE_KIOSK toggled but no install block existed), so the rpi400 image
shipped without chromium / X / kiosk service. The boot menu's apply_mode
"kiosk" branch even references a secubox-kiosk.service that was never
created. Adding all the missing pieces:
- New Step 5.4 block (gated by INCLUDE_KIOSK=1):
- apt install xserver-xorg xinit openbox chromium x11-xserver-utils
- install image/kiosk/secubox-kiosk.sh → /usr/share/secubox/kiosk/
- install image/kiosk/xinitrc → /root/.xinitrc
- create /etc/systemd/system/secubox-kiosk.service (startx on vt7,
Conflicts with getty@tty7; matches apply_mode's expected unit name)
- seed /var/lib/secubox/boot-mode = "kiosk" so first boot enters kiosk
- set default systemd target to graphical.target + enable the unit
- CI build-all-live-usb.yml: add `extra_args: "--kiosk"` to the rpi400
matrix entry and append `${{ matrix.extra_args }}` to the build invocation.
Pi 400 = keyboard + HDMI, kiosk-by-default is the obvious user-facing
shape; other platforms keep their headless default.
The v2.12.16 fix put the nft cache populator in firstboot.sh, which
broke the rule "cron lives next to the module that consumes it" — the
SOC firewall_summary endpoint is owned by secubox-hub, so the timer
that feeds its cache belongs in the same package.
This commit:
* removes the inline cache populator from image/firstboot.sh
* ships /usr/sbin/secubox-nft-cache + secubox-nft-cache.{service,timer}
inside the secubox-hub package, with the timer auto-enabled via the
timers.target.wants/ symlink
* adds a sudoers fragment authorising user secubox to run
`nft list *`, `nft -j list *`, and `systemctl --no-block start
secubox-nft-cache.service` (and only that one unit)
* rewrites the /firewall_summary endpoint so the cache is a speed
optimisation, not the source of truth: if the cache file is missing
or older than 60 s, fall back to a realtime `sudo nft list`,
surface `source: "realtime"` in the JSON, and nudge the cache
populator via systemctl so the next request lands on a hot cache
Per the operator directive: "les cache double buffer ne doivent pas
tomber et permettre juste d'aller plus vite... un echec de cron et de
cache doit forcer le cron et faire du realtime".
Operator screenshot: SOC dashboard "🔥 FIREWALL nftables / Status
INACTIVE / Tables 7 / Chains 10 / Rules 19" with all counters at 0
(Accept/CAPI/CrowdSec/Manual).
Two bugs found:
1. The /api/v1/hub/public/firewall_summary endpoint docstring says
it reads cache files `/var/cache/secubox/nft-*.txt` "updated by
cron every 30s" — but the cron never existed in the repo. The
table/chain/rule counts were getting populated somehow (probably
a one-shot at firstboot) while the counters file stayed empty,
hence 7/10/19 visible but all hit-counts zero.
2. The endpoint never returned a `status` field, so the React
bundle (secubox-soc-web) defaulted to "INACTIVE" regardless of
whether nftables.service was active or rules were loaded.
Fixes:
* firstboot.sh installs `/usr/local/bin/secubox-nft-cache` +
`secubox-nft-cache.service` + `.timer` (oneshot, every 30s,
OnBootSec=10s). Runs as root via systemd so it can call
`nft -j list ruleset` + `nft list counters`. Writes the JSON
ruleset + counters dump + a `.lastrun` sentinel into
/var/cache/secubox/.
* hub `/firewall_summary` endpoint now:
- calls `systemctl is-active nftables.service` and returns a
`status` field ("active" | "inactive" | "active-manual" | "error")
- "active-manual" = systemd reports inactive but the kernel has
rules loaded (happens when firstboot.sh did `nft -f` directly
and the systemd unit didn't get re-enabled). Treat as active
for dashboard purposes — operator wants to see "firewall is up"
when there ARE rules.
- returns `cache_last_run` so the dashboard can warn on stale
cache.
WAF metrics empty is by design on a live USB without mitmproxy LXC
(THREATS_LOG never gets written → zero counters). Not addressed
here. Operator runs install-lxc.sh once to enable WAF traffic.
rpi400 v2.12.13 image audit (operator-reported "ancien style et il
manque des tas de seccubox tools"):
136 secubox-* packages installed ✓
but only 11 secubox-* binaries on PATH
/etc/motd was vanilla Debian
/etc/issue was vanilla Debian "Debian GNU/Linux 12 \n \l"
no /etc/secubox/build-info.json
build-live-usb.sh creates ~30 secubox-* CLI helpers + the cyber-CRT
boot banner + dynamic MOTD via update-motd.d. None of that was in
build-rpi-usb.sh. Targeted port of the four most-visible blocks
into build-rpi-usb.sh at line ~780 (right after `SecuBox packages
installed`):
* /etc/issue with cybermind banner + getty `\4` IPv4 escape
* /etc/update-motd.d/10-secubox with live `hostname -I` substitution
* /usr/bin/secubox-status (system overview, services, network)
* /usr/bin/secubox-help (command index)
* /etc/secubox/build-info.json (version + git commit + builder)
Broader refactor of the four build-*.sh siblings into shared lib/*.sh
is tracked in docs/superpowers/plans/2026-05-26-build-scripts-refactor.md
— this commit is the tactical port to unblock rpi400 operators now.
The MOTD says "RPI400 LIVE" instead of "LIVE USB MODE" so operators
can tell at a glance which platform they're on without `uname -m`.
secubox-net-detect.service runs on first boot (gated by
!/var/lib/secubox/.net-configured) and REWRITES /etc/netplan/00-secubox.yaml
based on the detected board layout. The "no LAN interfaces" branch
in generate_netplan() was emitting an empty br-lan bridge with a
static 192.168.1.1/24 address. On single-NIC live USB hardware that
made systemd-networkd happily honour the phantom bridge while the
real WAN NIC fell off the DHCP path — the operator saw a working
br-lan in `ip a` but no upstream IP and no internet.
Same shape of bug as the v2.12.7 build-live-usb.sh static-in-DHCP
regression (commit a88c1773): a useless static IP on a non-routing
interface confused networkd into "I'm done, no DHCP needed". Fix:
when there are no LAN interfaces, emit NO bridges block at all.
Operators wire br-lan later via secubox-net-* tools once they've
decided the box is a router vs an endpoint.
v2.12.11's rule `udp sport 67 udp dport 68 accept` was invalid nft
syntax — protocol prefix can't be repeated inside a single rule, the
firstboot.sh-generated /etc/nftables.conf failed to load and the
firewall stayed in its previous state (or empty), defeating the
whole point of the DHCP fix.
Just match the destination port: `udp dport 68 accept`. DHCPOFFER /
DHCPACK from any server (sport 67) lands on dport 68 of the client,
so the inbound match is sufficient and unambiguous.
Live-system workaround (no rebuild needed):
nft add rule inet secubox_filter input udp dport 68 accept
Bare-metal box ended up with 192.168.10.250 (assigned by secubox-net-
fallback's ARP-probe loop) instead of the real DHCP lease from the
LAN router. Operator confirmed DHCP server is healthy on the LAN
(dev box on same switch got 192.168.1.13 via DHCP without issue).
Root cause: firstboot.sh writes /etc/nftables.conf with policy=drop
on the input chain and no explicit DHCP allowance. The `ct state
established,related accept` rule does NOT cover DHCP because the
client request leaves from 0.0.0.0:68 and the server reply comes back
broadcast (or unicast direct to the offered IP before it's bound).
Neither matches the original 5-tuple → conntrack treats the reply
as a new packet → policy drop → DHCPOFFER lost → networkd times out.
secubox-net-fallback then picks a random gateway from its probe
list and lands the box on the wrong subnet.
Add `udp sport 67 udp dport 68 accept` to the input chain. This
matches DHCPv4 server-to-client replies specifically (port 67/68
are reserved BOOTP/DHCP). DHCPv6 would need a sibling `udp dport
546 accept` rule but is out of scope here.
Real UEFI hardware kept landing in grub rescue / grub shell across every
v2.12.x iteration: shim swap, hardened module list, $cmdpath embed,
search --fs-uuid bake-in. None of them shipped a working real-UEFI
boot. v2.10.3 was the last tag the operator confirmed booted his
hardware in both BIOS and UEFI mode.
Drop the experimental UUID capture + the embed-cfg fallback chain.
Restore EXACTLY what v2.10.3 had:
# embed-cfg
search --no-floppy --label ESP --set=root
set prefix=($root)/boot/grub
configfile $prefix/grub.cfg
# main grub.cfg LIVE search
search --no-floppy --label LIVE --set=live
Validated other v2.12.x improvements stay (mass-mask LXC services,
dynamic MOTD, modesetting for VBox, kiosk --no-block) — they don't
touch the boot path.
Real UEFI hardware v2.12.7/v2.12.8 still landed in the grub> rescue
shell, and the user reported the same happening in legacy BIOS mode
too — both code paths share the /boot/grub/grub.cfg which begins with
`search --no-floppy --label LIVE --set=live`. If that label lookup
fails (some firmware doesn't surface FAT/ext4 labels, or the firmware
exposes the disk through a path GRUB's label table doesn't recognise)
\$live stays unset, every menuentry's `linux (\$live)/live/vmlinuz`
becomes `linux ()/live/vmlinuz`, the kernel doesn't load, GRUB
drops the operator at the rescue prompt. Same root cause as the
EFI embed-cfg's `search --label ESP` failure.
Capture the ESP + LIVE UUIDs with blkid right after mkfs.* and
substitute them into both configs:
* grub.cfg uses `search --fs-uuid <LIVE_UUID> --set=live` as
primary; falls back to `search --label LIVE` for firmware that
DOES prefer labels (no-op if UUID already resolved).
* embed-cfg uses `\$cmdpath/grub.cfg` first, then falls back to
`search --fs-uuid <ESP_UUID>` + configfile from that root.
UUIDs are deterministic at build time, can't be shadowed by similarly
labelled partitions, and don't depend on the firmware's label
indexing — should work on any firmware that surfaces the disk to
GRUB's block IO at all.
Real UEFI hardware v2.12.7 reported landing at the grub> rescue
prompt even with the simple pre-e1a53297 embed-cfg restored. Cause:
`search --no-floppy --label ESP --set=root` failed silently on that
firmware (FAT label not recognised, predictable rename, etc.), so
\$root stayed unset, prefix was empty, configfile fell through to
the rescue shell.
Replace the embed with a single-line `configfile \$cmdpath/grub.cfg`.
\$cmdpath is set by the EFI firmware itself to the directory it
loaded BOOTX64.EFI from — for us that's (hdX,gptY)/EFI/BOOT, where
the build copies grub.cfg already. The full menu at /EFI/BOOT/grub.cfg
still does its own `search --label LIVE --set=live` to locate the
squashfs partition (unchanged), so this only changes the very first
lookup hop.
Legacy BIOS grub-install --target=i386-pc untouched — it never had
the issue.
Real-hardware report: only the phantom `br-lan` (interfaces: [] +
static 192.168.1.1/24) showed an IP, the physical ethernet stayed
sec. The old netplan declared three separate interface groups
(en*, eth*, all-wifi with empty SSID) plus a member-less bridge with
a static address — that was enough to confuse networkd into
honouring the bridge while dropping the real DHCP request on the
floor.
Cut to the bone: one `eth-all` block matching `e*` (which covers
enpXsY, enoX, ensX, enxAABBCC and ethX), DHCP only, no static, no
bridge, no wifi-with-empty-SSID. Operators wire router-mode br-lan
or wifi later via the secubox-net-* tooling once they've decided
this is an endpoint vs a router.
For the no-DHCP fallback case, secubox-net-fallback.service is
already shipped and runs after networkd to ARP-probe common gateways
and assign a free .250 IP — that remains unchanged.
This means v2.12.6 has both a clean EFI build AND a netplan that
actually requests DHCP on the physical NIC instead of squatting on
the bridge.
Real UEFI hardware boot was broken by my e1a53297 "hardening" — the
gold-plated module list (disk/usb/usbms/ahci/ata/...) and multi-stage
embed-cfg search fallback that I added to chase a VBox EFI Shell drop
turned out to break legitimate UEFI firmware too. None of those
modules exist in x86_64-efi (block IO comes from the EFI firmware
itself), and the if/else cmdpath fallback landed users in a `grub>`
rescue shell on hardware that worked fine with the original config.
Revert the EFI section to the v2.10.x layout that v2.10/v2.11 booted
cleanly on real amd64 boxes: 14 modules, single-line search, plain
grub-mkimage with no compress/no Secure Boot shim. Also drop the
shim-signed + grub-efi-amd64-signed from the host apt install — they
were only there to feed the SB swap that's now removed.
VBox EFI drops to the GRUB rescue shell with this config; that's
acceptable per operator direction ("je m'en fous de booter en legacy
sur vbox, ca marche sur real hw") — VBox testing has always used the
BIOS firmware path which still works perfectly.
Live USB v2.12.4 boot showed `secubox-kiosk-setup enable --x11` running
for 5+ minutes while the kiosk.service stayed `inactive (dead)`. The
synchronous `systemctl start secubox-kiosk.service` at the end of
enable_kiosk was hanging.
Root cause: secubox-kiosk.service declares
Conflicts=getty@tty7.service
TTYPath=/dev/tty7
systemd waits for tty7 to be released and for the conflicting unit's
JobsToStart to clear before kiosk's ExecStart can fire. When the call
comes from a process running inside the tty1 autologin shell (which
is what firstboot.sh's `secubox-kiosk-setup enable --x11` safety net
does), systemd deadlocks itself waiting on a tty operation the caller
can't unblock. The kiosk service appears to start fine when triggered
manually from SSH (proven during debug — once the stuck setup PID was
killed, `systemctl start secubox-kiosk.service` returned in <1s and
the service activated cleanly).
Fix: pass --no-block so systemctl returns as soon as the job is
queued, regardless of the tty wait. systemd then runs the start in
the background after the parent autologin shell exits, breaking the
loop. The exit-code branch is preserved for legitimate failures.
Three regressions surfaced on the v2.12.3 live USB amd64 boot console:
1. /etc/issue + /etc/motd had `<IP>` as literal text — nothing was
substituting at runtime, so operators saw `https://<IP>:9443`
instead of the actual address. The bashrc splash showed real IP
via `hostname -I` but was masked by the static MOTD on top.
Fixes:
* /etc/issue: switch to getty's `\4` escape (resolves to first
IPv4 address at TTY render time).
* /etc/motd: blank out the static file + ship the banner as
/etc/update-motd.d/10-secubox, which pam_motd regenerates on
every interactive login. The script substitutes the live
`hostname -I` first address (falls back to `no-ip` while DHCP
hasn't completed).
2. Kiosk Xorg failed under VirtualBox VMSVGA. The auto-detect picked
the `vmware` Xorg driver, which loads vmwgfx kernel module — vmwgfx
tries to talk to a VMware Workstation host channel that doesn't
exist on VBox, prints "Failed to send host log message" then
bails. The kiosk launcher then logs Xorg failure, retries 3 times,
self-disables. Operator sees no kiosk even after running
`secubox-kiosk-setup enable` (because the launcher disabled the
sentinel again).
Fix: secubox-x11-setup unconditionally picks `modesetting` on
oracle (VirtualBox), regardless of VMSVGA vs VBoxVGA. modesetting
uses DRM/KMS through vmwgfx but skips the broken host channel.
Real VMware Workstation users (VM_TYPE=vmware) still get `vmware`
via the dedicated case branch — only the VBox path changed.
Note: didn't tag yet — wait for user retest after CI rebuilds. If the
boot console comes up with the right IP + the kiosk paints pixels, we
tag v2.12.4 next.
VBox + bare-metal v2.12.2 amd64 tests showed the live USB had no IP at
all (DHCP nor static fallback worked) and ~10 [FAILED] secubox-*
services on the boot console.
Two independent fixes layered together:
1. Inline netplan: my v2.12.1 commit (27645f0a) baked
`addresses: [192.168.1.55/24]` + gw 192.168.1.254 into both
eth-dhcp (match en*) and eth-legacy (match eth*). On a single-NIC
box both interface defs match the same kernel interface and try to
claim 192.168.1.55 simultaneously → systemd-networkd refuses the
apply and the link gets NO IP at all. Even DHCP was broken.
Drop the static block entirely; let secubox-net-fallback.service
(already shipped, runs after networkd) handle the no-DHCP case via
ARP-probe gateway discovery + free .250 IP assignment.
2. INCOMPLETE_MODULES list: extended with the host-side control planes
that proxy to an LXC container — without `install-lxc.sh` having
run, the container at 10.100.0.X doesn't exist, the health probe
fails immediately, and Restart=on-failure spins the service into a
FAILED loop that fills the console and blocks multi-user.target.
Added: grafana, yacy, rustdesk, lyrion, authelia (host), mail,
gitea, matrix, horde, mitmproxy, nextcloud, rbs-sensor.
Operator workflow stays unchanged on real installs: run
`bash install-lxc.sh` to provision the container, then
`systemctl unmask secubox-<name>` + enable + start.
Both modules need physical hardware (RTL-SDR, EP06 modem) and crash
on `import gr_osmosdr` when those aren't there. On the live USB
amd64 build neither is present, so the service died at startup,
systemd restarted in 5s under `Restart=on-failure`, and the cycle
repeated until /var/log filled with hundreds of FAILED lines —
visible on the boot console and blocking the kiosk target from
ever finishing.
Two fixes layered:
1. systemd unit (sentinelle-gsm.service): add StartLimitIntervalSec=30
+ StartLimitBurst=3 + StartLimitAction=none. If start fails 3
times in 30 s, give up instead of looping forever. The kiosk's
multi-user.target settles even when the SDR is missing.
2. build-live-usb.sh: mask secubox-sentinelle-gsm + secubox-fmrelay
in the INCOMPLETE_MODULES list alongside other hardware-gated
modules. Operators enable + unmask by hand once they wire the
physical RTL-SDR / GSM modem.
Operator quick fix on an already-flashed live USB:
sudo systemctl stop secubox-sentinelle-gsm
sudo systemctl mask secubox-sentinelle-gsm
sudo systemctl mask secubox-fmrelay
sudo systemctl daemon-reload
Then `secubox-kiosk-setup enable --x11` should complete normally.
v2.12.1 booted in VBox dropped to the EFI Shell PXE prompt: BOOTX64.EFI
was built but the module set lacked `disk` / `usb` / `usbms` so OVMF
couldn't hand off block IO once GRUB took over. Same image worked in
BIOS mode — the i386-pc grub didn't need those modules.
Hardening:
* GRUB_MODS now includes `disk usb usbms ahci ata` + `loadenv` +
`gfxterm_background gzio png jpeg font sleep reboot halt true exfat
iso9660 ntfs search_fs_uuid search_fs_file` — the full toolbox a
USB / VM EFI handoff needs.
* `grub-mkimage` failure aborts the build instead of warning. The
embed-cfg's `search` chain falls back through label-ESP →
label-LIVE → `$cmdpath/../../boot/grub` so the bootloader still
finds the menu when the partition label was lost (some flashers
rewrite it).
* `--compress=xz` on the EFI binary shaves ~40% off its size.
* Secure Boot: if `shimx64.efi.signed` + `grubx64.efi.signed` are
available on the builder (they're already installed by CI line 108),
ship the Microsoft-trusted shim AS BOOTX64.EFI and the signed grub
alongside. Falls through to plain unsigned grub when those packages
are missing locally, with a noisy warn explaining how to install
them.
* `grub-install --target=i386-pc` failures now propagate via err
instead of warn — silent BIOS install failure was the v2.10.x
"boots into nothing" footgun.
* Mirror BOOTX64.EFI to /EFI/secubox/grubx64.efi so a later
`efibootmgr -c` can register a named entry without clobbering
/EFI/BOOT.
* Local apt-get install adds `grub-efi-amd64-signed` + `shim-signed`
so dev-host builds get the same Secure Boot output as CI.
Two bugs surfaced on the v2.12.0 amd64 live USB build (user-tested on
both VirtualBox + bare-metal amd64 2026-05-24):
1. **IP**: the static fallback added to board/x64-live/netplan/
00-secubox.yaml in commit 264e23e4 never reached the booted image
because image/build-live-usb.sh writes its OWN inline netplan
template (line 782, here-doc) that's a copy of the board file
without the static block. Both DHCP-only matches `en*` and `eth*`
now also carry `addresses: [192.168.1.55/24]` + gw 192.168.1.254 +
nameservers, with a high route metric (1000) so DHCP still wins
when available — but the appliance stays reachable on .55 when it
doesn't (the original #370 intent).
2. **Kiosk**: build-live-usb.sh touches /var/lib/secubox/.kiosk-
enabled + symlinks secubox-kiosk.service into multi-user.target.
wants at build time, but operators reported having to run
`secubox-kiosk-setup enable` manually on first boot. Suspected
cause is a live-boot persistence layer that masks parts of /var,
needs more digging. As a safety net firstboot.sh now invokes
`secubox-kiosk-setup enable --x11` whenever it can't find the
sentinel — idempotent, no-op on a working image, recovers a
broken one without needing console intervention.
Symptom: pre-login banner on ttyS0 (Minicom 2.9 VT102) rendered as
trailing `�` after every emoji line. The console driver couldn't
decode the UTF-8 multi-byte sequences for ⚡🔐🌐📡 and emitted the
replacement character instead.
The `█` box-block characters (U+2588) DO render correctly in VT102+
UTF-8 — they're a single-byte-class glyph in most modern fonts. We
keep those so the SECUBOX wordmart still looks like SecuBox.
Replacements (compatible with VT102 minicom AND SSH UTF-8 alike):
⚡ -> >>
🔐 -> [user]
🌐 -> [web]
📡 -> [ssh]
The /etc/motd block (shown after SSH login) is left untouched — SSH
sessions have proper UTF-8 terminals so the rich emojis are fine
there.
Also add scripts/build-kernel-local.sh (new): the local kernel build
script that I authored today for #255. Documented; runs the same
defconfig + merge_config + scripts/config + olddefconfig + sed ZRAM
override + bindeb-pkg flow as .github/workflows/build-kernel.yml,
without the 20-min GitHub Actions ping-pong.
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Two regressions on a --static-ip image:
1. Default match `name: "eth0"` matched nothing on modern Debian. The
booted kiosk ended up on `enp0s31f6` with link-local 169.254.x.x
because the static stanza was inert.
Fix: default STATIC_IFACE="en*" (covers enp*/eno*/ens*/enx*).
2. firstboot.sh ran `secubox-net-detect apply router` unconditionally
if the binary was present, even though build-live-usb.sh's
--static-ip branch pre-touches /var/lib/secubox/.net-configured to
short-circuit the systemd unit. That re-injected the router-mode
bootstrap (WAN + br-lan 192.168.1.1/24), colliding with the user's
gateway.
Fix: gate the firstboot net-detect block on the marker file.
Also clarify --help wording about glob semantics.
Closes#128.
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
* image/splash/tui-splash.sh: collapse alternating G1/G2 per-letter
coloring to a single ROOT-green (ANSI 29 ~ Charte #0A5840) across
the whole SECUBOX word.
* image/build-image.sh + image/build-live-usb.sh: pre-login /etc/issue
banner switched from 256-color orange (38;5;214) to ROOT green
(38;5;29). Post-login MOTD untouched (it keeps cyan-on-orange-frame
for contrast on console).
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Three operator tools and one discoverable alias were missing from the
shipped images:
- `secubox-flash-disk` and `secubox-flash-emmc` — internal disk/eMMC
flashing from a running system. Both exist in `image/sbin/` but only
flash-disk was installed by build-live-usb.sh (and neither by
build-image.sh).
- `secubox-console-tui` and `secubox-factory-reset` — console-side
dashboard + factory-reset. Live image installed them; the system
image didn't.
- `secubox-kiosk-tui` — operator-expected discovery name for the
kiosk-mode fallback TUI. Now ships as a symlink to
`secubox-console-tui` (matches the `kiosk` operating mode in
`secubox-mode`).
build-image.sh now installs all four tools + the symlink for every
board (mochabin/espressobin/vm-* — eMMC flashing is just as relevant
on arm64 SBCs as on x86_64).
build-live-usb.sh extends its existing first install loop with
`secubox-flash-emmc` and adds the same `secubox-kiosk-tui` symlink.
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Bare-metal x86_64 validation of v2.10.1 surfaced two ergonomic issues:
1) Three Pi-hardware services [FAILED] in a tight loop on x86_64:
secubox-healthbump (I2C), secubox-led-trigger (LEDs), secubox-picobrew
(1-Wire fermentation sensors). The boot console got flooded with
restart attempts until systemd's StartLimit kicked in. The sibling
`secubox-led-heartbeat.service` already uses ConditionPathExists to
skip silently — apply the same pattern.
Use `ConditionArchitecture=arm64` since all Pi/MOCHAbin/ESPRESSObin
targets are arm64. systemd skips the unit with a "condition failed"
line in journal but no [FAILED] noise on the console.
2) image/build-image.sh:606-607 installed only xserver-xorg-video-fbdev,
leaving modern Intel/AMD/Nvidia bare-metal at the 30-90s X11 cold-init
fallback path. Expand the install line to xorg's standard set:
fbdev, vesa, intel, amdgpu, radeon, nouveau, qxl, vmware. ~15 MB
extra; KMS modesetting picks the right one at boot for both bare-metal
and VM targets.
Validated path: VBox amd64 kiosk still loads in ~5s (vmware/qxl drivers
present). Bare-metal x86_64 USB boot should hit X11 init under 15s,
boot console no longer shows recurring healthbump/led-trigger/picobrew
[FAILED] lines.
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
On bare-metal x86_64 with the v2.10.0 image, chromium's zygote crashes
in a restart loop:
ERROR:content/browser/zygote_host/zygote_host_impl_linux.cc(...):
... disabled unprivileged user namespaces with AppArmor,
... you can try using --no-sandbox.
Debian 12 ships AppArmor with the `unprivileged_userns` profile that
restricts the unprivileged user namespace creation chromium's zygote
needs to set up its renderer sandbox. The kernel sysctl
`kernel.unprivileged_userns_clone` is `1`, but the AppArmor profile
whitelists only known binaries — chromium launched as the
`secubox-kiosk` user doesn't match.
`secubox-kiosk-launcher:443-451` only passed `--no-sandbox` for VM
targets (`systemd-detect-virt` non-`none`). On bare metal it omitted
the flag, so the zygote died on every launch → systemd restart loop
until MAX_FAILURES=3 disabled the kiosk and left X11 stranded on VT7.
Drop `--no-sandbox` from both branches. The SecuBox kiosk runs as a
dedicated unprivileged user on a single VT, AppArmor still enforces
its per-process chromium profile, and the appliance sits behind its
own WAF — chromium's internal sandbox provides marginal additional
security here while breaking the kiosk entirely.
Validated path: VBox kiosk continues to work; bare-metal x86_64 will
reach the SecuBox login form after this fix.
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
* fix(image): pull Debian deps for slipstreamed packages (closes#218)
dpkg -i --force-depends in the slipstream step left python3-argon2,
python3-pyotp, python3-qrcode, python3-jsonschema, python3-jose,
python3-cryptography, python3-maxminddb and ~8 other declared deps
uninstalled. firstboot.sh then crashed on `from argon2 import
PasswordHasher`, users.json was never created, secubox-portal failed
to start, and the kiosk fell into a /portal/login.html redirect loop
on every fresh image.
Two-step fix in build-image.sh:
1. Pre-install the auth/crypto/observability deps via INCLUDE_PKGS
so they exist before any .deb is unpacked.
2. After `dpkg -i --force-depends`, run `apt-get install -f -y` to
resolve whatever else the slipstreamed packages declared. The
misleading "pip provides Python deps" comment is replaced.
Validated against amd64 CI run 26079813517 (commit 53a630e9) where
the redirect loop was observable in the VBox kiosk and the boot
journal showed secubox-firstboot.service / secubox-portal.service
FAILED.
* fix(image): drop kernel-touching deps from INCLUDE_PKGS (ref #218)
CI run 26081463597 failed: python3-zmq postinst aborts in debootstrap
second-stage (touches kernel/devfs before the rootfs is mountable).
Trim INCLUDE_PKGS to bootstrap-critical only: keep python3-argon2 (used
by firstboot.sh before any service starts) and rely on the new
`apt-get install -f -y` step to pull the long tail (python3-zmq,
pyroute2, evdev, serial, jose, cryptography, maxminddb, websockets,
qrcode, pyotp, jsonschema, rich) once /proc and /sys are real.
apt-get install -f remains the real fix from the previous commit —
this commit only narrows what's pre-staged at debootstrap time.
* fix(image): no C-extension Python pkgs in INCLUDE_PKGS (ref #218)
CI run 26081721762 still failed: python3-argon2 also breaks debootstrap
second-stage. Same root cause as python3-zmq — any Python package with
a compiled C extension fails to configure in the second-stage chroot
where /proc and /sys aren't fully set up yet.
Revert all new INCLUDE_PKGS entries from this branch. The first commit
(apt-get install -f -y after dpkg -i) is the real and sufficient fix —
once /proc and /sys are real, apt happily resolves every secubox-*.deb
declared dep including argon2, jose, cryptography, maxminddb, zmq, etc.
---------
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Fresh images had admin / secubox login always failing because
image/firstboot.sh wrote users.json with:
- Flat schema {"admin": {...}} instead of v2 {"users": [{...}]}
that secubox_core.user_store expects.
- SHA256 hash instead of argon2 that verify_password uses.
- And no /etc/secubox/auth.toml so the fallback path was also dead.
End result: locked out of every fresh image at the web UI; only root /
secubox shell login worked (unrelated chpasswd path earlier in firstboot).
Fix: replace the broken sha256sum + flat-dict heredoc with an inline
Python that uses argon2.PasswordHasher (already a secubox-core dep) and
writes the v2 schema directly. must_change_password is true so the very
first successful login is gated on a real password change — bypassing
password_policy.validate() (which would reject "secubox" as too weak)
is acceptable for the seed because the next interaction forces the upgrade.
Verified roundtrip locally:
ph.hash("secubox") -> argon2id$v=19$...
ph.verify(h, "secubox") -> True
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-all-live-usb.yml workflow passes `--slipstream` to every live-USB
build script when there are .deb files in output/debs/, but
build-mochabin-live-usb.sh only knew `--no-slipstream` (with slipstream
ON by default). The workflow's `--slipstream` therefore hit the `*) err
\"Unknown argument: \$1\"` branch and the job failed after ~50 min of
chroot setup — which is why mochabin live-USB was missing from v2.9.0
while x64 + rpi400 shipped fine (their scripts already accept both
forms).
This adds the no-op match, identical to what build-live-usb.sh and
build-rpi-usb.sh do at the same place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#124 — Don't bind-mount the host's /dev into the chroot.
The previous `mount --bind /dev "${ROOTFS}/dev"` exposed the host's
/dev to every chroot process. A killed build mid-debootstrap can
leave the host's /dev with regular files in place of device nodes,
or wipe entries the kernel can't auto-recreate. Effect today: every
shell on the host failed with `/bin/bash: /dev/null: Permission denied`
because bash startup uses `2>/dev/null`. Full recovery required
manual `mknod` for each missing node — there is no undo for damaged
devtmpfs without a reboot.
Switch to an isolated tmpfs at "${ROOTFS}/dev" with manually-created
minimal device nodes (null, zero, full, random, urandom, tty,
console, ptmx) + symlinks for stdin/stdout/stderr/fd. Chroot ops
can no longer reach host device nodes.
Additional safety nets:
- Startup sanity check: refuses to run if host /dev/null is not a
character device (with the exact recovery command in the error).
- Cleanup fallback: if /dev/null is missing/corrupted at exit
(despite isolation), recreate it.
- Trap extended to INT and TERM, not just EXIT.
#125 — VBox guest additions actually install now.
contrib is enabled in the chroot's sources.list at step 3 (already
on master), but the install at step 6 was silenced with `2>/dev/null`,
hiding any real failure. Remove the swallow, refresh apt indices
before the install, and emit a structured OK / WARN line so we know
exactly what happened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When --static-ip CIDR is set:
- Replaces the bootstrap DHCP netplan with a fixed-IP config on the
interface chosen by --iface (default: eth0)
- Honors --gateway (required), --dns (default: gateway, comma list ok)
- Pre-touches /var/lib/secubox/.net-configured so secubox-net-detect
short-circuits at first boot and doesn't overwrite the static config
--hostname NAME overrides the baked-in "secubox-live" hostname in both
/etc/hostname and /etc/hosts.
When --static-ip is absent, behavior is unchanged (DHCP bootstrap +
secubox-net-detect at first boot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New build-mochabin-live-usb.sh for ARM64 live USB images
- Includes LED kernel support (IS31FL3199 driver)
- Recovery tools and diagnostics built-in
- extlinux boot configuration for Tow-Boot/U-Boot
- Network config for 10GbE + SFP+ + 4x GbE
Recovery documentation added to issue #39
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
python3-cryptography fails to configure under QEMU ARM64 emulation
during debootstrap due to Rust compilation requirements.
It's already installed via pip later in the build process, which
works reliably cross-architecture.
Fixes rpi400 image build failure.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Services were failing in restart loops due to missing dependencies:
- python-multipart: required for FastAPI file upload endpoints
- email-validator: required for Pydantic email field validation
Updated build scripts:
- build-image.sh: added both packages
- build-rpi-usb.sh: added email-validator (multipart was present)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
dpkg leaves .dpkg-new suffix on conffiles when installing over existing
configs. This caused nginx API routing to fail as the module configs
(system.conf, etc.) were not loaded.
Added activation step to all build scripts that renames .dpkg-new files
to their proper .conf names after package installation.
Affected scripts:
- build-image.sh
- build-live-usb.sh
- build-rpi-usb.sh
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Build scripts copied ALL .deb packages without architecture filtering.
When building arm64 images, amd64-only packages (secubox-daemon,
secubox-c3box) were included and dpkg failed with "package architecture
does not match system".
Fixed in:
- build-image.sh: filter by $DEBIAN_ARCH variable
- build-ebin-live-usb.sh: filter for _all.deb and _arm64.deb
- build-live-usb.sh: filter for _all.deb and _amd64.deb
This fixes GitHub Actions workflow failures on arm64 boards.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
VMs with multiple interfaces (NAT + host-only) now correctly configure
all interfaces with DHCP instead of creating a br-lan bridge that
conflicts with VBox's host-only DHCP server.
Changes:
- Add dedicated x64-vm case in get_interface_config()
- Set profile="vm" for VMs to trigger single mode (no bridge)
- In generate_netplan(), detect x64-vm board and configure ALL
physical interfaces with DHCP instead of WAN-only
- VM profile forces mode="single" in main() to skip bridge creation
This fixes host-only network access for VirtualBox VMs while
preserving router/bridge functionality for baremetal deployments.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix vm_type variable sanitization in kiosk launcher
- Enhance network detection for AMD64 hardware
- Improve build-live-usb service configuration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Problem: IP assignment failures on bare metal AMD64 due to broken
netplan wildcard syntax - using wan0: with match: name: "e*" but
no set-name: directive caused conflicts on multi-interface systems.
Solution:
- Split single e* pattern into separate en* and eth* matches
- Different route metrics (100/200) for deterministic routing
- Added br-lan bridge pre-definition for router mode
- Enhanced secubox-net-detect with better logging and patterns
- New secubox-net-reset utility for forcing re-detection
Changes:
- board/x64-live/netplan/00-secubox.yaml: Fixed bootstrap config
- image/build-live-usb.sh: Updated embedded netplan with same fix
- image/sbin/secubox-net-detect: Enhanced interface detection
- image/sbin/secubox-net-reset: New utility script
- CLAUDE.md: Added Debian shell scripting guidelines
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Eye Remote Interactive UI enhancements:
- TTY mode: Serial terminal display from /dev/ttyGS0
- Flash mode: Progress bar with speed/ETA for USB transfers
- Auth mode: QR code generation for backup authentication
- Mode detection via /etc/secubox/gadget-mode
Hub service VM compatibility fix:
- Changed from Unix socket to TCP port 8001
- Updated nginx configs for TCP proxy
- Fixes 502 errors in VirtualBox VMs
Also includes:
- FAQ/Troubleshooting wiki page with GitHub issue links
- Kiosk launcher --no-sandbox fix for VMs
- Profile Generator GUI mockup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CORS headers to nginx secubox-proxy.conf for cross-origin API requests
- Fix login.html endpoints: /auth/login -> /login
- Upgrade Python deps in build scripts: pydantic>=2.0, fastapi>=0.100, uvicorn>=0.25
- Add pip upgrade in secubox-core postinst for Debian bookworm compatibility
- Fix display/__init__.py to import existing modules only
Fixes authentication and API issues in VBox and ebin builds.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Display system for Pi Zero Eye Remote (HyperPixel 2.1 Round 480x480):
Splash Screen (splash.py):
- Animated phoenix logo for boot/halt/start/reboot states
- Pulsing glow effects with fire colors
- Progress indicator ring with rotating dots
- Fallback phoenix symbol if logo image missing
Fallback Display Manager (fallback_manager.py):
- Connection state detection (OTG 10.55.0.1, WiFi secubox.local)
- Four modes: OFFLINE, CONNECTING, ONLINE, COMMUNICATING
- Local metrics radar with 6 concentric rings
- 3D rotating cube with module icons when connected
- Rainbow sweep line animation
Touch Analysis Tools:
- touch_analyzer.py: Noise pattern analysis (Y-axis oscillation at stable X)
- touch_calibrate.py: Corner target display for manual calibration
- touch_filter.py: X-stable noise filtering
Radar Variants:
- radar_flashy.py: Vibrant colors with 3D cube
- radar_concentric.py: Balanced metric arcs centered at 12 o'clock
- radar_rainbow.py: Rainbow colorization with sweep
- radar_full.py: Complete feature set
Also includes:
- Hardware Smart-Strip module specs (SBX-STR-01)
- Host configuration for USB OTG network
- Systemd service for USB auto-mode
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix glob pattern in bash test (glob in [[ -f ]] doesn't work correctly)
- Use find command instead of glob for reliable kernel detection
- Add fallback to extract ARM64 kernel from live USB image if not in rootfs
- Add verification step in GitHub Actions to check boot files after build
- Sort kernel files by version to get latest when multiple exist
Fixes missing vmlinuz/Image on multiboot USB issue.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>