haproxyctl: honour an opt-in `waf_bypass = true` field per vhost so the
generator emits `use_backend <backend>` (nginx_vhosts) instead of forcing
mitmproxy_inspector under waf_enabled. Makes the WAF-bypass of media/large-
upload services (nc/photoprism/matrix/torrent) declarative, auditable and
regen-durable instead of fragile hand-edits of the generated haproxy.cfg.
Normal vhosts are unaffected (verified in isolation).
secubox-torrent postinst: register torrent.<host>.<suffix> in haproxy.toml
(backend=nginx_vhosts, waf_bypass=true, ssl=true) idempotently and run a
drift-guarded `haproxyctl generate` — which refuses safely on a board that
still has un-migrated hand-edit vhosts (live cfg untouched) and succeeds on a
fresh board. Torrent bypasses sbxwaf by design, so no sbxwaf route is needed.
CLAUDE.md: WAF-bypass section updated — default stays 'never bypass'; the ONLY
sanctioned exception is the declarative waf_bypass=true in haproxy.toml, never
a hand-edit of the generated cfg.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Backports the live gk2 WAN link guard into source as a proper package
(mirrors the secubox-frigate-diskguard skeleton):
- sbin/secubox-wan-link-guard: mvpp2 eth2 pause-off (ethtool -A) + renegotiate
(ethtool -r) + lan0 parasite route/addr cleanup + force default via eth2.
NEVER ip link down/up (wedges the comphy). Tunable via /etc/default.
- secubox-wan-link-guard.service + boot/30s .timer
- 10-secubox-eth2-noflowctl.link: flow-control off at eth2 appearance (boot)
- secubox-eth2-flowctl-off.service backstop (installed, left disabled; .link
supersedes it, matching live)
- postinst migrates the pre-dpkg hand-placed copies out of /etc/systemd +
/usr/local/sbin so the packaged units win, then enables the timer.
netplan (board/mochabin): add static fallback 192.168.1.200/24 on eth2 at
route-metric 200 (higher than the DHCP route's 100) so the box is always
reachable even without a lease — closes#913 task 1. Source-only; not applied
live (avoids a remote WAN blip on the already-marginal link).
Verified on gk2: install migrated the old units, timer enabled+active, guard
ran clean (Result=success), WAN gateway stayed reachable throughout.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
WebTorrent 2.x made client.get() async (returns a Promise). engine.get() used it
as if synchronous, so every caller operated on a Promise instead of the Torrent:
remove → promise.destroy() (500), /files → promise.files (500), stats →
promise.progress (garbage), and /stream never resolved its file. Match on the
synchronous client.torrents array instead. FakeWebTorrent.get() was sync and
masked this — made it async to model reality, added regression tests asserting
engine.get() returns a Torrent (not a Promise) and remove() drops it cleanly.
Verified on gk2: add/files/remove all 200 (files+remove were 500), service
restarts:0.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
uint8-util 2.3.0 rewrote arr2hex() to require a TypedArray (reads data.buffer/
byteOffset/byteLength). webtorrent 2.8.5's _onTorrentId passes it parse-torrent's
infoHash, which is a hex STRING (bytes live in infoHashBuffer) — so arr2hex threw
'Received undefined', crashing the add in a microtask before the torrent resolved
its infoHash. The request then blocked past the proxy read timeout and surfaced as
a board-wide 502 on /add. webtorrent declares uint8-util ^2.2.5, so the committed
lockfile had silently drifted up to the breaking 2.3.x. Pin to 2.2.6 (last pre-2.3,
string-tolerant arr2hex) via npm overrides; regenerate lockfile.
Verified on gk2: add returns 200 in ~0.1s (was a >15s hang), torrent lands in the
library, service stays up with no unhandledRejection.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
WebTorrent throws SYNCHRONOUSLY (arr2hex(undefined) in _onTorrentId) on some
malformed magnets — not an 'error' event, so engine listeners can't catch it,
crashing the process (user sees 502). Add process.on('uncaughtException') in
start() (kept alive), and store a canonical infohash-derived magnet when
magnetURI is absent so resumeLibrary never re-adds a bad value.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
library.js add() used INSERT OR REPLACE, so re-adding an already-kept
magnet reset kept=0 and added_at — the ephemeral purge sweep would then
delete it 6h later as if it had never been kept. Switch to
INSERT ... ON CONFLICT DO UPDATE SET last_played_at only, preserving kept/
added_at/name/magnet/path across a re-add.
server.js never resynced the WebTorrent engine with the library DB at
startup: kept titles 404'd on /stream after any restart (engine.get()
returns null for a torrent the engine never re-learned about), and
runPurge's engine.remove() became a silent no-op for ephemeral rows,
leaking their downloaded bytes on disk while their DB rows vanished. Add
resumeLibrary(engine, library, { rmrf }), wired into start(): kept rows are
re-added to the engine (try/catch + promise .catch so one bad magnet can't
abort the loop); ephemeral rows are dropped from the library and their
download dir is reclaimed via an injectable rmrf.
api.js recorded a fictitious tmp/<infohash> (add) and library/<infohash>
(keep) path that no code ever moves files into — files download flat to
downloadDir. Record path.join(downloadDir, infohash) in both places so the
path resumeLibrary's rmrf acts on matches where files actually live.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
postinst invoked the bash provisioner through /bin/sh (dash), which rejects
`set -o pipefail` on line 1 and silently aborted LXC provisioning behind the
`|| echo … deferred …` guard — apt install exited 0 with no LXC ever
created. Also add secubox-hub to Depends (owns the $lan_client nginx map
torrent.conf's auth gate references) and python3/build-essential to the
in-LXC apt install so npm can compile better-sqlite3/@roamhq/wrtc from
source on bookworm's Node 18 when an arm64 prebuild is missing.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Pivots the secubox-torrent Debian package to the WebTorrent/LXC-native
design (Tasks 1-7): control/rules/postinst/postrm now ship the Node app
to /usr/lib/secubox/torrent/app, run install-lxc.sh at configure, load
the nft egress scope from /etc/nftables.d/ (not the brief's unused
/etc/secubox/nft.d/), and symlink the standalone vhost into
sites-available/sites-enabled instead of the location-snippet secubox.d/.
Deletes the old host-side Transmission FastAPI (api/main.py) and its
systemd unit; Depends drops python3-uvicorn/transmission in favour of
lxc, debootstrap, nftables. Structural packaging test added
(tests/test_packaging.py). Builds cleanly to
secubox-torrent_2.0.0-1~bookworm1_all.deb.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Extends the Task 1 install-lxc.sh skeleton with the full app deploy
(tar-copy app/+www/ into the LXC, npm ci, torrent.env, in-LXC systemd
unit enable+start), plus the standalone torrent.gk2.secubox.in nginx
vhost, an nft egress visibility tap on the LXC veth, conf/torrent.toml,
and the menu.d entry (icon/category/description).
Picks 10.100.0.160 for the LXC (10.100.0.130 was already taken), pins
a predictable veth (veth-torrent0) so the nft rule can target it, and
bind-mounts /data/torrent from the host so downloaded data and the
disk-floor statfs() check in server.js see real storage instead of
LXC-overlay space.
The nginx vhost is a full standalone server{} block (own server_name),
so it targets sites-available/ (not secubox.d/, which is merged into
the shared hub server block and can't hold a nested server{}) -- Task 8
must update debian/rules accordingly. Its auth gate mirrors the current
LAN-only $lan_client stub (Authelia was decommissioned; there is no
aggregator JWT-verify endpoint to proxy to yet), not the brief's
aggregator/api/v1/auth/verify snippet.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Adds server.js (env-driven Engine/Library/buildApi wiring, ephemeral
torrent purge sweep on a timer, static webui serving) and the runPurge
unit tests (TTL + kept-flag + disk-floor sweep, in-memory Library only).
webtorrent and @fastify/static are imported dynamically inside start()
so runPurge stays testable without the native wrtc build.
Replaces the old Transmission-container panel with a single-page
WebTorrent player webui (magnet add, list with keep/remove, in-browser
playback via /stream, BitTorrent-only status badge) per the cyan
hybrid-dark self-contained module vhost guidelines.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Adds SQLite-backed torrent library (add/list/get/touch/keep/remove) and
the expiredEphemeral() query the purge job uses to find stale
kept=0 torrents (last_played_at < now-ttl). Includes package-lock.json
and a lxc/app .gitignore for node_modules.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Critical: Change file.stream({start,end}) → file.createReadStream({start,end})
to match real webtorrent ^2.5.1 API (WHATWG ReadableStream vs Node Readable).
Important #1: RFC 7233 compliance — clamp end >= total to total-1 instead
of rejecting with 416. Only reject when start >= total or start > end.
Important #2: Attach stream error handlers to prevent unhandled crashes
on peer disconnect or missing pieces.
Minor: Add tests for range clamping (bytes=50-999999 on 100-byte file)
and out-of-range fileIdx. All 6 tests passing (green).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Implement handleStream(engine, req, res) for WebTorrent streaming with
HTTP Range support. Handles 200 full, 206 partial, 416 unsatisfiable,
404 not found responses. MIME type detection from file extension.
Tests via node --test with fake engine/request/response (all green).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
FakeTorrent now stores client reference and splices itself from torrents[]
on destroy(), matching real webtorrent semantics. New test verifies
remove→add within maxActive limit succeeds (capacity freed).
Also remove unused 'import path' from engine.js.
4/4 tests passing.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
TDD complete: 3/3 tests passing. Engine constructor stores webrtc flag for
API /status route to report transport availability. Fakes enable unit tests
without real WebTorrent network or WebRTC dependencies.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Node app skeleton (package.json, wrtc-probe.js) and idempotent
install-lxc.sh skeleton for the secubox-torrent v2.0 WebTorrent
streaming pivot.
Validated @roamhq/wrtc on arm64 via the existing peertube LXC on gk2
(Node v22.22.2, Debian 12 aarch64) since the dev workstation is x86-64
and the dedicated torrent LXC is not provisioned yet: npm install
resolved a linux-arm64 prebuild cleanly, and the shipped wrtc-probe.js
printed WEBRTC_AVAILABLE=true with a working RTCPeerConnection. Result
recorded in SPIKE-RESULT.md; @roamhq/wrtc stays in package.json.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
At ~150 execs/s a dpkg -S subprocess per event can't keep up; the scanner
fell permanently behind and never reached recent execs. DpkgIndex loads all
/var/lib/dpkg/info/*.list paths into a set once at startup. +2 tests.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
On bookworm merged-usr, audit reports /usr/bin/grep but dpkg owns /bin/grep,
so dpkg -S failed and legit system binaries were flagged non-dpkg -> false
positive flood in the scanner. Try both aliased forms. +2 regression tests.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
-i renders uid=root and decodes exe; parse_ausearch expects raw (uid=0,
hex/quoted exe) and matched ZERO events on interpreted output. Regression
guard test added. This was the last blocker: scanner now flags non-dpkg execs.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
-ts windowing was unworkable (epoch rejected, recent=30k events/poll); use
ausearch's native --checkpoint incremental mode + seed the checkpoint on first
start so pre-daemon history isn't replayed. Verified on gk2.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
On-hardware validation (gk2, aarch64) revealed the v1 exec-detection didn't
fire and had an unsafe blast radius. Fixes:
- audit: replace targeted `-w <dir> -p x` watches (which watch the dir inode,
NOT execs of files within it — 0 records on real execs) with a broad
`-a always,exit -F arch=b64 -S execve -k sbx_exec` rule. `-F dir=` does not
filter execve by target path either (both verified on the raw audit log).
~150 exec/s on a populated board, lost=0.
- api/policy.py (new): enforce flag (default FALSE = alert-only 'process
scanner') + jail_dirs scope. Broad detection would otherwise auto-jail
legitimate non-dpkg SecuBox binaries and cut their egress. Alert-only logs
+ alerts every flagged exec but never jails; enforce=true jails only flagged
execs under a jail_dir. Fail-safe: corrupt/missing config => alert-only.
- daemon: ExecLog now persists only FLAGGED (non-dpkg) execs, not the ~150/s
legitimate ones (would balloon to millions of rows/day, zero forensic value).
jail_fn receives the whole ExecEvent so policy can gate on the exe path.
- unit: SupplementaryGroups=adm so the daemon can actually read
/var/log/audit/audit.log (0640 root:adm) — without it the scanner is blind.
- ctl nft-load: mkdir the slice cgroup before `nft -f` (nft resolves the
cgroupv2 path at load time; missing cgroup => load fails).
- packaging: nginx route -> /etc/nginx/secubox-routes.d (the dir the hub's
webui.conf includes; secubox.d is inert on the admin vhost); postinst
reloads nginx so the panel goes live.
89 tests (was 80): +test_policy.py, +make_jail_fn alert-only/enforce tests.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Re-review caught that api/alertstore.py was a module-level Python list,
which is a no-op across the real deployment topology: sbx-antirootkitd
.service (the daemon, python3 -m api.daemon) and secubox-antirootkit
.service (the API, uvicorn api.main:app) are two SEPARATE systemd
processes. The daemon's alertstore.append() mutated a list private to
its own process; GET /alerts, served by the other process, always read
a different, permanently-empty copy. The previous tests only passed
because they ran in a single pytest process.
Rework alertstore.py into an AlertStore class backed by SQLite at
/var/lib/secubox/antirootkit/alerts.db (WAL, check_same_thread=False),
exactly mirroring how api/execlog.py already correctly shares
execlog.db between both processes. The daemon and the API each
construct their own AlertStore pointed at the same db_path.
Added test_alertstore.py::test_cross_process_visibility: two
independent AlertStore instances (independent sqlite3 connections)
opened against the same db_path, standing in for the daemon and the
API, proving one instance's append() is visible from the other — the
assertion the previous in-memory store could never make.
README/changelog "Alerts (LIVE)" wording updated to describe the
SQLite-backed, cross-process store instead of the incorrect
"in-process" store it previously claimed.
80 tests passing (was 78; +2 new: full alert round-trip + the
cross-process proof).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Whole-branch review fix wave on secubox-antirootkit:
- daemon.py: the ausearch checkpoint was reset to "recent" every loop,
re-fetching/re-parsing/re-jailing the same ~10-min window forever
(execlog rows duplicated ~120x, recycled PIDs falsely re-jailed). Now
tracks a real (ts, serial) cursor via ExecEvent's own audit-record
identity, so each event is processed exactly once regardless of how
much successive ausearch windows overlap.
- New api/alertstore.py wires GET /alerts to real jail events (was a
hardcoded [] stub): the daemon appends a minimal alert whenever it
jails an unknown process.
- Trim over-claims in control/changelog/README: LIVE (anti-escape, exec
scanner, execlog, webui, alert-only quarantine-prep) vs SCAFFOLDED
v1.1 follow-up (debsums/aide/rkhunter integrity timer not shipped).
- Remove dead [procwatch] mode key; add /home to the auditd watch rules
(heuristics.SUSPECT_DIRS already flags it, but nothing watched it).
- debian/control: add sudo + nftables to Depends (jail path + anti-escape
silently no-op without them).
- nft/secubox-antiescape.nft: idempotent add+delete+redefine teardown so
re-running nft-load never appends duplicate rules.
78 tests passing (was 66; +12 new covering the cursor and alert wiring).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Turns the existing api/, sbin/, nft/, systemd/, sudoers/, www/, conf/,
menu.d/ trees (Tasks 1-10) into an installable secubox-antirootkit .deb,
mirroring secubox-p2p's debian/ conventions.
- debian/control: arch all, Depends auditd+debsums+python3+python3-fastapi,
Recommends aide/rkhunter/chkrootkit (soft integrity-tool degradation).
- debian/rules installs the ctl to /usr/sbin, the nft table to
/usr/share/secubox-antirootkit/ (matches the ctl's nft-load path), the
sudoers drop-in, the audit rule, both systemd services + the
sbx-untrusted.slice, the webui, menu entry and nginx route.
- debian/postinst creates the secubox-antirootkit system user, installs
-d (never chowns) /var/lib/secubox/antirootkit under the shared
/var/lib/secubox parent, visudo -cf validates the sudoers before
trusting it, loads the nft table + augenrules, and starts
sbx-untrusted.slice at the cgroup root (unnested — required for the nft
`cgroupv2 level 1` match) plus both services.
- conf/99-sbx-procwatch.rules: targeted (not full) auditd exec watches on
/usr/local/bin, /usr/local/sbin, /tmp, /dev/shm, /opt, /usr/lib/jvm.
- api/daemon.py: thin runner wiring ausearch polling around the existing
execwatch.run_once()/cgroup.jail_pid()/ExecLog primitives for
sbx-antirootkitd.service — no new decision logic.
- tests/test_packaging.py: 13 tests reading debian/ as text (control
deps, no-chown-shared-parent, nft/audit wiring, DEBHELPER tokens,
compat level, service units, slice nesting).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
len(recent(limit=100)) caps the "Exec Rows" badge at 100 forever once a
live host records more than that — a monitoring blind spot. Add
ExecLog.count() (true SELECT COUNT(*) over the table, unbounded by any
recent() limit) and use it in the /status handler.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
api/main.py exposes GET /status, /execlog, /alerts and POST
/quarantine-prep (delegates to api.quarantine.prepare, never executes)
via a create_app(execlog=None) factory; module-level `app` uses a
default ExecLog singleton. nginx/antirootkit.conf routes /antirootkit/
static + /api/v1/antirootkit/ to the dedicated socket, and
menu.d/595-antirootkit.json registers the sidebar entry.
api/execlog.py gains an optional check_same_thread kwarg (default True,
backward compatible) so the API's singleton can be opened with
check_same_thread=False — required because FastAPI's sync routes run
in a threadpool worker thread, not the thread that opened the sqlite3
connection.
www/antirootkit/index.html follows the WEBUI-PANEL-GUIDELINES
hybrid-dark look, builds every row via createElement/textContent (no
innerHTML, no inline onclick) with event-delegated alert-queue
quarantine buttons, and reads the bearer token from
localStorage['sbx_token'].
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
nft_block and disable_unit interpolated c2_ip/unit raw, unlike path,
letting a crafted unit or c2_ip smuggle shell metacharacters into the
plan strings an operator copies/pastes and runs. Quote both like path.
Also strengthens the no-side-effects test to monkeypatch os.chmod and
shutil.copy alongside os.system/subprocess.run, and adds a test
asserting metacharacters in c2_ip/unit come out shell-quoted.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
api/quarantine.prepare() computes the neutralization plan for a
confirmed-malicious binary as data (chmod/copy/sha256/nft_block/
disable_unit strings) — it never executes anything, only builds
shlex-quoted command strings for operator confirmation. Adds the
matching quarantine-prep <path> verb to secubox-antirootkitctl,
which writes the plan to /root/quarantine/plan-<name>.txt and logs
it, without running any of the destructive steps itself.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>