Anchor the pattern that secubox-lyrion v1.1.0 demonstrated (admin static page on the canonical hub vhost + real app served at the root of its own dedicated vhost) as a REQUIRED rule across the docs: - docs/MODULE-GUIDELINES.md §4 — new "REQUIRED: dual-vhost pattern" subsection with the URL table, the JS pattern for reading the public URL from /access, and the explicit forbidden anti-pattern (`proxy_pass /<module>/ → app LXC`). - docs/MODULE-GUIDELINES.md §5 — nginx template updated: `/<module>/` is `alias` (static), the dedicated vhost is a separate file. The previous "Iframe pattern for LXC web UIs" + "LXC daemon iframe target" sections are flagged deprecated. - .claude/PATTERNS.md Pattern 12 — same rule reproduced for the Claude-facing pattern catalog with the source-of-truth JS snippet. - .claude/HISTORY.md + .claude/WIP.md — entry for today's three- module alignment (lyrion 1.1.0 + zigbee + authelia). - wiki/Architecture.md — short reference + pointer back to MODULE-GUIDELINES. - packages/secubox-lyrion/README.md — adds the URLs table at the top. - packages/secubox-zigbee/README.md — was a verbatim copy of the lyrion README, rewritten as a proper zigbee README with the new URLs table + API surface + ports + files. - packages/secubox-authelia/README.md — Quickstart points at sso.gk2.secubox.in (was auth.maegia.tv); URLs table added. The forbidden anti-pattern catches LMS Material, Nextcloud, Grafana, z2m and any future module whose UI uses absolute asset URLs — incident on gk2 2026-05-24, see secubox-lyrion 1.1.0 changelog.
26 KiB
SecuBox Module Guidelines
Canonical reference for authoring a new
secubox-<module>Debian package. Read alongsidedocs/grammar.md(CTL grammar) anddocs/UI-GUIDE.md(theme + sidebar conventions).The HOWTO that walks through adding a new CTL verb is at
HOWTO-grammar.md. This document covers the module-level scaffolding around a verb (packaging, LXC, FastAPI host control plane, web UI, nginx wiring).
1. When to create a new module
A new secubox-<module> package is justified when all of these hold:
- It introduces a new noun in the CTL grammar (not a verb on an existing noun).
- It owns at least one of: a long-running daemon, a web UI route under
/<module>/, a public-facing endpoint under/api/v1/<module>/, or a Debian-distributable bundle of templates/config. - It is independently uninstallable (
apt remove secubox-<module>must leave the rest of SecuBox functional).
Examples:
| Add a module | Don't add a module |
|---|---|
| Grafana dashboards backed by a Grafana LXC | A new dashboard JSON for the existing secubox-metrics module |
| YaCy peer search engine | A YaCy crawl-result viewer that lives in secubox-hub |
| RustDesk relay | A wireguard add peer rustdesk-relay verb on the existing VPN module |
2. Module shape (file structure)
The canonical layout. Every directory below is optional except debian/, but most modules grow into the full shape.
packages/secubox-<module>/
├── api/
│ ├── __init__.py
│ ├── main.py # FastAPI on /run/secubox/<module>.sock
│ ├── models.py # Pydantic schemas
│ └── lxc_helpers.py # LXC modules only
├── conf/
│ └── <module>.toml.example # installed to /etc/secubox/
├── debian/
│ ├── changelog
│ ├── control # see §6
│ ├── rules # see §6
│ ├── install # or use rules override_dh_auto_install
│ ├── postinst # idempotent — see §6
│ ├── prerm
│ ├── postrm
│ └── secubox-<module>.service # systemd unit — see §6
├── lib/
│ └── <module>/ # LXC modules only — see §3
│ ├── install-lxc.sh
│ ├── update-lxc.sh
│ └── provision/
├── menu.d/
│ └── 50-<module>.json # SecuBox sidebar entry — see §4
├── nginx/
│ └── <module>.conf # /etc/nginx/secubox.d/ snippet — see §5
├── README.md # operator-facing
├── sbin/
│ └── <module>ctl # bash CLI — see §7 + grammar.md
├── tests/
│ ├── helpers.bash # bats helpers
│ └── test-*.bats # CLI tests
└── www/
├── <module>/
│ ├── index.html # SecuBox-themed landing — see UI-GUIDE.md
│ ├── settings.html
│ └── logs.html
└── shared/ # rarely needed — most JS/CSS lives in secubox-hub
Mirror an existing module of similar shape rather than starting from scratch:
| Pattern | Reference module |
|---|---|
| LXC-hosted daemon | packages/secubox-gitea/, packages/secubox-mail/ |
| Host-only daemon | packages/secubox-metrics/, packages/secubox-crowdsec/ |
| WAF / proxy add-on | packages/secubox-mitmproxy/ |
| Pure web UI on existing service | packages/secubox-soc-web/ |
| Pure CLI / no daemon | packages/secubox-droplet/ |
3. LXC layout (modules that run their daemon in a Debian LXC)
When LXC is the right choice
- Daemon needs a different runtime (JVM, Alpine, custom apt repo) than the host.
- Daemon is high-risk and should run in an isolated rootfs (RustDesk, mitmproxy).
- Daemon needs root-level setup that would conflict with another module's daemon if run on the host.
IP allocation
The SecuBox LXC bridge is br-lxc on 10.100.0.0/24. Allocations:
| IP | Module |
|---|---|
.1 |
bridge gateway (host side) |
.10 |
|
.11/.12 |
mail-related (rspamd, sieve) |
.30 |
(reserved) |
.40 |
gitea |
.70 |
grafana (planned, #229) |
.80 |
yacy (planned, #230) |
.90 |
rustdesk (planned, #231) |
Pick the next free .X above the current high-water mark. Reserve the IP in
docs/grammar.md or this doc on submission of the module PR.
Bootstrap script: lib/<module>/install-lxc.sh
Idempotent shell script that creates the LXC if absent, debootstraps it,
installs the daemon, provisions defaults, and exits 0. Must be safely
re-runnable (the operator may run it from <module>ctl install repeatedly).
Anatomy:
#!/usr/bin/env bash
set -euo pipefail
readonly LXC_NAME="${SECUBOX_LXC_NAME:-<module>}"
readonly LXC_IP="${SECUBOX_LXC_IP:-10.100.0.XX}"
readonly LXC_PATH="${SECUBOX_LXC_PATH:-/data/lxc}"
readonly DEBIAN_SUITE="bookworm"
# 1. Idempotency short-circuit
lxc-info -n "$LXC_NAME" -P "$LXC_PATH" 2>/dev/null | grep -q '^State: *RUNNING' && exit 0
# 2. Create if missing
[ -d "$LXC_PATH/$LXC_NAME" ] || lxc-create -n "$LXC_NAME" -t debian -P "$LXC_PATH" -- -r "$DEBIAN_SUITE"
# 3. Pin static IP, AppArmor profile, autostart
cat > "$LXC_PATH/$LXC_NAME/config" <<EOF
lxc.uts.name = $LXC_NAME
lxc.net.0.type = veth
lxc.net.0.link = br-lxc
lxc.net.0.flags = up
lxc.net.0.ipv4.address = $LXC_IP/24
lxc.net.0.ipv4.gateway = 10.100.0.1
lxc.rootfs.path = dir:$LXC_PATH/$LXC_NAME/rootfs
lxc.include = /usr/share/lxc/config/common.conf
lxc.apparmor.profile = generated
lxc.start.auto = 1
EOF
# 4. Start + wait for network
lxc-start -n "$LXC_NAME" -P "$LXC_PATH"
for i in $(seq 1 30); do
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- ping -c1 -W1 10.100.0.1 >/dev/null 2>&1 && break
sleep 1
done
# 5. Install daemon + dependencies
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- bash -c '
apt-get update -q
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends <packages>
'
# 6. Provision (dashboards / peer lists / keys)
"$(dirname "$0")/provision/run.sh"
# 7. Sentinel
touch /var/lib/secubox/<module>/.lxc-provisioned
Provisioning (lib/<module>/provision/)
Ship configuration that the operator expects out-of-the-box:
- Grafana dashboards (
.json), datasource yaml. - YaCy peer seed list, blacklist, default crawl profile.
- RustDesk key material is generated, not shipped — but a
key generatewrapper goes here.
Provisioned content is read-only after install; user customisation lives in
/etc/secubox/<module>.toml and overrides the provisioned defaults.
Not in the image — installed on demand
Modules whose LXC rootfs is >100 MB should not be pre-built into the
SecuBox system image. The package ships only the install script; the
operator runs <module>ctl install post-firstboot.
This keeps the v2.10.x image at ~8 GB. Modules requiring large daemons (grafana, yacy, rustdesk) follow this rule. Compact LXCs (mitmproxy ~50 MB) can pre-build during image construction.
4. WebUI conventions
Theme
Mandatory. See docs/UI-GUIDE.md:
- CRT P31 phosphor palette via
/shared/crt-{light,dark}.css - Sidebar via
/shared/sidebar-{light,dark}.csspopulated by/shared/sidebar.js - Theme toggle persists to
localStorage.sbx_theme
Auth wrapper
Every page (except /login.html) must redirect to /login.html?redirect=<current-path>
when no localStorage.sbx_token is present. The hub's index.html shows the
canonical checkAuth() implementation:
function checkAuth() {
if (!localStorage.getItem('sbx_token')) {
window.location.href = '/login.html?redirect=' + encodeURIComponent(window.location.pathname);
return false;
}
return true;
}
if (!checkAuth()) {
document.body.innerHTML = '<div style="padding:2rem;color:var(--root-main);">Redirecting to login...</div>';
}
Path is /login.html, never /portal/login.html — see #222.
Menu integration (menu.d/<module>.json)
{
"title": "Module Name",
"subtitle": "One-line purpose",
"icon": "fa-icon-name",
"url": "/<module>/",
"section": "monitoring|security|network|publishing|hosting|search|admin",
"order": 50,
"module": "<module>"
}
section slots into the existing sidebar grouping. order controls
position inside the section (10/20/30/.../90).
Three-fold landing
The module's index.html should surface the same three sections the CTL
exposes — components / status / access — at the top of the page:
<section class="threefold">
<article id="components">...</article> <!-- LXC state, daemon state, host API state -->
<article id="status">...</article> <!-- overall green/yellow/red + the last event -->
<article id="access">...</article> <!-- public hostname + auth method -->
</section>
Pulls live data from /api/v1/<module>/components, /status, /access.
REQUIRED: dual-vhost pattern for modules with a real web UI
When the module exposes a real web UI (LMS Material, z2m frontend, Authelia portal, Nextcloud, …), the two surfaces MUST be kept on separate hostnames:
| URL | Role |
|---|---|
https://admin.gk2.secubox.in/<module>/ |
SecuBox admin — static page calling /api/v1/<module>/*. NEVER a proxy to the app. |
https://<module>.gk2.secubox.in/ |
Real app web UI at the vhost root. Authelia-gated. |
Why this is mandatory:
Most apps hardcode absolute asset paths (LMS Material loads
/material/customcss/, /cometd/handshake, /css/, /js/;
Nextcloud loads /apps/, /core/; Grafana loads /public/). Reverse-
proxying them under a /<module>/ subpath drops the prefix on every
asset request and the browser sees text/html 404s where it expects
CSS/JS. The dedicated vhost serves the app at its root, so absolute
URLs resolve correctly.
The SecuBox admin under /<module>/ is a separate, static page
under SecuBox theming. Its job is to surface status, control actions
(restart, rescan, …) and the Open <App> UI → button that points to
the dedicated vhost.
Source-of-truth rule — the Open button MUST read its URL from
/api/v1/<module>/access at runtime; never hardcode the public
hostname in the static HTML. The /access endpoint is the only
place the hostname appears (see §6 for the access-emit pattern in
<module>ctl).
<!-- index.html — the "Open <App> UI" button -->
<a class="btn primary" id="open-app" href="#" target="_blank" rel="noopener">
Open <App> UI →
</a>
// JS — read /access and patch the button href on load
fetch('/api/v1/<module>/access').then(r => r.json()).then(d => {
const access = d.access || [];
const pub = access.find(a => a.scope === 'public');
const lan = access.find(a => a.scope === 'lan');
const target = (pub && pub.url) || (lan && lan.url) || '#';
const btn = document.getElementById('open-app');
if (btn) btn.href = target;
});
Forbidden anti-pattern: a single nginx proxy_pass from
/<module>/ to the LXC daemon. That always silently breaks an absolute
asset path somewhere, sometimes only after a Material/skin/plugin
upgrade. We hit this in #244 with LMS (incident 2026-05-24, see
secubox-lyrion 1.1.0 changelog).
5. nginx wiring (nginx/<module>.conf)
Installed to /etc/nginx/secubox.d/<module>.conf and auto-included by
/etc/nginx/sites-available/secubox.
Standard pattern (host FastAPI only)
# /etc/nginx/secubox.d/<module>.conf
location /api/v1/<module>/ {
proxy_pass http://unix:/run/secubox/<module>.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf;
}
Module with a real web UI — REQUIRED dual-vhost split
/<module>/ on the canonical hub vhost serves the static SecuBox
admin (via alias, not proxy_pass). The real app lives at its own
vhost (<module>.gk2.secubox.in) which serves at the root path so
absolute asset URLs resolve correctly.
# /etc/nginx/secubox.d/<module>.conf — on the canonical hub vhost
location /api/v1/<module>/ {
auth_request /__sbx_auth_verify;
error_page 401 = @sbx_auth_login;
rewrite ^/api/v1/<module>/(.*)$ /$1 break;
proxy_pass http://unix:/run/secubox/<module>.sock;
include /etc/nginx/snippets/secubox-proxy.conf;
}
# SecuBox admin static page. NOT a proxy to the app.
location /<module>/ {
auth_request /__sbx_auth_verify;
error_page 401 = @sbx_auth_login;
alias /usr/share/secubox/www/<module>/;
index index.html;
try_files $uri $uri/ /<module>/index.html;
}
The dedicated vhost lives in /etc/nginx/sites-available/<module>.conf
(template in packages/secubox-<module>/nginx/<module>-vhost.conf),
listens on 0.0.0.0:9080 for server_name <module>.gk2.secubox.in,
gates with auth_request and proxy_passes to the LXC daemon at root.
Deprecated: iframe pattern / proxy_pass /<module>/
Both patterns are now forbidden for modules with a real web UI. They silently break absolute asset URLs (see §4 REQUIRED). Existing modules still on those patterns are bugs to be migrated.
Non-HTTP daemons (rustdesk, raw TCP/UDP)
Don't proxy through nginx — proxy through HAProxy stream mode. See
packages/secubox-haproxy/conf/ for the stream-backend pattern.
6. Debian packaging conventions
debian/control
Source: secubox-<module>
Section: admin
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13), dh-python, python3-all
Standards-Version: 4.6.2
Homepage: https://cybermind.fr/secubox
Package: secubox-<module>
Architecture: all
Depends: ${misc:Depends},
secubox-core (>= 1.0),
<module-specific deps>
Recommends: <optional companions>
Description: SecuBox <Module> — <one-line purpose>
<Longer description, 3-5 lines.>
Do not ship a debian/compat file. The Build-Depends: debhelper-compat (= 13) is the single source of truth (see #220).
debian/rules
#!/usr/bin/make -f
%:
dh $@ --with python3
override_dh_auto_install:
install -d debian/secubox-<module>/usr/lib/secubox/<module>
cp -r api debian/secubox-<module>/usr/lib/secubox/<module>/
install -d debian/secubox-<module>/usr/sbin
install -m 755 sbin/<module>ctl debian/secubox-<module>/usr/sbin/<module>ctl
install -d debian/secubox-<module>/usr/share/secubox/www
[ -d www ] && cp -r www/. debian/secubox-<module>/usr/share/secubox/www/ || true
install -d debian/secubox-<module>/usr/share/secubox/menu.d
[ -d menu.d ] && cp -r menu.d/. debian/secubox-<module>/usr/share/secubox/menu.d/ || true
install -d debian/secubox-<module>/etc/nginx/secubox.d
[ -f nginx/<module>.conf ] && cp nginx/<module>.conf debian/secubox-<module>/etc/nginx/secubox.d/ || true
# LXC modules only:
install -d debian/secubox-<module>/usr/share/secubox/lib/<module>
[ -d lib/<module> ] && cp -r lib/<module>/. debian/secubox-<module>/usr/share/secubox/lib/<module>/ || true
debian/postinst
Idempotent system-user creation, directory provisioning, conditional
nginx reload, service enable. Never start the FastAPI before its
prerequisites are present — for LXC modules, defer the first start
to <module>ctl install (which fires after the LXC is up).
#!/bin/sh
set -e
case "$1" in
configure)
getent group secubox >/dev/null || groupadd --system secubox
getent passwd secubox >/dev/null || useradd --system --gid secubox \
--home /var/lib/secubox --no-create-home --shell /usr/sbin/nologin secubox
install -d -m 0770 -o root -g secubox /etc/secubox
install -d -m 0755 -o secubox -g secubox /var/lib/secubox/<module>
if [ ! -f /etc/secubox/<module>.toml ] && [ -f /etc/secubox/<module>.toml.example ]; then
cp /etc/secubox/<module>.toml.example /etc/secubox/<module>.toml
chmod 640 /etc/secubox/<module>.toml
chown root:secubox /etc/secubox/<module>.toml
fi
if systemctl is-active --quiet nginx 2>/dev/null; then
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
fi
systemctl daemon-reload 2>/dev/null || true
systemctl enable secubox-<module>.service 2>/dev/null || true
# LXC modules: do not start yet; ctl install will start after LXC up.
# Host-only modules: uncomment the next line:
# systemctl start secubox-<module>.service 2>/dev/null || true
;;
esac
#DEBHELPER#
exit 0
debian/secubox-<module>.service
[Unit]
Description=SecuBox <Module> API
After=network.target secubox-core.service
Wants=secubox-core.service
[Service]
UMask=0007
Type=simple
User=secubox
Group=secubox
WorkingDirectory=/usr/lib/secubox/<module>
RuntimeDirectory=secubox
RuntimeDirectoryMode=0755
RuntimeDirectoryPreserve=yes
ExecStartPre=/bin/mkdir -p /etc/secubox
ExecStartPre=/bin/chown secubox:secubox /etc/secubox
ExecStart=/usr/bin/python3 -m uvicorn api.main:app --uds /run/secubox/<module>.sock --log-level warning
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
LogsDirectory=secubox
LogsDirectoryMode=0755
ReadWritePaths=/run/secubox /var/lib/secubox /etc/secubox /var/log/secubox
[Install]
WantedBy=multi-user.target
Board-specific units (Pi/MOCHAbin I2C/LED) must gate on ConditionArchitecture=arm64 to skip silently on x86_64 (see #226).
Slipstream into the system image
The build-packages.yml workflow auto-discovers any new packages/secubox-*/debian/control on the next push. No workflow edit is required.
The build-image.sh slipstream loop (cp /tmp/secubox-debs/secubox-*.deb) picks up the new _all.deb automatically. New module appears in dpkg -l 'secubox-*' on the next image build.
apt-get install -f -y after the slipstream dpkg -i --force-depends step (since #218) pulls any new declared Debian deps — no need to add them to the debootstrap INCLUDE_PKGS.
7. CTL conventions
Read first: docs/grammar.md (canonical verbs table) +
HOWTO-grammar.md (recipe for adding a verb).
Mandatory three-fold
Every <module>ctl exposes:
| Noun | Verb | Output |
|---|---|---|
components |
(no verb) list status |
one line per managed component (LXC, daemon, host-API, sub-units) |
status |
(no verb) | overall green/yellow/red + last event line |
access |
list show <name> |
every URL/socket the module exposes + the auth method |
Plain stdout for humans, --json for machines. Schema:
{
"module": "<module>",
"version": "<semver>",
"components": [
{"name": "lxc", "state": "running|stopped|absent", "detail": "..."},
{"name": "daemon", "state": "running|stopped|absent", "detail": "..."},
{"name": "host-api", "state": "running|stopped|absent", "detail": "..."}
]
}
Mandatory lifecycle verbs
In addition to the three-fold introspection, every <module>ctl MUST expose
the following lifecycle verbs. They form the operator contract every module
of SecuBox honours so the hub can drive provisioning, healing, and onboarding
through the same surface.
| Verb | Behaviour | Idempotent? | Required? |
|---|---|---|---|
install |
Provisions everything the module needs (LXC, daemon, secrets, provisioning, systemd enable). Re-running is a no-op when already provisioned, or repairs in-place when partially present. | yes | yes |
repair |
Detects + fixes drift against the expected end-state. Examples: restart stopped daemon, re-issue missing secret, re-write missing config, restore broken nginx route, re-run failed provisioning step. Reports each detected issue + the action taken (one line each), exits 0 if everything ended green, exits 2 if any unfixable issue remains. | yes | yes |
wizard |
Interactive first-time-setup. Walks the operator through credentials, exposure choices, optional integrations. Writes /etc/secubox/<module>.toml then chains install + status. May be invoked from the WebUI as a /api/v1/<module>/wizard/* flow. |
should be (re-runnable to reconfigure) | yes — even if it's a thin wrapper that calls install with seed prompts |
reload |
Restart host FastAPI + the in-LXC daemon. Cheap, no provisioning. | yes | yes |
uninstall |
Remove the LXC + state + secrets. Asks for confirmation unless --yes. Does not remove the Debian package itself. |
yes | yes |
Implementation pattern:
cmd_install() { bash "$INSTALL_LIB" "$@" ; systemctl start "secubox-${MODULE}.service" ; }
cmd_repair() {
local fixed=0 broken=0
if ! daemon_running; then
log "repair: daemon stopped — restarting"
lxc-attach -n "$LXC_NAME" -P "$LXC_PATH" -- systemctl restart "$DAEMON_UNIT" && ((fixed++)) || ((broken++))
fi
if ! host_api_running; then
log "repair: host-api stopped — restarting"
systemctl restart "secubox-${MODULE}.service" && ((fixed++)) || ((broken++))
fi
# ...module-specific repairs: missing secrets, broken nginx, drifted resolv...
[ "$broken" -eq 0 ]
}
cmd_wizard() { prompt_then_write_config && cmd_install ; }
cmd_reload() { systemctl restart "secubox-${MODULE}.service" ; lxc-attach … restart … ; }
cmd_uninstall() { confirm "$@" && lxc-destroy … && rm -rf /var/lib/secubox/${MODULE} … ; }
Module-specific noun-verb pairs
Per the grammar table — one verb closes one previously-implicit operation beyond the mandatory lifecycle above. Examples in flight:
grafanactl dashboard list/add/remove/export # OPS MONITORING (#230)
yacyctl index status/build/clear/optimize # SEARCH (#232)
rustdeskctl peer add/remove/list # REMOTE-ACCESS (#234)
Style
- Imperative present tense:
add,remove,list,status. Notcreating,deletion,listed. - Singular nouns:
route,repo,app,peer,dashboard,vhost. - Aliases accepted but not advertised in
--help:rm/delforremove,lsforlist. - Exit codes:
0success,1user error,2runtime error,3lxc/daemon down.
Help
<module>ctl --help prints the noun-verb table; <module>ctl <noun> prints
the verbs for that noun; <module>ctl <noun> <verb> --help prints flags.
8. API conventions
Transport
FastAPI + Uvicorn on a Unix socket at /run/secubox/<module>.sock. Never a TCP port (avoids LAN-side enumeration).
Routing
nginx proxies /api/v1/<module>/* → the Unix socket. The FastAPI app
mounts its routes at the root (no /api/v1/<module> prefix in the app
code — nginx strips it).
Mandatory endpoints
| Endpoint | Purpose |
|---|---|
GET /status |
Same JSON as <module>ctl status --json |
GET /components |
Same JSON as <module>ctl components --json |
GET /access |
Same JSON as <module>ctl access --json |
GET /healthz |
{"ok": true} if the API process is alive — no shelling out |
GET /version |
{"version": "<semver>", "build": "<git-sha>"} |
These five let healthctl watch the module without module-specific code.
Module-specific endpoints
One endpoint per noun-verb pair the CTL exposes. The API can either
re-implement the logic in Python (fast) or shell out to <module>ctl <noun> <verb> --json
(simple, single source of truth). The latter is the SecuBox default.
Auth
All non-trivial endpoints (anything mutating state, or returning data
beyond status/components/access/healthz/version) require JWT via
Depends(auth.require_jwt). The JWT secret is generated at firstboot
into /etc/secubox/secubox.conf.
Pydantic models
Define in api/models.py. Re-use shared models from secubox-core when
they fit (secubox_core.models.JobStatus, etc.).
9. Tests
CTL tests
Use bats-core. One file per noun,
or one file per coherent flow (test-<module>ctl-install.bats,
test-<module>ctl-dashboards.bats).
PATH-shim external commands (lxc-create, lxc-attach, systemctl,
curl) in tests/helpers.bash so the suite runs offline:
# tests/helpers.bash
setup() {
export PATH="$BATS_TEST_DIRNAME/stubs:$PATH"
# Each stub is a shell script that echoes a canned response.
}
API tests
pytest + httpx async client. Spin up uvicorn on a tempfile socket per test session; assert JSON schema.
CI
Both run automatically as part of Build SecuBox .deb packages when
debian/rules includes them via dh_auto_test or debian/tests/.
10. Validation checklist (run before opening the PR)
- Package builds locally:
cd packages/secubox-<module> && dpkg-buildpackage -us -uc -bsucceeds with no warnings other than the standarddpkg-source: warning: source formatline. lintian --no-tag-display-limit ../secubox-<module>_*.debreports zeroE:and zeroW:(info lines OK).<module>ctl --helplists every noun.<module>ctl <noun> --helplists every verb on that noun.- Three-fold JSON schemas match §7 and §8.
bats tests/runs green offline (no internet, no LXC).- WebUI page loads with
localStorage.sbx_tokenset; redirects to/login.htmlwhen not. - nginx vhost includes both
/api/v1/<module>/and/<module>/(if iframe target). - No
debian/compatfile (compat 13 declared viaBuild-Dependsonly). - No Pi-only services without
ConditionArchitecture=arm64(see #226). - No reference to
/portal/login.html(use/login.html— see #222). apt-get install -y ../secubox-<module>_*.debon a fresh VM completes;systemctl status secubox-<module>isloaded(host modules: alsoactive); LXC modules:<module>ctl installthen<module>ctl statusreturns green.- Module added to
docs/MODULES.mdcatalog. - CTL verbs added to
docs/grammar.mdcanonical table. .claude/MIGRATION-MAP.mdrow added.
References
docs/grammar.md— canonical CTL grammarHOWTO-grammar.md— recipe for adding a verbdocs/UI-GUIDE.md— UI theme and sidebardocs/MODULES.md— module catalogdocs/SECUBOX-DEV-METHODOLOGY.md— overall development workflow.claude/PATTERNS.md— RPCD → FastAPI porting patterns- Plan example:
docs/superpowers/plans/2026-05-20-grafana-yacy-rustdesk-lxc-modules.md— three modules following this guideline end-to-end.