17 KiB
Deployment Guide
This guide walks through deploying Vacuum Wall on a real appliance or server. Vacuum Wall is an SSL proxy firewall appliance that combines edge proxying, firewall management, DHCP, DNS, and WireGuard in a single device.
Prerequisites
- OS: Clean Debian 13 (Trixie) system. Also works on Debian 12 with backports for firewalld.
- git: Required for cloning the repository.
- Access: Root access to the machine.
- Networking:
- One public-facing network interface (external/edge). This receives inbound traffic and serves the management UI.
- At least one LAN network interface (internal). This connects to your downstream network and will serve DHCP/DNS.
- DNS: A DNS record pointing to the appliance's public IP for the management domain (e.g.,
wall.example.com). - Minimum hardware: 1 CPU, 512 MB RAM, 4 GB disk.
Installation
Download the Vacuum Wall repository onto the target machine, then run the installer with required settings. All options accept both CLI flags and environment variables (CLI takes precedence).
# Production: all env vars
MGMT_DOMAIN=wall.example.com \
MGMT_PASS="strongpassword" \
ACME_EMAIL="admin@example.com" \
./install.sh --user vacuum-wall
# Dev mode: CLI flags, auto-detects repo owner
./install.sh --dev --mgmt-pass strongpassword --acme-email "admin@example.com"
# mDNS (LAN-only, no DNS record needed)
./install.sh --mgmt-domain vacuum-wall.local --mgmt-pass strongpass --acme-email "me@example.com"
Options
All settings that can be passed as an environment variable also have a CLI flag equivalent. CLI flags take precedence over environment variables.
| Flag | Env Var | Required | Description |
|---|---|---|---|
| -- | MGMT_DOMAIN |
No | Domain for the management WebUI. Defaults to $hostname.local (mDNS). Auto-detected from system hostname. Errors if hostname is undetectable and this is not set. |
--mgmt-domain |
MGMT_DOMAIN |
No | (same as above) |
--mgmt-pass |
MGMT_PASS |
Yes | Password for HTTP basic auth protecting the WebUI. |
--mgmt-user |
MGMT_USER |
No | Username for WebUI access. Defaults to admin. |
--acme-email |
ACME_EMAIL |
Yes | Email for ACME provider (ZeroSSL by default). |
--user, -u |
USER_NAME |
Yes* | WebUI service user (created if it does not exist). Required for non-dev mode. In --dev mode, auto-detected from repo owner. |
--path, -p |
INSTALL_DIR |
No | Install directory. Defaults to repo root. Set to deploy from a custom path (e.g., /opt/vacuum-wall). |
--dev |
-- | No | Development mode: auto-detects repo owner as service user, skips safety warning. |
--wan-iface |
WAN_IFACE |
No | WAN interface name. Auto-detected from default gateway. |
--lan-ifaces |
LAN_IFACES |
No | LAN interface names, comma-separated. Auto-detected from non-loopback, non-WAN interfaces. |
--force-venv |
— | No | Force recreation of the Python virtual environment. |
Run ./install.sh --help for full usage.
Dev Mode
The --dev flag is designed for developers working in a git clone. It auto-detects the repo owner and uses that user as the WebUI service user.
Ownership Model
In dev mode, the ownership model preserves the developer's ability to work with the repository:
- Project directory: Owned by the repo owner (e.g.,
wall), group is the repo owner's primary group (e.g.,wall). The developer retains full control —git add,git commit, editing code and config files all work normally. - Daemon access: The daemon user (
vacuum-walld) has the repo owner's primary group as its own primary group, granting read access to all project files. The project directory has the setgid bit (g+s) on all subdirectories, ensuring new files inherit the group. .venv/anddata/: Owned by the repo owner, group is the repo owner's primary group. The developer can runpip install, inspect logs, and manage runtime artifacts. The daemon reads.venv/(Python interpreter) and writes todata/(runtime files) via group permissions.- Daemon socket (
data/daemon.sock): Owned byvacuum-walld:<group>(mode0660). The repo owner accesses it via primary group membership.
Running the Installer in Dev Mode
./install.sh --dev --mgmt-pass strongpassword --acme-email "dev@example.com"
The script detects the repo owner (e.g., wall), creates the vacuum-walld daemon user with the repo owner's primary group, and sets up the ownership model described above.
Idempotent Re-Runs
Running --dev again is safe. The ownership is idempotent (chown -R to the same owner), supplementary group membership is deduplicated by the OS, and setgid bits are applied recursively each time.
Container / Custom Deployment
You can deploy Vacuum Wall in a container or at any custom path. Use --path (or INSTALL_DIR) for the mount or bind path, and --user (or USER_NAME) for whatever system user exists:
# Docker volume mount example
./install.sh --path /app/vacuum-wall --user ww-app \
--mgmt-domain proxy.internal --mgmt-pass strongpassword \
--acme-email "admin@example.com"
The systemd service unit files and sudoers whitelist are rendered from Jinja2 templates at install time, substituting USER_NAME and INSTALL_DIR. This means no hardcoded paths remain after installation.
What install.sh Does
The installer performs the following steps automatically:
- Package installation: Installs firewalld, nginx, dnsmasq, avahi-daemon, wireguard-tools, python3, python3-pip, jq, curl, iptables, nftables, and apache2-utils.
- WebUI user creation: Creates the WebUI user (from
--user) as a system user if it does not exist. - Shared group: Uses the WebUI user's primary group as the shared group between both service users.
- Daemon user creation: Creates
vacuum-walld(derived from WebUI user name) — a system user withNOPASSWDsudo access for privileged operations. Owns the project directory and daemon socket. - Python venv: Creates the Python virtual environment and installs project dependencies. Skips if already present (use
--force-venvto recreate). - acme.sh installation: Copies the vendored acme.sh client to the data directory for ACME certificate management. Skips if already installed.
- Directory setup: Creates config directories under
config/for each subsystem's declarative JSON, and data directories underdata/for generated files (nginx sites, dnsmasq fragments, firewall backup, WireGuard config). - Template rendering: Renders system template files (
systemd/*.service,sudoers.d/) via Jinja2, substitutingUSER_NAME,INSTALL_DIR, andACME_HOME. Installed systemd and sudoers files contain no hardcoded values. - Sudoers whitelist: Installs a restrictive sudoers file at
/etc/sudoers.d/vacuum-walldgranting the daemon userNOPASSWDsudo for only the specific privileged commands needed for firewall, nginx, dnsmasq, and acme.sh management. Validates syntax withvisudo -cf. - IP forwarding: Enables
net.ipv4.ip_forward=1in sysctl.conf and applies it at runtime, required for routing traffic between zones. Appends only if not already present. - Firewalld initialization: Starts and enables firewalld. Opens HTTP, HTTPS, and SSH services on the public zone for management access.
- Dnsmasq initialization: Starts and enables dnsmasq for future DHCP/DNS serving on internal interfaces.
- mDNS broadcast: Enables and starts avahi-daemon so the appliance advertises its hostname (
<hostname>.local) on the local network. - Self-signed certificate: Generates a temporary self-signed X.509 certificate for the management domain with the correct CN and SAN, placed where acme.sh would store a real cert. Skips if a certificate already exists (preserves real ACME certs).
- Management proxy configuration: Configures nginx as a reverse proxy that forward-proxies to the WebUI at
127.0.0.1:9090, with HTTP-to-HTTPS redirect, basic auth, and WebSocket upgrade support. - Credentials: Generates an htpasswd file using
apache2-utils(with a Python fallback) for the management proxy's basic auth. Updates existing file if already present. - Initial nginx config: Writes
$PROJECT_DIR/config/nginx/config.jsonwith the management domain and auth settings pre-configured. Skips if the file already exists (preserves user-customized config). - Initial firewall config: Writes
$PROJECT_DIR/config/firewall/config.jsonwith auto-detected WAN/LAN interfaces. Skips if the file already exists. - Systemd units: Installs four units (rendered from Jinja2 templates):
vacuum-walld.service— the privileged background daemon (aiohttp, daemon socket).vacuum-wall.service— the Flask WebUI backend.vacuum-wall-acme.service— the certificate renewal oneshot.vacuum-wall-acme.timer— periodic timer that triggers cert renewals.
- Firewalld zones: Creates initial zones:
internal— trusted LAN zone with DHCP, DNS, and NTP services allowed.vpn— WireGuard tunnel zone.
- Service startup: Enables and starts/restarts nginx, the daemon (
vacuum-walld), the WebUI (vacuum-wall), and the ACME renewal timer. nginx is reloaded (or restarted) to pick up any config changes. - ACME registration: Registers the ACME account with the provided email via acme.sh.
Idempotent Re-Runs
install.sh is fully idempotent and safe to run multiple times. Re-running the script:
- Skips the Python venv (use
--force-venvto rebuild) - Restarts
vacuum-walld,vacuum-wall, and reloadsnginxto pick up changes - Preserves existing SSL certificates (skips self-signed generation if a cert exists)
- Preserves existing
config.jsonfiles (skips initial write if file exists) - Safely updates
htpasswd(uses update mode instead of create mode)
This makes it safe for development workflows: simply run bash install.sh again to update an existing installation.
Post-Installation
Verify Services
After the installer completes, confirm all services are running:
systemctl status vacuum-walld vacuum-wall nginx firewalld dnsmasq
Each should be active (running). The vacuum-wall-acme.timer should also be active (waiting).
Access the WebUI
Open a browser and navigate to:
https://wall.example.com
Log in with the username and password you provided during installation.
Certificate Note
The initial certificate is self-signed and generated during installation. Your browser will show a security warning. This is expected. Once DNS is pointing to the appliance and port 80 is accessible from the internet, use the Certs tab in the WebUI to issue a real ACME certificate for the management domain. After issuance, go to the Proxy tab and click Apply to reload nginx with the new cert.
Configuring Your First Network
After installation, the appliance has no interfaces assigned to zones and no DHCP ranges configured. Use the WebUI to set up your LAN.
1. Assign a LAN Interface to the Internal Zone
- Navigate to the Interfaces tab.
- From the interface list, select your LAN interface (e.g.,
eth1). - Assign it to the
internalzone. - Click Apply to update the firewall configuration.
2. Enable NAT/Masquerade
- Go to the NAT tab.
- Enable masquerade on the
internalzone. This allows devices on your LAN to reach the internet through the appliance's external interface. - Click Apply.
3. Configure DHCP
- Go to the DHCP tab.
- Click Add Range.
- Specify:
- Address range: e.g.,
192.168.2.100-192.168.2.200 - Lease time: e.g.,
12h - Interface:
eth1(or whichever interface you assigned to internal)
- Address range: e.g.,
- Click Apply. This writes the dnsmasq configuration and reloads the service.
DNS resolution will also be provided on this interface by dnsmasq, which forwards queries upstream.
Adding a Proxy Domain
Vacuum Wall's primary function is proxying incoming HTTPS traffic to internal backend services.
1. Add the Domain
- Navigate to the Proxy tab.
- Click Add Domain.
- Fill in:
- Domain: The public domain name (e.g.,
app.example.com). - Backend Host: The internal IP address of the service (e.g.,
192.168.2.50). - Backend Port: The port the service listens on (e.g.,
8080).
- Domain: The public domain name (e.g.,
2. Issue a Certificate
- Go to the Certs tab.
- Click Issue Certificate and enter the domain name.
- ACME validation requires that port 80 on the appliance is reachable from the internet and that the domain's DNS A record points to the appliance's public IP.
3. Reload Nginx
- Return to the Proxy tab.
- Click Apply to write the nginx configuration and reload the service.
The proxied domain is now accessible via HTTPS at the configured domain name.
Setting up WireGuard
Vacuum Wall includes integrated WireGuard server support for VPN access.
1. Initialize the Server
- Navigate to the WireGuard tab.
- Click Initialize. This generates the server's private and public keys and creates the
wg0interface configuration.
2. Add a Peer
- Click Add Peer.
- Enter a peer name (e.g.,
alice). - Optionally set a specific AllowedIPs range for this peer (defaults to
0.0.0.0/0). - Optionally set an Endpoint if you know the peer's static public IP (restricts incoming connections to that IP).
- Click Add. The peer's public key and preshared key are generated automatically.
3. Activate the Tunnel
- Click Apply to write the WireGuard configuration and bring up the
wg0interface.
4. Download Client Configuration
- In the peer list, use the peer actions menu to download the client configuration file for the peer.
- Install this configuration on the client device.
5. Assign WireGuard to a Firewall Zone
- Navigate to the Interfaces tab.
- Assign
wg0to thevpnzone. - The
vpnzone allows all traffic by default (target ACCEPT). Adjust firewall rules as needed to restrict VPN access to specific services.
6. Configure Firewall Rules for VPN Traffic
- Go to the Firewall tab or use the NAT tab.
- Add rules as needed to control what VPN peers can access. For example, you can restrict VPN peers to only reach specific internal services rather than the entire LAN.
- Optionally enable masquerade on the
vpnzone to allow VPN clients to reach the internet through the appliance.
Troubleshooting
Services Won't Start
Check service logs and configuration:
journalctl -u vacuum-walld --no-pager -n 50
journalctl -u vacuum-wall --no-pager -n 50
nginx -t
Common causes include port conflicts (another service on port 80/443), missing dependencies, or file permission issues on data/.
Firewall Rules Not Applying
Verify that firewalld is running:
firewall-cmd --state
systemctl status firewalld
If firewalld is not running, start it with systemctl start firewalld. Check that the sudoers whitelist is valid:
visudo -cf /etc/sudoers.d/vacuum-walld
Certificate Issuance Fails
ACME validation via the ACME provider requires:
- The domain's DNS A record points to the appliance's public IP.
- Port 80 (HTTP-01 challenge) is accessible from the internet on the external interface.
- The ACME email was registered correctly. Check with:
su -s /bin/bash "$USER_DAEMON_NAME" -c "~/data/acme/acme.sh --list"
If port 80 is blocked or the DNS record hasn't propagated yet, wait and retry. The ACME timer will also attempt renewal automatically.
DHCP Not Working
Verify that:
- The LAN interface is assigned to a firewalld zone (check the Interfaces tab or
firewall-cmd --get-active-zones). - Dnsmasq is running:
systemctl status dnsmasq. - A DHCP range is configured for the correct interface. Check dnsmasq config at
data/dnsmasq/. - The firewall allows DHCP traffic on the internal zone:
firewall-cmd --zone=internal --list-servicesshould includedhcpanddns.
WebUI Not Accessible
- Verify nginx is running:
systemctl status nginx. - Test nginx configuration:
nginx -t. - Check the management proxy configuration at
/etc/nginx/conf.d/vacuum-wall-mgmt.conf(initial) or via the WebUI Proxy tab (after first apply). - Ensure the WebUI service is listening on port 9090:
ss -tlnp | grep 9090. - If using the self-signed cert, confirm your browser trusts it or use the WebUI to issue a real ACME certificate.
Reference
| Component | Service | Config Location |
|---|---|---|
| Daemon (privileged) | vacuum-walld.service |
daemon/ |
| WebUI backend | vacuum-wall.service |
webui/ |
| Reverse proxy | nginx |
/etc/nginx/conf.d/vacuum-wall-mgmt.conf |
| Firewall | firewalld |
Managed via WebUI and firewall-cmd |
| DHCP/DNS | dnsmasq |
config/dnsmasq/ |
| VPN | wireguard-tools | config/wireguard/ |
| Certificates | vacuum-wall-acme.timer |
$PROJECT_DIR/data/acme/ |
| Sudoers (daemon) | — | /etc/sudoers.d/vacuum-walld |