Compare commits

...
4 Commits
Author SHA1 Message Date
gandalf a1c60d6be6 fix(soc): firewall_summary falls back to stale cache, not zeros
Deployment to gk2 surfaced two issues with v2.12.17:

  1. Stray cron files (/etc/cron.d/secubox-nft-cache, /etc/cron.d/
     secubox-nft-stats) left over from an earlier ad-hoc fix on the
     board were racing with the new systemd timer — both wrote to the
     SAME cache files, occasionally producing a 0-byte JSON that
     parsed to 0 tables/0 chains/0 rules. These crons are NOT shipped
     by any package (only mentioned in WIP/HISTORY); they have to be
     scrubbed from running boards manually. Documented for the
     upgrade-from-v2.12.16 path.

  2. The hub runs with NoNewPrivileges=true. That blocks setuid
     traversal, which means `sudo` from inside the hub process
     ALWAYS fails silently — the realtime fallback in v2.12.17
     therefore returned None and the endpoint surfaced zeros with
     source=realtime instead of useful data.

Rewrite the endpoint's fallback ladder:

  fresh cache (mtime < 60 s) → source=cache (fast path)
  stale/missing               → try realtime via sudo (best-effort)
  realtime failed             → fall back to STALE cache contents
                                source=cache-stale
  no cache at all              → source=none, zeros (only when the
                                timer has never run yet)

Showing 5-minute-old firewall stats on the dashboard is strictly
better than showing zeros while the cache populator is between runs.
The systemd timer recovers the cache within at most 30 s.

Sudoers fragment and -j arg ordering are kept verbatim from v2.12.17
so the realtime path lights up the moment an operator drops
NoNewPrivileges from the hub unit.

Verified on gk2:
  fresh cache         → source=cache, tables=7 chains=10 rules=19
  ruleset aged to 5m  → source=cache-stale, tables=7 chains=10 rules=19
  both files removed  → source=none, zeros
  +30 s timer tick    → source=cache restored
2026-05-26 13:47:01 +02:00
gandalf 40b5837262 fix(soc): move nft cache into secubox-hub + realtime fallback
The v2.12.16 fix put the nft cache populator in firstboot.sh, which
broke the rule "cron lives next to the module that consumes it" — the
SOC firewall_summary endpoint is owned by secubox-hub, so the timer
that feeds its cache belongs in the same package.

This commit:
  * removes the inline cache populator from image/firstboot.sh
  * ships /usr/sbin/secubox-nft-cache + secubox-nft-cache.{service,timer}
    inside the secubox-hub package, with the timer auto-enabled via the
    timers.target.wants/ symlink
  * adds a sudoers fragment authorising user secubox to run
    `nft list *`, `nft -j list *`, and `systemctl --no-block start
    secubox-nft-cache.service` (and only that one unit)
  * rewrites the /firewall_summary endpoint so the cache is a speed
    optimisation, not the source of truth: if the cache file is missing
    or older than 60 s, fall back to a realtime `sudo nft list`,
    surface `source: "realtime"` in the JSON, and nudge the cache
    populator via systemctl so the next request lands on a hot cache

Per the operator directive: "les cache double buffer ne doivent pas
tomber et permettre juste d'aller plus vite... un echec de cron et de
cache doit forcer le cron et faire du realtime".
2026-05-26 12:43:55 +02:00
gandalf 27f9cd9db0 fix(soc): populate nft cache + add status field to firewall_summary
Operator screenshot: SOC dashboard "🔥 FIREWALL nftables / Status
INACTIVE / Tables 7 / Chains 10 / Rules 19" with all counters at 0
(Accept/CAPI/CrowdSec/Manual).

Two bugs found:

1. The /api/v1/hub/public/firewall_summary endpoint docstring says
   it reads cache files `/var/cache/secubox/nft-*.txt` "updated by
   cron every 30s" — but the cron never existed in the repo. The
   table/chain/rule counts were getting populated somehow (probably
   a one-shot at firstboot) while the counters file stayed empty,
   hence 7/10/19 visible but all hit-counts zero.

