mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 16:37:04 +00:00
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.
702 lines
26 KiB
Markdown
702 lines
26 KiB
Markdown
# SecuBox Module Guidelines
|
|
|
|
> Canonical reference for authoring a new `secubox-<module>` 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-<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.
|
|
|
|
```text
|
|
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` | 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/<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:
|
|
|
|
```bash
|
|
#!/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 generate` wrapper 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`](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=<current-path>`
|
|
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 = '<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`)
|
|
|
|
```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:
|
|
|
|
```html
|
|
<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`).
|
|
|
|
```html
|
|
<!-- index.html — the "Open <App> UI" button -->
|
|
<a class="btn primary" id="open-app" href="#" target="_blank" rel="noopener">
|
|
Open <App> UI →
|
|
</a>
|
|
```
|
|
|
|
```js
|
|
// 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)
|
|
|
|
```nginx
|
|
# /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.
|
|
|
|
```nginx
|
|
# /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_pass`es 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`
|
|
|
|
```text
|
|
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`
|
|
|
|
```makefile
|
|
#!/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).
|
|
|
|
```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/<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`
|
|
|
|
```ini
|
|
[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`](grammar.md) (canonical verbs table) +
|
|
[`HOWTO-grammar.md`](../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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```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
|
|
|
|
`<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](https://github.com/bats-core/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:
|
|
|
|
```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-<module> && 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-<module>_*.deb` reports zero `E:` and zero `W:` (info lines OK).
|
|
- [ ] `<module>ctl --help` lists every noun.
|
|
- [ ] `<module>ctl <noun> --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/<module>/` and `/<module>/` (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-<module>_*.deb` on a fresh VM completes; `systemctl status secubox-<module>` is `loaded` (host modules: also `active`); LXC modules: `<module>ctl install` then `<module>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.
|