# SecuBox Module Guidelines > Canonical reference for authoring a new `secubox-` Debian package. > Read alongside [`docs/grammar.md`](grammar.md) (CTL grammar) and > [`docs/UI-GUIDE.md`](UI-GUIDE.md) (theme + sidebar conventions). > > The HOWTO that walks through adding a new CTL verb is at > [`HOWTO-grammar.md`](../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-` 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 `//`, a public-facing endpoint under `/api/v1//`, or a Debian-distributable bundle of templates/config. - It is independently uninstallable (`apt remove secubox-` 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. ```text packages/secubox-/ ├── api/ │ ├── __init__.py │ ├── main.py # FastAPI on /run/secubox/.sock │ ├── models.py # Pydantic schemas │ └── lxc_helpers.py # LXC modules only ├── conf/ │ └── .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-.service # systemd unit — see §6 ├── lib/ │ └── / # LXC modules only — see §3 │ ├── install-lxc.sh │ ├── update-lxc.sh │ └── provision/ ├── menu.d/ │ └── 50-.json # SecuBox sidebar entry — see §4 ├── nginx/ │ └── .conf # /etc/nginx/secubox.d/ snippet — see §5 ├── README.md # operator-facing ├── sbin/ │ └── ctl # bash CLI — see §7 + grammar.md ├── tests/ │ ├── helpers.bash # bats helpers │ └── test-*.bats # CLI tests └── www/ ├── / │ ├── 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` | mail | | `.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//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 `ctl install` repeatedly). Anatomy: ```bash #!/usr/bin/env bash set -euo pipefail readonly LXC_NAME="${SECUBOX_LXC_NAME:-}" 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" </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 ' # 6. Provision (dashboards / peer lists / keys) "$(dirname "$0")/provision/run.sh" # 7. Sentinel touch /var/lib/secubox//.lxc-provisioned ``` ### Provisioning (`lib//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 generate` wrapper goes here. Provisioned content is read-only after install; user customisation lives in `/etc/secubox/.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 `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`](UI-GUIDE.md): - CRT P31 phosphor palette via `/shared/crt-{light,dark}.css` - Sidebar via `/shared/sidebar-{light,dark}.css` populated by `/shared/sidebar.js` - Theme toggle persists to `localStorage.sbx_theme` ### Auth wrapper Every page (except `/login.html`) must redirect to `/login.html?redirect=` when no `localStorage.sbx_token` is present. The hub's `index.html` shows the canonical `checkAuth()` implementation: ```js 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 = '
Redirecting to login...
'; } ``` Path is `/login.html`, **never** `/portal/login.html` — see #222. ### Menu integration (`menu.d/.json`) ```json { "title": "Module Name", "subtitle": "One-line purpose", "icon": "fa-icon-name", "url": "//", "section": "monitoring|security|network|publishing|hosting|search|admin", "order": 50, "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: ```html
...
...
...
``` Pulls live data from `/api/v1//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//` | **SecuBox admin** — static page calling `/api/v1//*`. NEVER a proxy to the app. | | `https://.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 `//` 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 `//` is a **separate, static** page under SecuBox theming. Its job is to surface status, control actions (restart, rescan, …) and the `Open UI →` button that points to the dedicated vhost. **Source-of-truth rule** — the `Open` button MUST read its URL from `/api/v1//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 `ctl`). ```html Open UI → ``` ```js // JS — read /access and patch the button href on load fetch('/api/v1//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 `//` 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/.conf`) Installed to `/etc/nginx/secubox.d/.conf` and auto-included by `/etc/nginx/sites-available/secubox`. ### Standard pattern (host FastAPI only) ```nginx # /etc/nginx/secubox.d/.conf location /api/v1// { proxy_pass http://unix:/run/secubox/.sock:/; include /etc/nginx/snippets/secubox-proxy.conf; } ``` ### Module with a real web UI — REQUIRED dual-vhost split `//` on the canonical hub vhost serves the **static SecuBox admin** (via `alias`, not `proxy_pass`). The real app lives at its own vhost (`.gk2.secubox.in`) which serves at the root path so absolute asset URLs resolve correctly. ```nginx # /etc/nginx/secubox.d/.conf — on the canonical hub vhost location /api/v1// { auth_request /__sbx_auth_verify; error_page 401 = @sbx_auth_login; rewrite ^/api/v1//(.*)$ /$1 break; proxy_pass http://unix:/run/secubox/.sock; include /etc/nginx/snippets/secubox-proxy.conf; } # SecuBox admin static page. NOT a proxy to the app. location // { auth_request /__sbx_auth_verify; error_page 401 = @sbx_auth_login; alias /usr/share/secubox/www//; index index.html; try_files $uri $uri/ //index.html; } ``` The dedicated vhost lives in `/etc/nginx/sites-available/.conf` (template in `packages/secubox-/nginx/-vhost.conf`), listens on `0.0.0.0:9080` for `server_name .gk2.secubox.in`, gates with `auth_request` and `proxy_pass`es to the LXC daemon at root. ### Deprecated: iframe pattern / `proxy_pass //` 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` ```text Source: secubox- Section: admin Priority: optional Maintainer: Gerald KERMA Build-Depends: debhelper-compat (= 13), dh-python, python3-all Standards-Version: 4.6.2 Homepage: https://cybermind.fr/secubox Package: secubox- Architecture: all Depends: ${misc:Depends}, secubox-core (>= 1.0), Recommends: Description: SecuBox ``` **Do not** ship a `debian/compat` file. The `Build-Depends: debhelper-compat (= 13)` is the single source of truth (see #220). ### `debian/rules` ```makefile #!/usr/bin/make -f %: dh $@ --with python3 override_dh_auto_install: install -d debian/secubox-/usr/lib/secubox/ cp -r api debian/secubox-/usr/lib/secubox// install -d debian/secubox-/usr/sbin install -m 755 sbin/ctl debian/secubox-/usr/sbin/ctl install -d debian/secubox-/usr/share/secubox/www [ -d www ] && cp -r www/. debian/secubox-/usr/share/secubox/www/ || true install -d debian/secubox-/usr/share/secubox/menu.d [ -d menu.d ] && cp -r menu.d/. debian/secubox-/usr/share/secubox/menu.d/ || true install -d debian/secubox-/etc/nginx/secubox.d [ -f nginx/.conf ] && cp nginx/.conf debian/secubox-/etc/nginx/secubox.d/ || true # LXC modules only: install -d debian/secubox-/usr/share/secubox/lib/ [ -d lib/ ] && cp -r lib//. debian/secubox-/usr/share/secubox/lib// || 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 `ctl install` (which fires after the LXC is up). ```sh #!/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/ if [ ! -f /etc/secubox/.toml ] && [ -f /etc/secubox/.toml.example ]; then cp /etc/secubox/.toml.example /etc/secubox/.toml chmod 640 /etc/secubox/.toml chown root:secubox /etc/secubox/.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-.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-.service 2>/dev/null || true ;; esac #DEBHELPER# exit 0 ``` ### `debian/secubox-.service` ```ini [Unit] Description=SecuBox API After=network.target secubox-core.service Wants=secubox-core.service [Service] UMask=0007 Type=simple User=secubox Group=secubox WorkingDirectory=/usr/lib/secubox/ 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/.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`](grammar.md) (canonical verbs table) + [`HOWTO-grammar.md`](../HOWTO-grammar.md) (recipe for adding a verb). ### Mandatory three-fold Every `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 ` | every URL/socket the module exposes + the auth method | Plain stdout for humans, `--json` for machines. Schema: ```json { "module": "", "version": "", "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 `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/.toml` then chains `install` + `status`. May be invoked from the WebUI as a `/api/v1//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: ```bash 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: ```text 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`. Not `creating`, `deletion`, `listed`. - Singular nouns: `route`, `repo`, `app`, `peer`, `dashboard`, `vhost`. - Aliases accepted but not advertised in `--help`: `rm`/`del` for `remove`, `ls` for `list`. - Exit codes: `0` success, `1` user error, `2` runtime error, `3` lxc/daemon down. ### Help `ctl --help` prints the noun-verb table; `ctl ` prints the verbs for that noun; `ctl --help` prints flags. --- ## 8. API conventions ### Transport FastAPI + Uvicorn on a Unix socket at `/run/secubox/.sock`. **Never** a TCP port (avoids LAN-side enumeration). ### Routing nginx proxies `/api/v1//*` → the Unix socket. The FastAPI app mounts its routes at the root (no `/api/v1/` prefix in the app code — nginx strips it). ### Mandatory endpoints | Endpoint | Purpose | |---|---| | `GET /status` | Same JSON as `ctl status --json` | | `GET /components` | Same JSON as `ctl components --json` | | `GET /access` | Same JSON as `ctl access --json` | | `GET /healthz` | `{"ok": true}` if the API process is alive — no shelling out | | `GET /version` | `{"version": "", "build": ""}` | 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 `ctl --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](https://github.com/bats-core/bats-core). One file per noun, or one file per coherent flow (`test-ctl-install.bats`, `test-ctl-dashboards.bats`). PATH-shim external commands (`lxc-create`, `lxc-attach`, `systemctl`, `curl`) in `tests/helpers.bash` so the suite runs offline: ```bash # 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- && dpkg-buildpackage -us -uc -b` succeeds with no warnings other than the standard `dpkg-source: warning: source format` line. - [ ] `lintian --no-tag-display-limit ../secubox-_*.deb` reports zero `E:` and zero `W:` (info lines OK). - [ ] `ctl --help` lists every noun. - [ ] `ctl --help` lists 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_token` set; redirects to `/login.html` when not. - [ ] nginx vhost includes both `/api/v1//` and `//` (if iframe target). - [ ] No `debian/compat` file (compat 13 declared via `Build-Depends` only). - [ ] 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-_*.deb` on a fresh VM completes; `systemctl status secubox-` is `loaded` (host modules: also `active`); LXC modules: `ctl install` then `ctl status` returns green. - [ ] Module added to `docs/MODULES.md` catalog. - [ ] CTL verbs added to `docs/grammar.md` canonical table. - [ ] `.claude/MIGRATION-MAP.md` row added. --- ## References - [`docs/grammar.md`](grammar.md) — canonical CTL grammar - [`HOWTO-grammar.md`](../HOWTO-grammar.md) — recipe for adding a verb - [`docs/UI-GUIDE.md`](UI-GUIDE.md) — UI theme and sidebar - [`docs/MODULES.md`](MODULES.md) — module catalog - [`docs/SECUBOX-DEV-METHODOLOGY.md`](SECUBOX-DEV-METHODOLOGY.md) — overall development workflow - [`.claude/PATTERNS.md`](../.claude/PATTERNS.md) — RPCD → FastAPI porting patterns - Plan example: [`docs/superpowers/plans/2026-05-20-grafana-yacy-rustdesk-lxc-modules.md`](superpowers/plans/2026-05-20-grafana-yacy-rustdesk-lxc-modules.md) — three modules following this guideline end-to-end.