mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-08-16 22:37:12 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1c60d6be6 | ||
|
|
40b5837262 | ||
|
|
27f9cd9db0 | ||
|
|
efd6a462be |
@@ -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
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Executable
+14
@@ -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
|
||||
Reference in New Issue
Block a user