diff --git a/DOCS/DOCUMENTATION-INDEX.md b/DOCS/DOCUMENTATION-INDEX.md index 31ef35b..9d59d3b 100644 --- a/DOCS/DOCUMENTATION-INDEX.md +++ b/DOCS/DOCUMENTATION-INDEX.md @@ -213,6 +213,11 @@ Follow this template when creating or revising documentation: Pointer: see `docs/embedded/docker-zigbee2mqtt.md` for the canonical version. +#### **embedded/vhost-manager.md** 🌐 +*How to publish services through nginx using the vhost manager and CLI helper.* + +Pointer: see `docs/embedded/vhost-manager.md` for the canonical version. + --- ### 5. Tools & Scripts Documentation diff --git a/DOCS/embedded/vhost-manager.md b/DOCS/embedded/vhost-manager.md new file mode 100644 index 0000000..ee9db6d --- /dev/null +++ b/DOCS/embedded/vhost-manager.md @@ -0,0 +1,102 @@ +# VHost Manager & Reverse Proxy Notes + +**Version:** 1.0.0 +**Last Updated:** 2025-12-28 +**Status:** Active + +SecuBox ships `luci-app-vhost-manager` (LuCI dashboard + RPC backend) and now the `scripts/vhostctl.sh` helper so apps, wizards, and profiles can declaratively publish HTTP services behind nginx with optional TLS and HTTP auth. + +--- + +## Prerequisites + +1. **Packages**: `luci-app-vhost-manager` installed (installs RPCD script + LuCI UI) and nginx with SSL (`nginx-ssl`). +2. **Certificates**: ACME via `acme.sh` (auto) or manual PEM files for `tls manual`. +3. **Apps**: Ensure the upstream service listens on localhost or LAN (e.g., Zigbee2MQTT UI on `http://127.0.0.1:8080`). +4. **Firewall**: Allow inbound 80/443 on the WAN interface. + +--- + +## CLI (`scripts/vhostctl.sh`) + +This helper manipulates `/etc/config/vhosts` and can be invoked by future wizards/App Store installers. + +```sh +# List existing mappings +scripts/vhostctl.sh list + +# Add HTTPS reverse proxy for Zigbee2MQTT UI +scripts/vhostctl.sh add \ + --domain zigbee.home.lab \ + --upstream http://127.0.0.1:8080 \ + --tls acme \ + --websocket \ + --enable + +# Enable/disable or remove later +scripts/vhostctl.sh disable --domain zigbee.home.lab +scripts/vhostctl.sh remove --domain zigbee.home.lab + +# Reload nginx after edits +scripts/vhostctl.sh reload +``` + +Options: + +| Option | Purpose | +|--------|---------| +| `--domain` | Public hostname (required). | +| `--upstream` | Local service URL (`http://127.0.0.1:8080`). | +| `--tls off|acme|manual` | TLS strategy. Use `manual` + `--cert/--key` for custom certs. | +| `--auth-user/--auth-pass` | Enable HTTP basic auth. | +| `--websocket` | Add `Upgrade` headers for WebSocket apps. | +| `--enable` / `--disable` | Toggle without deleting. | + +The script is idempotent: running `add` with an existing domain updates the entry. + +--- + +## LuCI Dashboard + +Navigate to **Services → SecuBox → VHost Manager** to: +- View active/disabled vhosts, TLS status, certificate expirations. +- Edit or delete entries, request ACME certificates, tail access logs. +- Use the form to create entries (domain, upstream, TLS, auth, WebSocket). + +The LuCI backend writes to the same `/etc/config/vhosts` file, so changes from `vhostctl.sh` appear immediately. + +--- + +## Example: Publish Zigbee2MQTT + +1. Install Zigbee2MQTT (Docker) and confirm the UI listens on port 8080 (see `docs/embedded/zigbee2mqtt-docker.md`). +2. Map it behind HTTPS: + ```sh + scripts/vhostctl.sh add \ + --domain zigbee.secubox.local \ + --upstream http://127.0.0.1:8080 \ + --tls acme \ + --websocket + scripts/vhostctl.sh reload + ``` +3. (Optional) Use LuCI to request certificates and monitor logs. + +--- + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| `scripts/vhostctl.sh add ...` errors “Unknown option” | Ensure busybox `sh` is used (`/bin/sh`). | +| ACME cert missing | Confirm `acme.sh` installed, domain resolves to router, 80/443 reachable. | +| 502/504 errors | Check upstream service, firewall, or change `--upstream` to LAN IP. | +| TLS manual mode fails | Provide full paths to PEM files and verify permissions. | +| Changes not visible | Run `scripts/vhostctl.sh reload` or `ubus call luci.vhost-manager reload_nginx`. | + +--- + +## Automation Notes + +- Wizards/App Store can shell out to `scripts/vhostctl.sh` to register services as they are installed. +- Profiles can keep declarative manifests (domain → upstream) and call `vhostctl.sh add/remove` when switching modes. +- `/etc/config/vhosts` remains the single source of truth, consumed by the LuCI app and the RPC backend. diff --git a/docs/documentation-index.md b/docs/documentation-index.md index 359fabb..e125e56 100644 --- a/docs/documentation-index.md +++ b/docs/documentation-index.md @@ -221,6 +221,19 @@ Follow this template when creating or revising documentation: **Size:** Short (~100 lines) +#### **embedded/vhost-manager.md** 🌐 +*How to publish local services behind nginx with vhost manager + CLI helper.* + +**Contents:** +- Overview of `luci-app-vhost-manager` UI +- CLI usage for `scripts/vhostctl.sh` (list/add/remove/reload) +- Example mapping for Zigbee2MQTT +- Troubleshooting tips (ACME, manual certs, upstream errors) + +**When to use:** Exposing Docker/LXC apps through HTTPS or integrating profiles/wizards. + +**Size:** Short (~120 lines) + --- ### 5. Tools & Scripts Documentation diff --git a/docs/embedded/vhost-manager.md b/docs/embedded/vhost-manager.md new file mode 100644 index 0000000..ee9db6d --- /dev/null +++ b/docs/embedded/vhost-manager.md @@ -0,0 +1,102 @@ +# VHost Manager & Reverse Proxy Notes + +**Version:** 1.0.0 +**Last Updated:** 2025-12-28 +**Status:** Active + +SecuBox ships `luci-app-vhost-manager` (LuCI dashboard + RPC backend) and now the `scripts/vhostctl.sh` helper so apps, wizards, and profiles can declaratively publish HTTP services behind nginx with optional TLS and HTTP auth. + +--- + +## Prerequisites + +1. **Packages**: `luci-app-vhost-manager` installed (installs RPCD script + LuCI UI) and nginx with SSL (`nginx-ssl`). +2. **Certificates**: ACME via `acme.sh` (auto) or manual PEM files for `tls manual`. +3. **Apps**: Ensure the upstream service listens on localhost or LAN (e.g., Zigbee2MQTT UI on `http://127.0.0.1:8080`). +4. **Firewall**: Allow inbound 80/443 on the WAN interface. + +--- + +## CLI (`scripts/vhostctl.sh`) + +This helper manipulates `/etc/config/vhosts` and can be invoked by future wizards/App Store installers. + +```sh +# List existing mappings +scripts/vhostctl.sh list + +# Add HTTPS reverse proxy for Zigbee2MQTT UI +scripts/vhostctl.sh add \ + --domain zigbee.home.lab \ + --upstream http://127.0.0.1:8080 \ + --tls acme \ + --websocket \ + --enable + +# Enable/disable or remove later +scripts/vhostctl.sh disable --domain zigbee.home.lab +scripts/vhostctl.sh remove --domain zigbee.home.lab + +# Reload nginx after edits +scripts/vhostctl.sh reload +``` + +Options: + +| Option | Purpose | +|--------|---------| +| `--domain` | Public hostname (required). | +| `--upstream` | Local service URL (`http://127.0.0.1:8080`). | +| `--tls off|acme|manual` | TLS strategy. Use `manual` + `--cert/--key` for custom certs. | +| `--auth-user/--auth-pass` | Enable HTTP basic auth. | +| `--websocket` | Add `Upgrade` headers for WebSocket apps. | +| `--enable` / `--disable` | Toggle without deleting. | + +The script is idempotent: running `add` with an existing domain updates the entry. + +--- + +## LuCI Dashboard + +Navigate to **Services → SecuBox → VHost Manager** to: +- View active/disabled vhosts, TLS status, certificate expirations. +- Edit or delete entries, request ACME certificates, tail access logs. +- Use the form to create entries (domain, upstream, TLS, auth, WebSocket). + +The LuCI backend writes to the same `/etc/config/vhosts` file, so changes from `vhostctl.sh` appear immediately. + +--- + +## Example: Publish Zigbee2MQTT + +1. Install Zigbee2MQTT (Docker) and confirm the UI listens on port 8080 (see `docs/embedded/zigbee2mqtt-docker.md`). +2. Map it behind HTTPS: + ```sh + scripts/vhostctl.sh add \ + --domain zigbee.secubox.local \ + --upstream http://127.0.0.1:8080 \ + --tls acme \ + --websocket + scripts/vhostctl.sh reload + ``` +3. (Optional) Use LuCI to request certificates and monitor logs. + +--- + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| `scripts/vhostctl.sh add ...` errors “Unknown option” | Ensure busybox `sh` is used (`/bin/sh`). | +| ACME cert missing | Confirm `acme.sh` installed, domain resolves to router, 80/443 reachable. | +| 502/504 errors | Check upstream service, firewall, or change `--upstream` to LAN IP. | +| TLS manual mode fails | Provide full paths to PEM files and verify permissions. | +| Changes not visible | Run `scripts/vhostctl.sh reload` or `ubus call luci.vhost-manager reload_nginx`. | + +--- + +## Automation Notes + +- Wizards/App Store can shell out to `scripts/vhostctl.sh` to register services as they are installed. +- Profiles can keep declarative manifests (domain → upstream) and call `vhostctl.sh add/remove` when switching modes. +- `/etc/config/vhosts` remains the single source of truth, consumed by the LuCI app and the RPC backend. diff --git a/scripts/vhostctl.sh b/scripts/vhostctl.sh new file mode 100755 index 0000000..da489dc --- /dev/null +++ b/scripts/vhostctl.sh @@ -0,0 +1,240 @@ +#!/bin/sh +# +# SecuBox VHost helper +# Manipulates /etc/config/vhosts from the CLI so wizards/app store can add reverse proxies. + +set -eu + +CONFIG="/etc/config/vhosts" + +info() { printf '[INFO] %s\n' "$*"; } +warn() { printf '[WARN] %s\n' "$*" >&2; } +err() { printf '[ERROR] %s\n' "$*" >&2; } + +usage() { + cat <<'EOF' +Usage: vhostctl.sh [options] + +Commands: + list List all configured vhosts + show --domain example.com Show raw UCI entries for a vhost + add --domain example.com --upstream http://127.0.0.1:8080 [options] + remove --domain example.com Delete a vhost + enable --domain example.com Set enabled=1 + disable --domain example.com Set enabled=0 + reload Trigger nginx reload via luci.vhost-manager + +Options for add/show: + --tls off|acme|manual + --cert /path/to/fullchain.pem (required when --tls manual) + --key /path/to/privkey.pem (required when --tls manual) + --auth-user USER --auth-pass PASS + --websocket Enable websocket headers + --no-websocket Disable websocket headers + +Examples: + vhostctl.sh add --domain zigbee.local --upstream http://127.0.0.1:8080 --tls acme + vhostctl.sh add --domain media.lab --upstream http://10.10.6.20:32400 --tls manual \ + --cert /etc/ssl/media.pem --key /etc/ssl/media.key + vhostctl.sh enable --domain zigbee.local + vhostctl.sh reload +EOF +} + +ensure_config() { + [ -f "$CONFIG" ] && return + cat <<'EOF' >"$CONFIG" +config global 'global' + option enabled '1' + option auto_reload '1' +EOF +} + +sanitize_section() { + local cleaned + cleaned=$(echo "$1" | tr 'A-Z' 'a-z' | tr -cd 'a-z0-9_' ) + [ -z "$cleaned" ] && cleaned="vhost" + printf 'vh_%s' "$cleaned" +} + +strip_quotes() { + local val="$1" + val="${val#\'}" + val="${val%\'}" + printf '%s' "$val" +} + +find_section() { + local domain="$1" + uci -q show vhosts 2>/dev/null | while IFS='=' read -r key value; do + case "$key" in + vhosts.*.domain) + local section="${key%.*}" + local val + val=$(strip_quotes "$value") + if [ "$val" = "$domain" ]; then + echo "${section#vhosts.}" + return + fi + ;; + esac + done + return 1 +} + +set_option() { + local section="$1" + local key="$2" + local value="$3" + [ -n "$value" ] && uci set "vhosts.${section}.${key}=$value" || uci -q delete "vhosts.${section}.${key}" +} + +reload_nginx() { + if command -v ubus >/dev/null 2>&1; then + if ubus call luci.vhost-manager reload_nginx >/dev/null 2>&1; then + info "nginx reloaded." + else + warn "Failed to reload nginx via luci.vhost-manager." + fi + else + warn "ubus not available; reload nginx manually." + return 1 + fi +} + +cmd_list() { + ensure_config + printf '%-30s %-30s %-6s %-6s\n' "Domain" "Upstream" "TLS" "En" + uci -q show vhosts 2>/dev/null | grep '\.domain=' | while IFS='=' read -r key value; do + local sname="${key%.*}" + sname="${sname#vhosts.}" + local domain upstream tls enabled + domain=$(strip_quotes "$value") + upstream=$(uci -q get "vhosts.${sname}.upstream" 2>/dev/null || printf '') + tls=$(uci -q get "vhosts.${sname}.tls" 2>/dev/null || printf 'off') + enabled=$(uci -q get "vhosts.${sname}.enabled" 2>/dev/null || printf '1') + printf '%-30s %-30s %-6s %-6s\n' "$domain" "${upstream:-n/a}" "$tls" "$enabled" + done +} + +cmd_show() { + local domain="$1" + local section + section=$(find_section "$domain") || { + err "Domain not found: $domain" + exit 1 + } + uci -q show "vhosts.$section" +} + +cmd_add() { + local domain='' upstream='' tls='off' websocket='' auth_user='' auth_pass='' cert='' key='' + local enabled='1' + while [ $# -gt 0 ]; do + case "$1" in + --domain) domain="$2"; shift 2 ;; + --upstream) upstream="$2"; shift 2 ;; + --tls) tls="$2"; shift 2 ;; + --cert) cert="$2"; shift 2 ;; + --key) key="$2"; shift 2 ;; + --auth-user) auth_user="$2"; shift 2 ;; + --auth-pass) auth_pass="$2"; shift 2 ;; + --websocket) websocket='1'; shift ;; + --no-websocket) websocket='0'; shift ;; + --enable) enabled='1'; shift ;; + --disable) enabled='0'; shift ;; + *) err "Unknown option: $1"; usage; exit 1 ;; + esac + done + [ -n "$domain" ] || { err "--domain required"; exit 1; } + [ -n "$upstream" ] || { err "--upstream required"; exit 1; } + if [ "$tls" = "manual" ] && { [ -z "$cert" ] || [ -z "$key" ]; }; then + err "Manual TLS requires --cert and --key" + exit 1 + fi + ensure_config + local section + section=$(find_section "$domain" || true) + if [ -z "$section" ]; then + section=$(uci add vhosts vhost) + local sanitized + sanitized=$(sanitize_section "$domain") + uci rename "vhosts.$section=$sanitized" + section="$sanitized" + fi + set_option "$section" domain "$domain" + set_option "$section" upstream "$upstream" + set_option "$section" tls "$tls" + set_option "$section" cert_path "$cert" + set_option "$section" key_path "$key" + set_option "$section" auth_user "$auth_user" + set_option "$section" auth_pass "$auth_pass" + set_option "$section" enabled "$enabled" + [ -n "$websocket" ] && set_option "$section" websocket "$websocket" + uci commit vhosts + info "VHost saved for $domain → $upstream (section: $section)" +} + +cmd_remove() { + local domain="$1" + local section + section=$(find_section "$domain") || { + err "Domain not found: $domain" + exit 1 + } + uci delete "vhosts.$section" + uci commit vhosts + info "Removed vhost for $domain" +} + +cmd_toggle() { + local domain="$1" + local value="$2" + local section + section=$(find_section "$domain") || { + err "Domain not found: $domain" + exit 1 + } + set_option "$section" enabled "$value" + uci commit vhosts + info "Set enabled=$value for $domain" +} + +command="${1:-help}" +[ $# -gt 0 ] && shift + +case "$command" in + list) cmd_list ;; + show) + if [ "${1:-}" != "--domain" ] || [ -z "${2:-}" ]; then + err "show requires --domain " + exit 1 + fi + cmd_show "$2" + ;; + add) cmd_add "$@" ;; + remove) + if [ "${1:-}" != "--domain" ] || [ -z "${2:-}" ]; then + err "remove requires --domain " + exit 1 + fi + cmd_remove "$2" + ;; + enable) + if [ "${1:-}" != "--domain" ] || [ -z "${2:-}" ]; then + err "enable requires --domain " + exit 1 + fi + cmd_toggle "$2" "1" + ;; + disable) + if [ "${1:-}" != "--domain" ] || [ -z "${2:-}" ]; then + err "disable requires --domain " + exit 1 + fi + cmd_toggle "$2" "0" + ;; + reload) reload_nginx ;; + help|--help|-h|"") usage ;; + *) err "Unknown command: $command"; usage; exit 1 ;; +esac