fb39af126a
- Update AGENTS.md, README.md, and docs/* with revisions - Refactor lib/acme.py and lib/state.py - Add tests for acme module - Remove install.sh and restart-services.sh (moved to scripts/) - Normalize vendor files (acme.sh, htm.js)
345 lines
16 KiB
Markdown
345 lines
16 KiB
Markdown
# 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).
|
|
|
|
```bash
|
|
# Production: all env vars
|
|
MGMT_DOMAIN=wall.example.com \
|
|
MGMT_PASS="strongpassword" \
|
|
./scripts/install.sh --user vacuum-wall
|
|
|
|
# Dev mode: CLI flags, auto-detects repo owner
|
|
./scripts/install.sh --dev --mgmt-pass strongpassword
|
|
|
|
# mDNS (LAN-only, no DNS record needed)
|
|
./scripts/install.sh --mgmt-domain vacuum-wall.local --mgmt-pass strongpass
|
|
```
|
|
|
|
### 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`. |
|
|
| `--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 `./scripts/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/` and `data/`**: Owned by the repo owner, group is the repo owner's primary group. The developer can run `pip install`, inspect logs, and manage runtime artifacts. The daemon reads `.venv/` (Python interpreter) and writes to `data/` (runtime files) via group permissions.
|
|
- **Daemon socket** (`data/daemon.sock`): Owned by `vacuum-walld:<group>` (mode `0660`). The repo owner accesses it via primary group membership.
|
|
|
|
### Running the Installer in Dev Mode
|
|
|
|
```bash
|
|
./scripts/install.sh --dev --mgmt-pass strongpassword
|
|
```
|
|
|
|
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:
|
|
|
|
```bash
|
|
# Docker volume mount example
|
|
./scripts/install.sh --path /app/vacuum-wall --user ww-app \
|
|
--mgmt-domain proxy.internal --mgmt-pass strongpassword
|
|
```
|
|
|
|
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 scripts/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 with `NOPASSWD` sudo 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-venv` to 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 under `data/` for generated files (nginx sites, dnsmasq fragments, firewall backup, WireGuard config).
|
|
- **Template rendering**: Renders system template files (`systemd/*.service`, `sudoers.d/`) via Jinja2, substituting `USER_NAME`, `INSTALL_DIR`, and `ACME_HOME`. Installed systemd and sudoers files contain no hardcoded values.
|
|
- **Sudoers whitelist**: Installs a restrictive sudoers file at `/etc/sudoers.d/vacuum-walld` granting the daemon user `NOPASSWD` sudo for only the specific privileged commands needed for firewall, nginx, dnsmasq, and acme.sh management. Validates syntax with `visudo -cf`.
|
|
- **IP forwarding**: Enables `net.ipv4.ip_forward=1` in 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**: Calls the daemon API (`POST_NGINX_DOMAINS_ADD`) to register the management domain as a regular proxy entry with paths-based config (`/` → Flask, `/ws` → WebSocket). Then applies nginx via `POST_NGINX_APPLY`.
|
|
- **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 configs**: Firewall config and nginx proxy config are written via daemon API (skips if 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 account**: No account registration during install. Register the account via the WebUI after first login.
|
|
|
|
### Idempotent Re-Runs
|
|
|
|
`scripts/install.sh` is fully idempotent and safe to run multiple times. Re-running the script:
|
|
|
|
- Skips the Python venv (use `--force-venv` to rebuild)
|
|
- Restarts `vacuum-walld`, `vacuum-wall`, and reloads `nginx` to pick up changes
|
|
- Preserves existing SSL certificates (skips self-signed generation if a cert exists)
|
|
- Preserves existing `config.json` files (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 scripts/install.sh` again to update an existing installation.
|
|
|
|
---
|
|
|
|
## Post-Installation
|
|
|
|
### Verify Services
|
|
|
|
After the installer completes, confirm all services are running:
|
|
|
|
```bash
|
|
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
|
|
|
|
1. Navigate to the **Interfaces** tab.
|
|
2. From the interface list, select your LAN interface (e.g., `eth1`).
|
|
3. Assign it to the `internal` zone.
|
|
4. Click **Apply** to update the firewall configuration.
|
|
|
|
### 2. Enable NAT/Masquerade
|
|
|
|
1. Go to the **NAT** tab.
|
|
2. Enable masquerade on the `internal` zone. This allows devices on your LAN to reach the internet through the appliance's external interface.
|
|
3. Click **Apply**.
|
|
|
|
### 3. Configure DHCP
|
|
|
|
1. Go to the **DHCP** tab.
|
|
2. Click **Add Range**.
|
|
3. 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)
|
|
4. 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
|
|
|
|
1. Navigate to the **Proxy** tab.
|
|
2. Click **Add Domain**.
|
|
3. 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`).
|
|
|
|
### 2. Issue a Certificate
|
|
|
|
1. Go to the **Certs** tab.
|
|
2. Click **Issue Certificate** and enter the domain name.
|
|
3. 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
|
|
|
|
1. Return to the **Proxy** tab.
|
|
2. 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
|
|
|
|
1. Navigate to the **WireGuard** tab.
|
|
2. Click **Initialize**. This generates the server's private and public keys and creates the `wg0` interface configuration.
|
|
|
|
### 2. Add a Peer
|
|
|
|
1. Click **Add Peer**.
|
|
2. Enter a peer name (e.g., `alice`).
|
|
3. Optionally set a specific AllowedIPs range for this peer (defaults to `0.0.0.0/0`).
|
|
4. Optionally set an **Endpoint** if you know the peer's static public IP (restricts incoming connections to that IP).
|
|
5. Click **Add**. The peer's public key and preshared key are generated automatically.
|
|
|
|
### 3. Activate the Tunnel
|
|
|
|
1. Click **Apply** to write the WireGuard configuration and bring up the `wg0` interface.
|
|
|
|
### 4. Download Client Configuration
|
|
|
|
1. In the peer list, use the peer actions menu to download the client configuration file for the peer.
|
|
2. Install this configuration on the client device.
|
|
|
|
### 5. Assign WireGuard to a Firewall Zone
|
|
|
|
1. Navigate to the **Interfaces** tab.
|
|
2. Assign `wg0` to the `vpn` zone.
|
|
3. The `vpn` zone 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
|
|
|
|
1. Go to the **Firewall** tab or use the **NAT** tab.
|
|
2. 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.
|
|
3. Optionally enable masquerade on the `vpn` zone to allow VPN clients to reach the internet through the appliance.
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Services Won't Start
|
|
|
|
Check service logs and configuration:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
firewall-cmd --state
|
|
systemctl status firewalld
|
|
```
|
|
|
|
If firewalld is not running, start it with `systemctl start firewalld`. Check that the sudoers whitelist is valid:
|
|
|
|
```bash
|
|
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.
|
|
- An ACME account is registered (check the Certs page in the WebUI). Verify with:
|
|
|
|
```bash
|
|
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-services` should include `dhcp` and `dns`.
|
|
|
|
### WebUI Not Accessible
|
|
|
|
1. Verify nginx is running: `systemctl status nginx`.
|
|
2. Test nginx configuration: `nginx -t`.
|
|
3. Check the management proxy domain configuration via the WebUI Proxy tab, or by inspecting `config/nginx/config.json`.
|
|
4. Ensure the WebUI service is listening on port 9090: `ss -tlnp | grep 9090`.
|
|
5. 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` | `config/nginx/config.json` (via daemon) |
|
|
| 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` | |