2. The endpoint never returned a `status` field, so the React
   bundle (secubox-soc-web) defaulted to "INACTIVE" regardless of
   whether nftables.service was active or rules were loaded.

Fixes:

* firstboot.sh installs `/usr/local/bin/secubox-nft-cache` +
  `secubox-nft-cache.service` + `.timer` (oneshot, every 30s,
  OnBootSec=10s). Runs as root via systemd so it can call
  `nft -j list ruleset` + `nft list counters`. Writes the JSON
  ruleset + counters dump + a `.lastrun` sentinel into
  /var/cache/secubox/.

* hub `/firewall_summary` endpoint now:
  - calls `systemctl is-active nftables.service` and returns a
    `status` field ("active" | "inactive" | "active-manual" | "error")
  - "active-manual" = systemd reports inactive but the kernel has
    rules loaded (happens when firstboot.sh did `nft -f` directly
    and the systemd unit didn't get re-enabled). Treat as active
    for dashboard purposes — operator wants to see "firewall is up"
    when there ARE rules.
  - returns `cache_last_run` so the dashboard can warn on stale
    cache.

WAF metrics empty is by design on a live USB without mitmproxy LXC
(THREATS_LOG never gets written → zero counters). Not addressed
here. Operator runs install-lxc.sh once to enable WAF traffic.
2026-05-26 12:35:37 +02:00
gandalf efd6a462be feat(rpi400): port SecuBox banner + secubox-status/help + dynamic MOTD
rpi400 v2.12.13 image audit (operator-reported "ancien style et il
manque des tas de seccubox tools"):

  136 secubox-* packages installed ✓
  but only 11 secubox-* binaries on PATH
  /etc/motd was vanilla Debian
  /etc/issue was vanilla Debian "Debian GNU/Linux 12 \n \l"
  no /etc/secubox/build-info.json

build-live-usb.sh creates ~30 secubox-* CLI helpers + the cyber-CRT
boot banner + dynamic MOTD via update-motd.d. None of that was in
build-rpi-usb.sh. Targeted port of the four most-visible blocks
into build-rpi-usb.sh at line ~780 (right after `SecuBox packages
installed`):

  * /etc/issue with cybermind banner + getty `\4` IPv4 escape
  * /etc/update-motd.d/10-secubox with live `hostname -I` substitution
  * /usr/bin/secubox-status (system overview, services, network)
  * /usr/bin/secubox-help (command index)
  * /etc/secubox/build-info.json (version + git commit + builder)

Broader refactor of the four build-*.sh siblings into shared lib/*.sh
is tracked in docs/superpowers/plans/2026-05-26-build-scripts-refactor.md
— this commit is the tactical port to unblock rpi400 operators now.

The MOTD says "RPI400 LIVE" instead of "LIVE USB MODE" so operators
can tell at a glance which platform they're on without `uname -m`.
2026-05-26 12:14:02 +02:00
7 changed files with 387 additions and 33 deletions
+136
View File
@@ -777,6 +777,142 @@ ln -sf /usr/lib/systemd/system/nginx.service \
ok "SecuBox packages installed"
# ══════════════════════════════════════════════════════════════════
# Step 5.5: SecuBox CRT-style banners + CLI helpers
# ══════════════════════════════════════════════════════════════════
# Ported from build-live-usb.sh so rpi400 ships with the same SecuBox
# look-and-feel + CLI tools as the amd64 live USB. Until the broader
# build-scripts refactor lands (see docs/superpowers/plans/2026-05-26-
# build-scripts-refactor.md), this block is the targeted port.
log "Creating SecuBox boot banners + CLI helpers..."
BUILD_TIMESTAMP=$(date -Iseconds)
# /etc/issue — pre-login banner with getty \4 escape for IPv4
printf '%b' "\e[38;5;29m
██████ ███████ ██████ ██ ██ ██████ ██████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
███████ █████ ██ ██ ██ ██████ ██ ██ ███
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
███████ ███████ ██████ ██████ ██████ ██████ ██ ██
\e[0m
\e[38;5;45m ⚡ CyberMind Security Platform\e[0m \e[38;5;82mv${SECUBOX_VERSION}\e[0m \e[38;5;242m\\l @ \\n\e[0m
\e[38;5;242m Build: ${BUILD_TIMESTAMP}\e[0m
\e[38;5;250m 🔐 Default: \e[38;5;214mroot\e[38;5;250m / \e[38;5;214msecubox\e[0m
\e[38;5;250m 🌐 Web UI: \e[38;5;45mhttps://\\4:9443\e[0m
\e[38;5;250m 📡 SSH: \e[38;5;45mport 22\e[0m
\e[38;5;242m─────────────────────────────────────────────────────────────\e[0m
" > "${ROOTFS}/etc/issue"
# Dynamic MOTD via update-motd.d — live IP at login time
: > "${ROOTFS}/etc/motd"
mkdir -p "${ROOTFS}/etc/update-motd.d"
cat > "${ROOTFS}/etc/update-motd.d/10-secubox" <<MOTD_DYN
#!/bin/sh
# Generated by build-rpi-usb.sh — dynamic MOTD with live IP.
ip=\$(hostname -I 2>/dev/null | awk '{print \$1}')
[ -z "\$ip" ] && ip="no-ip"
printf '%b' "\e[38;5;214m
╔═══════════════════════════════════════════════════════════════╗
║\e[38;5;45m ███████╗███████╗ ██████╗██╗ ██╗██████╗ ██████╗ ██╗ ██╗ \e[38;5;214m║
║\e[38;5;45m ██╔════╝██╔════╝██╔════╝██║ ██║██╔══██╗██╔═══██╗╚██╗██╔╝ \e[38;5;214m║
║\e[38;5;45m ███████╗█████╗ ██║ ██║ ██║██████╔╝██║ ██║ ╚███╔╝ \e[38;5;214m║
║\e[38;5;45m ╚════██║██╔══╝ ██║ ██║ ██║██╔══██╗██║ ██║ ██╔██╗ \e[38;5;214m║
║\e[38;5;45m ███████║███████╗╚██████╗╚██████╔╝██████╔╝╚██████╔╝██╔╝ ██╗ \e[38;5;214m║
║\e[38;5;45m ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ \e[38;5;214m║
║\e[38;5;82m ⚡ RPI400 LIVE ⚡ v${SECUBOX_VERSION} \e[38;5;214m║
╚═══════════════════════════════════════════════════════════════╝\e[0m
\e[38;5;242m Build: ${BUILD_TIMESTAMP}\e[0m
\e[38;5;250m 🌐 Web UI: \e[38;5;45mhttps://\${ip}:9443\e[0m
\e[38;5;250m 🔐 Credentials: \e[38;5;214mroot\e[38;5;250m / \e[38;5;214msecubox\e[0m
\e[38;5;250m 📖 Docs: \e[38;5;45mhttps://secubox.in/docs\e[0m
\e[38;5;242m Type \e[38;5;82msecubox-status\e[38;5;242m for system overview\e[0m
"
MOTD_DYN
chmod +x "${ROOTFS}/etc/update-motd.d/10-secubox"
# /usr/bin/secubox-status — runtime system overview
cat > "${ROOTFS}/usr/bin/secubox-status" <<'STATUS_SCRIPT'
#!/bin/bash
GOLD='\033[38;5;214m'; CYAN='\033[38;5;45m'; GREEN='\033[38;5;82m'
RED='\033[38;5;196m'; GRAY='\033[38;5;242m'; WHITE='\033[38;5;250m'; RESET='\033[0m'
ok="${GREEN}●${RESET}"; fail="${RED}●${RESET}"; warn="${GOLD}●${RESET}"
echo -e "${CYAN}"
echo ' ╭──────────────────────────────────────────────────────────╮'
echo ' │ ⚡ SecuBox System Status ⚡ │'
echo ' ╰──────────────────────────────────────────────────────────╯'
echo -e "${RESET}"
echo -e "${WHITE} 📊 System Info${RESET}"
echo -e " ${GRAY}Hostname:${RESET} $(hostname)"
echo -e " ${GRAY}Uptime:${RESET} $(uptime -p 2>/dev/null || echo 'N/A')"
echo -e " ${GRAY}Memory:${RESET} $(free -h | awk '/^Mem:/{printf "%s / %s", $3, $2}')"
echo -e " ${GRAY}Disk:${RESET} $(df -h / | awk 'NR==2{printf "%s / %s (%s)", $3, $2, $5}')"
echo ""
echo -e "${WHITE} 🌐 Network${RESET}"
for iface in $(ip -o link show | awk -F': ' '{print $2}' | grep -v '^lo$'); do
ip_addr=$(ip -4 addr show "$iface" 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
[[ -n "$ip_addr" ]] && echo -e " ${ok} ${GRAY}${iface}:${RESET} ${CYAN}${ip_addr}${RESET}"
done
echo ""
echo -e "${WHITE} 🔧 Core Services${RESET}"
for svc in nginx haproxy secubox-api secubox-hub crowdsec; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo -e " ${ok} ${GRAY}${svc}${RESET}"
elif systemctl list-unit-files "${svc}.service" 2>/dev/null | grep -q "$svc"; then
echo -e " ${fail} ${GRAY}${svc}${RESET} (stopped)"
fi
done
echo ""
IP=$(hostname -I | awk '{print $1}')
echo -e "${GOLD} ────────────────────────────────────────────────────────────${RESET}"
echo -e "${WHITE} 🔗 Quick Access${RESET}"
echo -e " ${GRAY}Dashboard:${RESET} ${CYAN}https://${IP:-localhost}:9443${RESET}"
echo ""
STATUS_SCRIPT
chmod +x "${ROOTFS}/usr/bin/secubox-status"
# /usr/bin/secubox-help — command index
cat > "${ROOTFS}/usr/bin/secubox-help" <<'HELP_CMD'
#!/bin/bash
GOLD='\033[38;5;214m'; CYAN='\033[38;5;45m'; WHITE='\033[38;5;250m'
GRAY='\033[38;5;242m'; RESET='\033[0m'
echo -e "${GOLD}"
echo ' ╭─────────────────────────────────────────────────────────╮'
echo ' │ ⚡ SecuBox Quick Commands │'
echo ' ╰─────────────────────────────────────────────────────────╯'
echo -e "${RESET}"
echo -e " ${WHITE}System${RESET}"
echo -e " ${CYAN}secubox-status${GRAY} System overview with services${RESET}"
echo ""
echo -e " ${WHITE}Web Access${RESET}"
echo -e " ${GRAY}Dashboard:${RESET} ${CYAN}https://$(hostname -I 2>/dev/null | awk '{print $1}' || echo 'localhost'):9443${RESET}"
echo ""
HELP_CMD
chmod +x "${ROOTFS}/usr/bin/secubox-help"
# /etc/secubox/build-info.json — version + build identification
mkdir -p "${ROOTFS}/etc/secubox"
cat > "${ROOTFS}/etc/secubox/build-info.json" <<BUILDINFO
{
"build_timestamp": "${BUILD_TIMESTAMP}",
"build_date": "$(date -I)",
"git_commit": "$(git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse --short HEAD 2>/dev/null || echo 'unknown')",
"git_branch": "$(git -C "$(dirname "${BASH_SOURCE[0]}")/.." rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')",
"board": "rpi400-arm64-live",
"version": "${SECUBOX_VERSION}",
"builder": "$(whoami)@$(hostname)"
}
BUILDINFO
ok "SecuBox banners + CLI helpers installed"
# ══════════════════════════════════════════════════════════════════
# Step 6: Raspberry Pi boot configuration
# ══════════════════════════════════════════════════════════════════
+9
View File
@@ -478,6 +478,15 @@ systemctl enable nftables
systemctl restart nftables 2>/dev/null || true
ok "nftables configuré"
# The nftables cache timer/service that powers the SOC firewall_summary
# widget is now shipped by the secubox-hub package itself (it owns the
# consuming endpoint). See:
# packages/secubox-hub/debian/secubox-nft-cache.service
# packages/secubox-hub/debian/secubox-nft-cache.timer
# packages/secubox-hub/sbin/secubox-nft-cache
# Each module that needs cached external state ships its own
# cache-populator + timer; firstboot stays platform-only.
# ── Kiosk safety net ────────────────────────────────────────────────
# build-live-usb.sh touches /var/lib/secubox/.kiosk-enabled and enables
# secubox-kiosk.service at build time, but those have been observed
+184 -33
View File
@@ -11,6 +11,7 @@ from secubox_core.kiosk import (
import subprocess
import json
import asyncio
import os
import time
from pathlib import Path
@@ -1863,59 +1864,209 @@ app.include_router(router)
# ══════════════════════════════════════════════════════════════════
# Firewall Summary — nftables counters for SOC dashboard
# Reads from cache files (/var/cache/secubox/nft-*.txt) updated by cron
#
# Cache (/var/cache/secubox/nft-*) is populated every 30s by
# secubox-nft-cache.timer (shipped by this package). It is a speed
# optimisation — the dashboard widget must never go blank because the
# cache is missing or stale. If the cache file is absent or older than
# NFT_CACHE_MAX_AGE seconds we fall back to a realtime `sudo nft list`
# call AND nudge systemd to refresh the cache for the next request.
# ══════════════════════════════════════════════════════════════════
NFT_CACHE_DIR = "/var/cache/secubox"
NFT_CACHE_MAX_AGE = 60 # seconds before we consider the cache stale
def _parse_nft_counters(text: str) -> dict:
counters: dict = {}
current_counter = None
for line in text.splitlines():
line = line.strip()
if line.startswith("counter "):
parts = line.split()
if len(parts) >= 2:
current_counter = parts[1]
elif "packets" in line and current_counter:
parts = line.split()
for i, p in enumerate(parts):
if p == "packets" and i + 1 < len(parts):
try:
packets = int(parts[i + 1])
except ValueError:
break
counters[current_counter] = counters.get(current_counter, 0) + packets
break
return counters
def _parse_nft_ruleset_json(text: str) -> tuple:
try:
data = json.loads(text)
except (json.JSONDecodeError, ValueError):
return 0, 0, 0
nftables = data.get("nftables", [])
tables = sum(1 for x in nftables if "table" in x)
chains = sum(1 for x in nftables if "chain" in x)
rules = sum(1 for x in nftables if "rule" in x)
return tables, chains, rules
def _read_cache(path: str, max_age: int | None = None) -> tuple:
"""Return (contents, age_seconds) or (None, None) if unreadable.
When max_age is given, also return (None, age) if older than that
callers can still use the stale contents by reading the file again
with max_age=None."""
try:
st = os.stat(path)
except FileNotFoundError:
return None, None
age = time.time() - st.st_mtime
if max_age is not None and age > max_age:
return None, age
try:
with open(path, "r") as f:
return f.read(), age
except OSError:
return None, age
def _nft_realtime(args: list) -> str | None:
"""Run `sudo /usr/sbin/nft <args>` with a tight timeout. Returns
stdout on success, None on any failure (including the silent
failure that happens when the hub runs under NoNewPrivileges=true,
which blocks setuid traversal so sudo itself can never elevate).
The sudoers fragment shipped by this package whitelists `nft list *`
for user secubox useful if the operator ever decides to drop the
NoNewPrivileges sandbox; harmless otherwise."""
try:
r = subprocess.run(
["sudo", "-n", "/usr/sbin/nft"] + args,
capture_output=True, text=True, timeout=3,
)
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
return None
if r.returncode != 0 or not r.stdout.strip():
return None
return r.stdout
def _trigger_cache_refresh() -> None:
"""Fire-and-forget systemctl start of the cache populator. Used
when we just served a realtime response so the next request lands
on a hot cache. The hub runs as user `secubox`; the sudoers
fragment shipped by this package whitelists the start of this one
specific unit, password-less."""
try:
subprocess.Popen(
["sudo", "-n", "/usr/bin/systemctl", "--no-block", "start",
"secubox-nft-cache.service"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
except (FileNotFoundError, OSError):
pass
@public_router.get("/firewall_summary")
async def firewall_summary():
"""Get nftables stats for dashboard from cache files."""
try:
counters = {}
# Read from cache file (updated by cron every 30s)
try:
with open("/var/cache/secubox/nft-counters.txt", "r") as f:
current_counter = None
for line in f:
line = line.strip()
if line.startswith("counter "):
parts = line.split()
if len(parts) >= 2:
current_counter = parts[1]
elif "packets" in line and current_counter:
parts = line.split()
for i, p in enumerate(parts):
if p == "packets" and i + 1 < len(parts):
packets = int(parts[i + 1])
counters[current_counter] = counters.get(current_counter, 0) + packets
break
except FileNotFoundError:
pass
"""nftables stats for the SOC dashboard widget.
Strategy: fresh cache realtime stale cache.
1. If the cache files are fresh (mtime within NFT_CACHE_MAX_AGE),
parse them fast path, no privileged calls.
2. Otherwise try realtime via `sudo nft list ...`. This works ONLY
if the hub is not sandboxed by NoNewPrivileges; on the shipped
systemd unit it is, so realtime is best-effort.
3. If realtime returned nothing, fall back to the *stale* cache
showing slightly old data is far better than zeros on the SOC
widget. The systemd timer will refresh the cache shortly.
`source` in the response tells the frontend which path we took:
"cache", "realtime", "cache-stale", or "none"."""
try:
ruleset_path = os.path.join(NFT_CACHE_DIR, "nft-ruleset.json")
counters_path = os.path.join(NFT_CACHE_DIR, "nft-counters.txt")
lastrun_path = os.path.join(NFT_CACHE_DIR, "nft-cache.lastrun")
ruleset_text, _ = _read_cache(ruleset_path, NFT_CACHE_MAX_AGE)
counters_text, _ = _read_cache(counters_path, NFT_CACHE_MAX_AGE)
source = "cache"
if ruleset_text is None or counters_text is None:
source = "realtime"
# `-j` is a global option for nft and must come before the
# command. The sudoers fragment shipped by this package
# whitelists both `nft list *` and `nft -j list *` — useful
# if the operator drops NoNewPrivileges from the hub unit.
rt_ruleset = _nft_realtime(["-j", "list", "ruleset"])
rt_counters = _nft_realtime(["list", "counters"])
if rt_ruleset is not None:
ruleset_text = rt_ruleset
if rt_counters is not None:
counters_text = rt_counters
_trigger_cache_refresh()
# Realtime denied (NoNewPrivileges blocks sudo) — fall back
# to the stale cache rather than returning zeros. The timer
# will refresh the cache to a fresh state within ~30 s.
if ruleset_text is None:
ruleset_text, _ = _read_cache(ruleset_path)
if ruleset_text:
source = "cache-stale"
if counters_text is None:
counters_text, _ = _read_cache(counters_path)
if counters_text and source != "cache-stale":
source = "cache-stale"
if ruleset_text is None and counters_text is None:
source = "none"
tables = chains = rules = 0
if ruleset_text:
tables, chains, rules = _parse_nft_ruleset_json(ruleset_text)
counters = _parse_nft_counters(counters_text) if counters_text else {}
processed = counters.get("processed", 0)
dropped = sum(v for k, v in counters.items() if "blacklist" in k.lower())
# Get table/chain/rule counts from JSON cache
tables, chains, rules = 2, 4, 0
# systemd state of nftables.service.
status = "inactive"
try:
with open("/var/cache/secubox/nft-ruleset.json", "r") as f:
data = json.loads(f.read())
nftables = data.get("nftables", [])
tables = sum(1 for x in nftables if "table" in x)
chains = sum(1 for x in nftables if "chain" in x)
rules = sum(1 for x in nftables if "rule" in x)
r = subprocess.run(
["systemctl", "is-active", "nftables.service"],
capture_output=True, text=True, timeout=2,
)
if r.stdout.strip() == "active":
status = "active"
except Exception:
pass
# Rules in the kernel but systemd inactive (e.g. firstboot
# loaded them with `nft -f`) — surface as active for the widget.
if status == "inactive" and rules > 0:
status = "active-manual"
last_cache_run = None
try:
with open(lastrun_path, "r") as f:
last_cache_run = f.read().strip()
except FileNotFoundError:
pass
return {
"status": status,
"tables": tables,
"chains": chains,
"rules": rules,
"processed": processed,
"dropped": dropped,
"accepted": processed - dropped if processed > dropped else 0,
"counters": counters
"counters": counters,
"cache_last_run": last_cache_run,
"source": source,
}
except Exception as e:
return {"error": str(e), "tables": 0, "chains": 0, "rules": 0, "dropped": 0, "accepted": 0, "processed": 0}
return {"error": str(e), "status": "error", "tables": 0, "chains": 0, "rules": 0, "dropped": 0, "accepted": 0, "processed": 0}
app.include_router(public_router) # Re-include for firewall_summary
+20
View File
@@ -15,3 +15,23 @@ override_dh_auto_install:
# Nginx snippets (shared CORS config)
install -d debian/secubox-hub/etc/nginx/snippets
[ -d nginx/snippets ] && cp -r nginx/snippets/. debian/secubox-hub/etc/nginx/snippets/ || true
# nftables cache populator + timer (powers SOC firewall_summary widget)
install -d debian/secubox-hub/usr/sbin
install -m 0755 sbin/secubox-nft-cache debian/secubox-hub/usr/sbin/secubox-nft-cache
install -d debian/secubox-hub/lib/systemd/system
install -m 0644 debian/secubox-nft-cache.service debian/secubox-hub/lib/systemd/system/
install -m 0644 debian/secubox-nft-cache.timer debian/secubox-hub/lib/systemd/system/
install -d debian/secubox-hub/etc/systemd/system/timers.target.wants
ln -sf /lib/systemd/system/secubox-nft-cache.timer \
debian/secubox-hub/etc/systemd/system/timers.target.wants/secubox-nft-cache.timer
# sudoers fragment so the hub service (User=secubox) can fall back to
# realtime `nft list` when the cache is stale (read-only nft, no risk).
# Two patterns because `-j` is a global option for nft and must precede
# the command (`nft -j list ruleset`, not `nft list ruleset -j`).
install -d debian/secubox-hub/etc/sudoers.d
printf '%s\n%s\n%s\n' \
'secubox ALL=(root) NOPASSWD: /usr/sbin/nft list *' \
'secubox ALL=(root) NOPASSWD: /usr/sbin/nft -j list *' \
'secubox ALL=(root) NOPASSWD: /usr/bin/systemctl --no-block start secubox-nft-cache.service' \
> debian/secubox-hub/etc/sudoers.d/secubox-hub-nft
chmod 0440 debian/secubox-hub/etc/sudoers.d/secubox-hub-nft
@@ -0,0 +1,12 @@
[Unit]
Description=SecuBox nftables cache populator (powers SOC firewall_summary)
Documentation=https://secubox.in/docs/soc
After=nftables.service
ConditionPathExists=/usr/sbin/nft
[Service]
Type=oneshot
ExecStart=/usr/sbin/secubox-nft-cache
# Runs as root via systemd — nft list requires CAP_NET_ADMIN
StandardOutput=journal
StandardError=journal
@@ -0,0 +1,12 @@
[Unit]
Description=SecuBox nftables cache populator timer (every 30s)
Documentation=https://secubox.in/docs/soc
[Timer]
OnBootSec=10s
OnUnitActiveSec=30s
AccuracySec=5s
Unit=secubox-nft-cache.service
[Install]
WantedBy=timers.target
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# SecuBox nftables cache populator
# Feeds /var/cache/secubox/nft-* files consumed by
# /api/v1/hub/public/firewall_summary. The endpoint falls back to
# realtime `nft list` (via sudo) when the cache is missing/stale,
# so this populator is a SPEED OPTIMISATION — not a source of truth.
set -e
mkdir -p /var/cache/secubox
chmod 755 /var/cache/secubox
nft -j list ruleset > /var/cache/secubox/nft-ruleset.json.new 2>/dev/null \
&& mv /var/cache/secubox/nft-ruleset.json.new /var/cache/secubox/nft-ruleset.json
nft list counters > /var/cache/secubox/nft-counters.txt.new 2>/dev/null \
&& mv /var/cache/secubox/nft-counters.txt.new /var/cache/secubox/nft-counters.txt
date -Iseconds > /var/cache/secubox/nft-cache.lastrun