mteehan 75b86fd60d fix: ACME ownership self-heal + daily timer, apply-all force, firewall baseline re-stamp
acme:
- acme.sh chmods its tree to owner-only (700/600) every run, which
  broke the two-user model: a tree left owner-only by one user made
  every acme.sh call of the other exit 2
- normalize_acme_home() reopens group access (sudo chmod g+rwX,
  files only — setgid dirs trip RestrictSUIDSGID); _run_acme_preflight
  is the choke point before every daemon acme.sh call + startup
- acme service now runs as the daemon user; --log persists the raw CA
  transcript; SYS_LOG=6 journals manual issue/renew runs
- timer daily-only: two runs/day landed inside ZeroSSL's 24h
  validation backoff (Retry-After: 86400) — a permanent renewal lockout
- _collect_acme no longer raises on cert-list failure; reports
  status.error (AcmeState.status) so the certs page can surface it

firewall: re-stamp the applied baseline on live zone mutations
(interfaces/services/rich-rules/masquerade/forward-ports) so cancel-all
reverts to post-mutation state, not a stale install-era snapshot;
set_masquerade syncs the declarative config for existing zones;
add_forward_port records toaddr only with toport

status: apply-all accepts {"force": true} (forwarded to the firewall
apply only); ApplyConfirm force checkbox; applyResultToasts() — the
errors map wins over the 200; ActionButton checks errors before the
success toast; dashboard uses ApplyConfirm

system_import: drift re-imports carry the existing apply-meta; first
import stamps the adopted content as applied (it is the running state)
— no phantom pending changes

nginx: get_config only re-saves when migration actually changed the
config (no more owner/mtime churn on every read)

install: repair mis-owned top-level system dirs (tmpfiles
unsafe-path-transition), warn with a full-repair command for deeper
mis-ownership

daemon/server: loop.get_exception_handler() (aiohttp API fix)

tests: 888 pytest + 24 node passing; ruff clean
2026-09-01 02:35:04 +00:00

Vacuum Wall

A zone-based firewall appliance with a built-in SSL reverse proxy. Combines firewalld policy control, DHCP/DNS, WireGuard VPN, and automated certificate provisioning into a single device managed through one web UI.

Tech Stack

  • Debian 13 (trixie) target platform
  • Python 3.13+, Flask 3.x web UI
  • firewalld (nftables backend), dnsmasq, nginx, WireGuard
  • acme.sh for ACME certificates (ZeroSSL)

For Operators

Prerequisites

  • Clean Debian 13 system with root access
  • git installed
  • One public-facing NIC (external) and at least one LAN NIC (internal)
  • A DNS A record pointing to the appliance's public IP for the management domain
  • Minimum hardware: 1 CPU, 512 MB RAM, 4 GB disk

Install (Production)

MGMT_DOMAIN=wall.example.com \
MGMT_PASS="strongpassword" \
MGMT_USER="admin" \
ACME_EMAIL="admin@example.com" \
bash scripts/install.sh

Install (Development)

./scripts/install.sh --dev --mgmt-pass strongpassword --acme-email "admin@example.com"

--dev auto-detects the repo's file owner as the service user, skips the safety warning about running as a regular user, and keeps file ownership dev-friendly.

Flag Env Var Required Description
-- MGMT_DOMAIN No Public domain for the management WebUI (auto-detected as hostname.local)
--mgmt-pass MGMT_PASS Yes HTTP basic auth password for the WebUI
--mgmt-user MGMT_USER No WebUI username (defaults to admin)
--acme-email ACME_EMAIL Yes ACME registration email (ZeroSSL by default)
--user, -u USER_NAME No System user for service (default: vacuum-wall)
--path, -p INSTALL_DIR No Install directory (default: repo root)
--dev -- No Auto-detect repo owner as service user, skip safety warning
--mgmt-domain MGMT_DOMAIN No (alias for env var)
--wan-iface WAN_IFACE No WAN interface (auto-detected)
--lan-ifaces LAN_IFACES No LAN interfaces, comma-separated (auto-detected)

CLI flags take precedence over environment variables. Run ./scripts/install.sh --help for full usage.

After installation, access the WebUI at https://<MGMT_DOMAIN>. The initial certificate is self-signed — use the Certs tab to issue a real one once DNS is propagating.

Next Steps

  1. Assign interfaces to zones from the Interfaces tab
  2. Configure DHCP ranges for your LAN
  3. Add proxy domains with ACME certificates
  4. Set up WireGuard (optional)

See docs/deployment.md for the full guide, including troubleshooting.


For Developers

Local Development

git clone <repo-url> && cd vacuum-wall
bash scripts/update-vendor.sh
python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"

Start the WebUI locally (binds to 127.0.0.1:9090):

.venv/bin/python webui/server.py

Lint and Format

.venv/bin/ruff check lib/ webui/ tests/
.venv/bin/ruff format lib/ webui/ tests/

All lib/ modules share lib.common utilities (run, run_proc, load_json, save_json, deep_merge, ensure_dirs) and have full type hints and __all__ exports. API blueprints share _ok/_error from webui.api.common.

Tests

.venv/bin/python -m pytest tests/ -v

Tests mock all subprocess calls (firewalld, acme.sh, WireGuard, nginx, dnsmasq) — no system services required. 192 tests across 5 test modules.

Documentation MCP Server

This project uses docs-mcp-server for grounded, real-time documentation lookups during development.

pipx install docs-mcp-server
docs-mcp init

Then run with Claude Code or Opencode to activate it. It automatically checks live docs before answering technical questions — no manual doc search needed.

Architecture

Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
Flask ──→ lib/*.py ──→ sudo <cmd> ──→ system service
Blueprint URL prefix Backend module
webui/api/firewall /api/firewall/ lib.firewall
webui/api/dhcp /api/dhcp/ lib.dnsmasq
webui/api/proxy /api/proxy/ lib.nginx
webui/api/certs /api/certs/ lib.acme
webui/api/wireguard /api/wireguard/ lib.wireguard

See docs/architecture.md for detailed request flow, zone model, and shared utility patterns.


Documentation

S
Description
No description provided
Readme 2.8 MiB
Languages
Python 73.1%
JavaScript 24%
Shell 1.6%
CSS 1.3%