Update documentation for config/ migration, new install model, and firewall config API
Reflect config/ vs data/ split, install-time Jinja2 templating, auto-discovered paths, ZeroSSL/ACME support, and the new firewall save-then-apply API in all reference docs.
This commit is contained in:
+4
-4
@@ -1,6 +1,6 @@
|
||||
# REST API Reference
|
||||
|
||||
All endpoints are served by the management WebUI Flask application bound to `127.0.0.1:9090`, proxied through nginx with SSL termination and HTTP basic authentication. Requests target the management domain (e.g., `https://wall.lan/api/...`).
|
||||
All endpoints are served by the management WebUI Flask application bound to `127.0.0.1:9090`, proxied through nginx with SSL termination and HTTP basic authentication. Requests target the management domain (e.g., `https://<hostname>.local/api/...`).
|
||||
|
||||
Every request and response uses `Content-Type: application/json`.
|
||||
|
||||
@@ -672,7 +672,7 @@ Configure the nginx proxy block for the management WebUI itself, including optio
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `domain` | `string` | Yes | Management domain (e.g., `"wall.lan"`) |
|
||||
| `domain` | `string` | Yes | Management domain (e.g., `"myhost.local"`) |
|
||||
| `flask_host` | `string` | No | Flask app bind host; defaults to `"127.0.0.1"` |
|
||||
| `flask_port` | `number` | No | Flask app bind port; defaults to `9090` |
|
||||
| `auth_user` | `string` | No | Username for basic auth. An `.htpasswd` entry is created when this field is present. |
|
||||
@@ -686,7 +686,7 @@ If `auth_user` and `auth_pass` are provided, the endpoint creates or updates the
|
||||
|
||||
## Certificate API
|
||||
|
||||
Endpoints prefixed with `/api/certs/...`. Manage TLS certificates via ACME (Let's Encrypt / certbot).
|
||||
Endpoints prefixed with `/api/certs/...`. Manage TLS certificates via ACME (ZeroSSL, Let's Encrypt, etc.).
|
||||
|
||||
### Listing & Details
|
||||
|
||||
@@ -793,7 +793,7 @@ Returns HTTP `404` if the certificate is not found.
|
||||
POST /api/certs/email
|
||||
```
|
||||
|
||||
Set or update the ACME account contact email (used by Let's Encrypt for expiration and security notices).
|
||||
Set or update the ACME account contact email (used by the CA for expiration and security notices).
|
||||
|
||||
**Request Body:**
|
||||
|
||||
|
||||
+55
-27
@@ -9,7 +9,7 @@ The following describes the path a request takes from an external client to a ba
|
||||
1. An external client sends an HTTP request to `app.example.com`.
|
||||
2. The request arrives at the Vacuum Wall host's WAN interface, assigned to the `external` firewalld zone. A firewall rule allows inbound traffic on port 443 (HTTPS).
|
||||
3. nginx, listening on port 443, terminates the TLS connection using the domain's certificate.
|
||||
4. nginx evaluates the `server_name` against the configured server blocks. The matching block is generated from the domain entry in `data/nginx/config.json`.
|
||||
4. nginx evaluates the `server_name` against the configured server blocks. The matching block is generated from the domain entry in `config/nginx/config.json`.
|
||||
5. The request is forwarded to the backend service (e.g., `192.168.2.50:8080`) via an `proxy_pass` directive.
|
||||
6. The backend service processes the request and returns an HTTP response.
|
||||
7. nginx adds security headers (`X-Content-Type-Options`, `X-Frame-Options`, HSTS, etc.) to the response.
|
||||
@@ -17,7 +17,7 @@ The following describes the path a request takes from an external client to a ba
|
||||
|
||||
For HTTP requests (port 80), nginx returns a 301 redirect to the HTTPS equivalent before any proxying occurs.
|
||||
|
||||
### Management WebUI Access (e.g., `wall.lan`)
|
||||
### Management WebUI Access (e.g., `<hostname>.local`)
|
||||
|
||||
1. A client sends an HTTPS request to the management domain.
|
||||
2. nginx terminates TLS and checks for HTTP Basic Authentication credentials against the `.htpasswd` file.
|
||||
@@ -36,42 +36,70 @@ External Client ──→ nginx (SSL termination) ──→ Flask WebUI (127.0.0
|
||||
Flask WebUI ──→ lib/firewall.py ──→ sudo firewall-cmd ──→ firewalld / D-Bus ──→ nftables
|
||||
Flask WebUI ──→ lib/nginx.py ──→ write local .conf files ──→ sudo cp to /etc/nginx/ ──→ sudo nginx -t && sudo nginx -s reload
|
||||
Flask WebUI ──→ lib/dnsmasq.py ──→ render config ──→ sudo tee /etc/dnsmasq.d/vacuum-wall.conf ──→ sudo systemctl reload dnsmasq
|
||||
Flask WebUI ──→ lib/acme.py ──→ acme.sh (no sudo, runs as vacuum-wall user) ──→ Let's Encrypt ACME
|
||||
Flask WebUI ──→ lib/wireguard.py ──→ render /home/wall/vacuum-wall/data/wireguard/wg0.conf ──→ sudo cp to /etc/wireguard/ ──→ sudo wg-quick up wg0
|
||||
Flask WebUI ──→ lib/acme.py ──→ acme.sh (no sudo, runs as service user) ──→ ZeroSSL ACME
|
||||
Flask WebUI ──→ lib/wireguard.py ──→ render $PROJECT_DIR/data/wireguard/wg0.conf ──→ sudo cp to /etc/wireguard/ ──→ sudo wg-quick up wg0
|
||||
```
|
||||
|
||||
Each `lib/` module encapsulates command construction, privilege escalation (via sudo where needed), and error handling for its subsystem. The modules read declarative configuration from `data/`, render the appropriate system configuration files, and invoke the corresponding privileged operation. Note that `lib/acme.py` runs `acme.sh` without sudo — it executes as the unprivileged `vacuum-wall` user using webroot validation rather than standalone/TLS-ALPN modes that would require elevated privileges.
|
||||
Each `lib/` module encapsulates command construction, privilege escalation (via sudo where needed), and error handling for its subsystem. The modules read declarative configuration from `config/` and runtime artifacts from `data/`, render the appropriate system configuration files, and invoke the corresponding privileged operation. Note that `lib/acme.py` runs `acme.sh` without sudo — it executes as the unprivileged service user using webroot validation rather than standalone/TLS-ALPN modes that would require elevated privileges.
|
||||
|
||||
The `lib/` modules auto-discover the project root at runtime via `Path(__file__).resolve().parent.parent`, so no hardcoded paths are needed in Python code.
|
||||
|
||||
## Install-Time Templating
|
||||
|
||||
System configuration files in `system/` are Jinja2 templates rendered by `install.sh` at install time:
|
||||
|
||||
- **`systemd/vacuum-wall.service`**, **`systemd/vacuum-wall-acme.service`** — `{{ USER_NAME }}`, `{{ PROJECT_DIR }}`, `{{ ACME_HOME }}` are substituted to produce the final systemd unit files installed to `/etc/systemd/system/`.
|
||||
- **`sudoers.d/vacuum-wall`** — `{{ USER_NAME }}` is substituted to produce the sudoers whitelist.
|
||||
- The timer file (`vacuum-wall-acme.timer`) contains no variable paths and is installed as-is.
|
||||
|
||||
Runtime templates (`system/nginx/*.conf`, `system/dnsmasq.conf`, `system/wireguard*.conf`) are rendered at runtime by `lib/` modules via Jinja2 with Python data.
|
||||
|
||||
## State Management
|
||||
|
||||
Vacuum Wall uses a declarative configuration model. The source of truth for each subsystem is a JSON file in the `data/` directory. The application renders these declarations into the format expected by the underlying system service.
|
||||
Vacuum Wall uses a declarative configuration model. Persistent user-facing configuration lives in `config/<subsystem>/config.json`. Runtime artifacts and generated files live in `data/<subsystem>/`. The application renders these declarations into the format expected by the underlying system service.
|
||||
|
||||
| Subsystem | Declarative Config | Rendered Target | State Persistence |
|
||||
|---|---|---|---|
|
||||
| firewalld | `data/firewall/rules.json` | N/A (commands issued directly to firewalld via D-Bus) | firewalld manages its own persistent state in `/etc/firewalld/`. `rules.json` serves as a declarative backup and can be used to restore firewall rules. |
|
||||
| dnsmasq | `data/dnsmasq/config.json` | `/etc/dnsmasq.d/vacuum-wall.conf` | The JSON file is the source of truth. The rendered `.conf` file is overwritten on each apply. |
|
||||
| nginx | `data/nginx/config.json` | `data/nginx/sites-enabled/<domain>.conf` + `/etc/nginx/conf.d/vacuum-wall.conf` | All proxy and management domain definitions are derived from the JSON config. Generated `.conf` files are overwritten on each apply. |
|
||||
| WireGuard | `data/wireguard/config.json` | `/etc/wireguard/wg0.conf` | The JSON file defines the interface and all peers. The rendered WireGuard config is overwritten on each apply. |
|
||||
| ACME | `~/.acme.sh/` (managed by acme.sh) | Certificate and key files | acme.sh manages its own state, renewal scheduling, and account keys. Vacuum Wall triggers issuance and renewal but does not maintain independent ACME state. |
|
||||
| Subsystem | Declarative Config | Runtime Data | Rendered Target | State Persistence |
|
||||
|---|---|---|---|---|
|
||||
| firewalld | N/A (firewalld manages own state) | `data/firewall/rules.json` | N/A (commands issued directly to firewalld via D-Bus) | firewalld manages its own persistent state in `/etc/firewalld/`. `rules.json` serves as an automated backup snapshot. |
|
||||
| dnsmasq | `config/dnsmasq/config.json` | `data/dnsmasq/fragments/` | `/etc/dnsmasq.d/vacuum-wall.conf` | The JSON file is the source of truth. The rendered `.conf` file is overwritten on each apply. |
|
||||
| nginx | `config/nginx/config.json` | `data/nginx/.htpasswd`, `data/nginx/sites-enabled/` | `data/nginx/sites-enabled/<domain>.conf` + `/etc/nginx/conf.d/vacuum-wall.conf` | All proxy and management domain definitions are derived from the JSON config. Generated `.conf` files are overwritten on each apply. |
|
||||
| WireGuard | `config/wireguard/config.json` | `data/wireguard/` | `/etc/wireguard/wg0.conf` | The JSON file defines the interface and all peers. The rendered WireGuard config is overwritten on each apply. |
|
||||
| ACME | N/A (`~/.acme.sh/` managed by acme.sh) | `data/acme/` | Certificate and key files | acme.sh manages its own state, renewal scheduling, and account keys. Vacuum Wall triggers issuance and renewal but does not maintain independent ACME state. |
|
||||
|
||||
## Data Directory Structure
|
||||
## Directory Structure
|
||||
|
||||
### Config — Declarative Settings
|
||||
|
||||
Config files are persistent, user-editable JSON that defines the desired state for each subsystem:
|
||||
|
||||
```
|
||||
data/
|
||||
├── nginx/
|
||||
│ ├── config.json # Proxy domain definitions, management domain, SSL settings
|
||||
│ ├── .htpasswd # HTTP Basic Authentication credentials for management UI
|
||||
│ └── sites-enabled/ # Generated nginx server block .conf files (one per domain)
|
||||
config/
|
||||
├── dnsmasq/
|
||||
│ ├── config.json # DHCP ranges, static leases, DNS forwarding, custom records
|
||||
│ └── fragments/ # User-defined dnsmasq config fragments (appended verbatim)
|
||||
├── firewall/
|
||||
│ └── rules.json # Declarative firewall rule state backup
|
||||
│ └── config.json # DHCP ranges, static leases, DNS forwarding, custom records
|
||||
├── nginx/
|
||||
│ └── config.json # Proxy domain definitions, management domain, SSL settings
|
||||
└── wireguard/
|
||||
└── config.json # WireGuard interface and peer configuration
|
||||
```
|
||||
|
||||
The `data/` directory resides within the `vacuum-wall` user's project directory (`/home/wall/vacuum-wall/data/`). The systemd service unit's `ReadWritePaths` directive grants the Flask process write access to this directory, while keeping the rest of the filesystem read-only.
|
||||
### Data — Runtime Artifacts
|
||||
|
||||
The `data/` directory holds generated files, credentials, and subsystem artifacts:
|
||||
|
||||
```
|
||||
data/
|
||||
├── nginx/
|
||||
│ ├── .htpasswd # HTTP Basic Authentication credentials for management UI
|
||||
│ └── sites-enabled/ # Generated nginx server block .conf files (one per domain)
|
||||
├── dnsmasq/
|
||||
│ └── fragments/ # User-defined dnsmasq config fragments (appended verbatim)
|
||||
├── firewall/
|
||||
│ └── rules.json # Auto-generated firewall rule state backup
|
||||
├── acme/ # ACME certificate files (acme.sh home)
|
||||
└── wireguard/ # WireGuard runtime artifacts
|
||||
```
|
||||
|
||||
Both `config/` and `data/` reside within the project directory (`$PROJECT_DIR/`). The systemd service unit's `ReadWritePaths` directive grants the Flask process write access to both directories, while keeping the rest of the filesystem read-only. The `PROJECT_DIR` value is templated into the service unit at install time.
|
||||
|
||||
## File System Layout
|
||||
|
||||
@@ -81,9 +109,9 @@ The following file system locations are used for integration with system service
|
||||
|---|---|---|
|
||||
| `/etc/nginx/conf.d/vacuum-wall.conf` | Include directive that pulls in `data/nginx/sites-enabled/*.conf`. | Vacuum Wall (lib/nginx.py) |
|
||||
| `/etc/nginx/snippets/vacuum-wall-ssl.conf` | Shared SSL configuration snippet (protocols, ciphers, DH parameters, OCSP). Included by all HTTPS server blocks. | Vacuum Wall (lib/nginx.py) |
|
||||
| `/etc/dnsmasq.d/vacuum-wall.conf` | Generated dnsmasq configuration file. Written from `data/dnsmasq/config.json`. | Vacuum Wall (lib/dnsmasq.py) |
|
||||
| `/etc/wireguard/wg0.conf` | Generated WireGuard interface configuration. Written from `data/wireguard/config.json`. | Vacuum Wall (lib/wireguard.py) |
|
||||
| `/etc/sudoers.d/vacuum-wall` | Sudo whitelist for the `vacuum-wall` user. Defines all permitted privilege escalations. | Install script (manual edits not required) |
|
||||
| `/etc/dnsmasq.d/vacuum-wall.conf` | Generated dnsmasq configuration file. Written from `config/dnsmasq/config.json`. | Vacuum Wall (lib/dnsmasq.py) |
|
||||
| `/etc/wireguard/wg0.conf` | Generated WireGuard interface configuration. Written from `config/wireguard/config.json`. | Vacuum Wall (lib/wireguard.py) |
|
||||
| `/etc/sudoers.d/vacuum-wall` | Sudo whitelist for the configured system user. Defines all permitted privilege escalations. | Install script (rendered from Jinja2 template) |
|
||||
|
||||
The `/etc/nginx/conf.d/vacuum-wall.conf` include file ensures that all domain-specific configurations in `sites-enabled/` are loaded by nginx without modifying the main `nginx.conf`. The SSL snippet keeps TLS settings consistent across all managed domains and allows global updates from a single location.
|
||||
|
||||
|
||||
+11
-11
@@ -1,10 +1,10 @@
|
||||
# Configuration Reference
|
||||
|
||||
This document describes the JSON configuration files used by Vacuum Wall to manage each subsystem. All configuration is stored in the `data/` directory as declarative JSON. The application renders these declarations into the format expected by each underlying service.
|
||||
This document describes the JSON configuration files used by Vacuum Wall to manage each subsystem. All persistent configuration is stored in the `config/` directory as declarative JSON. Runtime artifacts and generated files live in `data/`. The application renders these declarations into the format expected by each underlying service.
|
||||
|
||||
## DHCP/DNS Configuration
|
||||
|
||||
**File**: `data/dnsmasq/config.json`
|
||||
**File**: `config/dnsmasq/config.json`
|
||||
|
||||
This file defines all DHCP server settings and DNS resolution behavior for the dnsmasq service. The application renders it into `/etc/dnsmasq.d/vacuum-wall.conf`.
|
||||
|
||||
@@ -74,7 +74,7 @@ Additional dnsmasq directives can be appended verbatim by placing plain-text fil
|
||||
|
||||
## Nginx Configuration
|
||||
|
||||
**File**: `data/nginx/config.json`
|
||||
**File**: `config/nginx/config.json`
|
||||
|
||||
This file defines reverse proxy domains, the management interface, and global SSL settings. The application renders it into per-domain server block files in `data/nginx/sites-enabled/` and into the shared SSL snippet at `/etc/nginx/snippets/vacuum-wall-ssl.conf`.
|
||||
|
||||
@@ -99,7 +99,7 @@ This file defines reverse proxy domains, the management interface, and global SS
|
||||
}
|
||||
},
|
||||
"management": {
|
||||
"domain": "wall.lan",
|
||||
"domain": "<hostname>.local",
|
||||
"backend": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9090,
|
||||
@@ -107,7 +107,7 @@ This file defines reverse proxy domains, the management interface, and global SS
|
||||
},
|
||||
"auth": {
|
||||
"user": "admin",
|
||||
"htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
|
||||
"htpasswd": "$PROJECT_DIR/data/nginx/.htpasswd"
|
||||
}
|
||||
},
|
||||
"ssl": {
|
||||
@@ -132,7 +132,7 @@ The `domains` object maps domain names (keys) to proxy configurations. Each entr
|
||||
| `headers` | object | No | Custom headers to set on proxied requests. Supports nginx variable interpolation (e.g., `$remote_addr`). |
|
||||
| `cert` | object | No | Certificate configuration for this domain. Required unless the management domain shares its cert. |
|
||||
| `cert.type` | string | Yes (if `cert`) | Certificate provisioning method. One of: `acme`, `file`, or `selfsigned`. |
|
||||
| `cert.email` | string | Yes (if `acme`) | ACME account email used by Let's Encrypt. |
|
||||
| `cert.email` | string | Yes (if `acme`) | ACME account email used by the CA provider. |
|
||||
| `cert.path` | string | Yes (if `file`) | Full path to the public certificate file (PEM). |
|
||||
| `cert.key_path` | string | Yes (if `file`) | Full path to the private key file (PEM). |
|
||||
|
||||
@@ -140,9 +140,9 @@ The `domains` object maps domain names (keys) to proxy configurations. Each entr
|
||||
|
||||
| Type | Description |
|
||||
|---|---|
|
||||
| `acme` | Vacuum Wall uses acme.sh to request and renew a Let's Encrypt certificate via the HTTP-01 challenge. The nginx configuration is temporarily modified to serve the ACME challenge files at `/.well-known/acme-challenge/`. The `email` field is required. |
|
||||
| `acme` | Vacuum Wall uses acme.sh to request and renew an ACME certificate via the HTTP-01 challenge. The nginx configuration is temporarily modified to serve the ACME challenge files at `/.well-known/acme-challenge/`. The `email` field is required. |
|
||||
| `file` | Use a pre-existing certificate and private key from the local file system. The `path` and `key_path` fields must point to readable PEM files. Vacuum Wall will not attempt to renew these certificates. |
|
||||
| `selfsigned` | Vacuum Wall generates a self-signed certificate and private key on first apply. Useful for internal domains or testing. The generated certificate is stored alongside other acme-managed files in `~/.acme.sh/` with a `.selfsigned` marker. |
|
||||
| `selfsigned` | Vacuum Wall generates a self-signed certificate and private key on first apply. Useful for internal domains or testing. The generated certificate is stored at `$PROJECT_DIR/data/certs/`. |
|
||||
|
||||
### Management Domain
|
||||
|
||||
@@ -150,7 +150,7 @@ The `management` block configures the Vacuum Wall admin interface itself. It fol
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `domain` | string | Yes | The hostname used to access the management WebUI (e.g., `wall.lan`). |
|
||||
| `domain` | string | Yes | The hostname used to access the management WebUI (e.g., `<hostname>.local`). |
|
||||
| `backend` | object | Yes | Points to the Flask app at `127.0.0.1:9090`. |
|
||||
| `auth` | object | Yes | HTTP Basic Authentication configuration. |
|
||||
| `auth.user` | string | Yes | Username for the `.htpasswd` file. |
|
||||
@@ -159,7 +159,7 @@ The `management` block configures the Vacuum Wall admin interface itself. It fol
|
||||
The `.htpasswd` file can be created with the `htpasswd` utility:
|
||||
|
||||
```bash
|
||||
htpasswd -bc /home/wall/vacuum-wall/data/nginx/.htpasswd admin yourpassword
|
||||
htpasswd -bc $PROJECT_DIR/data/nginx/.htpasswd admin yourpassword
|
||||
```
|
||||
|
||||
### Global SSL Settings
|
||||
@@ -174,7 +174,7 @@ The `ssl` block defines TLS parameters applied to all HTTPS server blocks via th
|
||||
|
||||
## WireGuard Configuration
|
||||
|
||||
**File**: `data/wireguard/config.json`
|
||||
**File**: `config/wireguard/config.json`
|
||||
|
||||
This file defines the WireGuard server interface and all connected peers. The application renders it into `/etc/wireguard/wg0.conf` and applies it with `wg-quick`.
|
||||
|
||||
|
||||
+58
-25
@@ -1,4 +1,4 @@
|
||||
# Vacuum Wall Deployment Guide
|
||||
# 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.
|
||||
|
||||
@@ -22,21 +22,51 @@ This guide walks through deploying Vacuum Wall on a real appliance or server. Va
|
||||
Download the Vacuum Wall repository onto the target machine, then run the installer with the required environment variables:
|
||||
|
||||
```bash
|
||||
# Option A: Public DNS
|
||||
PROJECT_DIR="/opt/vacuum-wall" \
|
||||
USER_NAME="vacuum-wall" \
|
||||
MGMT_DOMAIN=wall.example.com \
|
||||
MGMT_PASS="strongpassword" \
|
||||
MGMT_USER="admin" \
|
||||
ACME_EMAIL="admin@example.com" \
|
||||
bash install.sh
|
||||
|
||||
# Option B: mDNS (LAN-only, no DNS record needed)
|
||||
MGMT_DOMAIN=vacuum-wall.local \
|
||||
MGMT_PASS="strongpassword" \
|
||||
MGMT_USER="admin" \
|
||||
ACME_EMAIL="admin@example.com" \
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|---|---|---|
|
||||
| `MGMT_DOMAIN` | Yes | The public-facing domain for the management WebUI. A DNS A record must point to the appliance's IP. |
|
||||
| `PROJECT_DIR` | No | Directory where the project resides. Auto-discovers from `install.sh` location if not set. |
|
||||
| `USER_NAME` | No | System user that runs the WebUI service. Defaults to `vacuum-wall`. |
|
||||
| `MGMT_DOMAIN` | No | The domain for the management WebUI. Defaults to `$hostname.local` (auto-detected from the system hostname), which works with mDNS on your LAN (avahi-daemon is installed and enabled automatically). Set explicitly for a custom DNS domain (e.g., `wall.example.com`). **Errors if hostname is undetectable and this var is not set.** |
|
||||
| `MGMT_PASS` | Yes | The password for HTTP basic auth protecting the WebUI. Use a strong, randomly generated password. |
|
||||
| `MGMT_USER` | No | The username for WebUI access. Defaults to `admin`. |
|
||||
| `ACME_EMAIL` | Yes | The email address registered with Let's Encrypt for certificate issuance and expiry notifications. |
|
||||
| `ACME_EMAIL` | Yes | The email address registered with the ACME provider (ZeroSSL by default) for certificate issuance and expiry notifications. |
|
||||
|
||||
---
|
||||
|
||||
## Container / Custom Deployment
|
||||
|
||||
You can deploy Vacuum Wall in a container or at any custom path. Set `PROJECT_DIR` to the mount or bind path, and `USER_NAME` to whatever system user exists in the container or host environment:
|
||||
|
||||
```bash
|
||||
# Docker volume mount example
|
||||
PROJECT_DIR="/app/vacuum-wall" \
|
||||
USER_NAME="ww-app" \
|
||||
MGMT_DOMAIN="proxy.internal" \
|
||||
MGMT_PASS="strongpassword" \
|
||||
ACME_EMAIL="admin@example.com" \
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
The systemd service unit files and sudoers whitelist are rendered from Jinja2 templates at install time, substituting `USER_NAME` and `PROJECT_DIR`. This means no hardcoded paths remain after installation.
|
||||
|
||||
---
|
||||
|
||||
@@ -44,27 +74,30 @@ bash install.sh
|
||||
|
||||
The installer performs the following steps automatically:
|
||||
|
||||
- **Package installation**: Installs firewalld, nginx, dnsmasq, wireguard-tools, python3, Flask, pip, jq, curl, iptables, nftables, and apache2-utils.
|
||||
- **acme.sh installation**: Downloads and installs the acme.sh client to the project user's home directory for Let's Encrypt certificate management.
|
||||
- **Flask installation**: Ensures the Flask Python package is available via pip for the WebUI backend.
|
||||
- **System user creation**: Creates a dedicated `vacuum-wall` system user (nologin shell) that owns the project data and runs the WebUI service.
|
||||
- **Directory setup**: Creates data directories under `/home/wall/vacuum-wall/data/` for nginx sites, dnsmasq config, firewall rules, and WireGuard config. Sets ownership to the `vacuum-wall` user.
|
||||
- **Sudoers whitelist**: Installs a restrictive sudoers file at `/etc/sudoers.d/vacuum-wall` allowing the `vacuum-wall` user to run only the specific privileged commands needed for firewall, nginx, and dnsmasq management. Validates syntax with `visudo -cf`.
|
||||
- **Package installation**: Installs firewalld, nginx, dnsmasq, avahi-daemon, wireguard-tools, python3, Flask, pip, jq, curl, iptables, nftables, and apache2-utils.
|
||||
- **System user creation**: Creates a dedicated system user (default: `vacuum-wall`, configurable via `USER_NAME`) with a nologin shell that owns the project data and runs the WebUI service.
|
||||
- **Python venv**: Sets up a Python virtual environment and installs project dependencies.
|
||||
- **acme.sh installation**: Downloads and installs the acme.sh client to the project user's home directory for ACME certificate management.
|
||||
- **Directory setup**: Creates config directories under `$PROJECT_DIR/config/` for each subsystem's declarative JSON, and data directories under `$PROJECT_DIR/data/` for nginx sites, dnsmasq fragments, firewall rules, and WireGuard config. Sets ownership to the configured system user.
|
||||
- **Template rendering**: Renders system template files (`systemd/*.service`, `sudoers.d/`) via Jinja2, substituting `USER_NAME`, `PROJECT_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-wall` allowing the configured user to run only the specific privileged commands needed for firewall, nginx, and dnsmasq 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.
|
||||
- **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.
|
||||
- **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. Copies it to both `$USER_HOME/vacuum-wall/.htpasswd` (used by install.sh's initial nginx config) and `data/nginx/.htpasswd` (used by the running app).
|
||||
- **Systemd units**: Installs three units:
|
||||
- **Credentials**: Generates an htpasswd file using `apache2-utils` (with a Python fallback) for the management proxy's basic auth. Copies it to both `$USER_HOME/.htpasswd` (used by install.sh's initial nginx config) and `$PROJECT_DIR/data/nginx/.htpasswd` (used by the running app).
|
||||
- **Initial nginx config**: Writes `$PROJECT_DIR/config/nginx/config.json` with the management domain and auth settings pre-configured, so the WebUI can render management proxy config out of the box.
|
||||
- **Systemd units**: Installs three units (rendered from Jinja2 templates):
|
||||
- `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 nginx, the vacuum-wall WebUI, and the ACME renewal timer.
|
||||
- **ACME registration**: Registers the Let's Encrypt account with the provided email via acme.sh.
|
||||
- **Service startup**: Enables and starts nginx, the WebUI service, and the ACME renewal timer.
|
||||
- **ACME registration**: Registers the ACME account with the provided email via acme.sh.
|
||||
|
||||
---
|
||||
|
||||
@@ -92,7 +125,7 @@ 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 Let's Encrypt certificate for the management domain. After issuance, go to the **Proxy** tab and click **Apply** to reload nginx with the new cert.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -207,7 +240,7 @@ journalctl -u nginx --no-pager -n 50
|
||||
nginx -t
|
||||
```
|
||||
|
||||
Common causes include port conflicts (another service on port 80/443), missing dependencies, or file permission issues on `/home/wall/vacuum-wall/data/`.
|
||||
Common causes include port conflicts (another service on port 80/443), missing dependencies, or file permission issues on `$PROJECT_DIR/data/`.
|
||||
|
||||
### Firewall Rules Not Applying
|
||||
|
||||
@@ -226,14 +259,14 @@ visudo -cf /etc/sudoers.d/vacuum-wall
|
||||
|
||||
### Certificate Issuance Fails
|
||||
|
||||
Let's Encrypt ACME validation requires:
|
||||
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:
|
||||
|
||||
```bash
|
||||
su -s /bin/bash vacuum-wall -c "~/.acme.sh/acme.sh --list"
|
||||
su -s /bin/bash "$USER_NAME" -c "~/.acme.sh/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.
|
||||
@@ -244,16 +277,16 @@ 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 `/home/wall/vacuum-wall/data/dnsmasq/`.
|
||||
- A DHCP range is configured for the correct interface. Check dnsmasq config at `$PROJECT_DIR/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`.
|
||||
2. Check the management proxy configuration at `/etc/nginx/conf.d/vacuum-wall-mgmt.conf` (initial) or via the WebUI Proxy tab (after first apply).
|
||||
4. Ensure the `vacuum-wall` 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 Let's Encrypt certificate.
|
||||
3. Check the management proxy configuration at `/etc/nginx/conf.d/vacuum-wall-mgmt.conf` (initial) or via the WebUI Proxy tab (after first apply).
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -261,10 +294,10 @@ Verify that:
|
||||
|
||||
| Component | Service | Config Location |
|
||||
|---|---|---|
|
||||
| WebUI backend | `vacuum-wall.service` | `/home/wall/vacuum-wall/webui/` |
|
||||
| WebUI backend | `vacuum-wall.service` | `$PROJECT_DIR/webui/` |
|
||||
| Reverse proxy | `nginx` | `/etc/nginx/conf.d/vacuum-wall-mgmt.conf` |
|
||||
| Firewall | `firewalld` | Managed via WebUI and `firewall-cmd` |
|
||||
| DHCP/DNS | `dnsmasq` | `/home/wall/vacuum-wall/data/dnsmasq/` |
|
||||
| VPN | wireguard-tools | `/home/wall/vacuum-wall/data/wireguard/` |
|
||||
| Certificates | `vacuum-wall-acme.timer` | `/home/vacuum-wall/.acme.sh/` |
|
||||
| DHCP/DNS | `dnsmasq` | `$PROJECT_DIR/config/dnsmasq/` |
|
||||
| VPN | wireguard-tools | `$PROJECT_DIR/config/wireguard/` |
|
||||
| Certificates | `vacuum-wall-acme.timer` | `~/.acme.sh/` |
|
||||
| Sudoers | — | `/etc/sudoers.d/vacuum-wall` |
|
||||
+22
-10
@@ -6,7 +6,7 @@ Vacuum Wall is a zone-based firewall appliance with a built-in SSL reverse proxy
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Vacuum Wall is built around four integrated subsystems managed through a central Flask web interface. The traffic plane uses firewalld with its nftables backend, supporting zone-based policies, source NAT, and destination NAT for port forwarding. The DNS/DHCP plane serves private subnets via dnsmasq, providing address allocation and local name resolution. The proxy plane runs nginx with automatic Let's Encrypt certificates through acme.sh, handling SSL termination and reverse proxying for backend services. The VPN plane uses WireGuard (wg-quick) for encrypted tunnel management. All subsystems are configured and monitored through the Flask web UI, which is itself proxied through nginx with basic HTTP authentication.
|
||||
Vacuum Wall is built around four integrated subsystems managed through a central Flask web interface. The traffic plane uses firewalld with its nftables backend, supporting zone-based policies, source NAT, and destination NAT for port forwarding. The DNS/DHCP plane serves private subnets via dnsmasq, providing address allocation and local name resolution. The proxy plane runs nginx with automatic ACME certificates through acme.sh, handling SSL termination and reverse proxying for backend services. The VPN plane uses WireGuard (wg-quick) for encrypted tunnel management. All subsystems are configured and monitored through the Flask web UI, which is itself proxied through nginx with basic HTTP authentication.
|
||||
|
||||
## Subsystems
|
||||
|
||||
@@ -20,7 +20,7 @@ dnsmasq serves as both the DHCP server and local DNS resolver. It is configured
|
||||
|
||||
### SSL Proxy
|
||||
|
||||
The nginx reverse proxy handles HTTPS termination for user-defined domains, with certificates automatically provisioned and renewed via acme.sh and Let's Encrypt. Each proxy domain is configured with an HTTP-to-HTTPS redirect, modern TLS settings, and a configurable backend target. New proxy domains are added through the web UI, and the configuration is applied without manual intervention.
|
||||
The nginx reverse proxy handles HTTPS termination for user-defined domains, with certificates automatically provisioned and renewed via acme.sh and an ACME provider (ZeroSSL by default). Each proxy domain is configured with an HTTP-to-HTTPS redirect, modern TLS settings, and a configurable backend target. New proxy domains are added through the web UI, and the configuration is applied without manual intervention.
|
||||
|
||||
### WireGuard
|
||||
|
||||
@@ -34,7 +34,7 @@ WireGuard support provides server-side VPN tunnel management. Peers are added th
|
||||
- nginx 1.26+
|
||||
- dnsmasq
|
||||
- WireGuard tools (wireguard-tools)
|
||||
- acme.sh for ACME/Let's Encrypt certificate management
|
||||
- acme.sh for ACME certificate management (ZeroSSL by default)
|
||||
- HTMX for dynamic UI updates
|
||||
- Jinja2 for server-side templating
|
||||
|
||||
@@ -43,24 +43,36 @@ WireGuard support provides server-side VPN tunnel management. Peers are added th
|
||||
To install Vacuum Wall on a Debian 13 system, run `install.sh` as root with the required environment variables:
|
||||
|
||||
```bash
|
||||
MGMT_DOMAIN=wall.lan MGMT_PASS=yourpassword ACME_EMAIL=admin@example.com \
|
||||
MGMT_PASS=yourpassword ACME_EMAIL=admin@example.com \
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
After installation, access the management interface at `https://wall.lan` using the credentials you configured. The `install.sh` script provisions nginx, sets up authentication, obtains an initial Let's Encrypt certificate, and starts all services.
|
||||
After installation, access the management interface at `https://<hostname>.local` using the credentials you configured. The `install.sh` script auto-detects the system hostname (use `MGMT_DOMAIN` to override), provisions nginx, sets up authentication, generates an initial self-signed certificate, and starts all services.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
├── install.sh # Deployment script
|
||||
├── install.sh # Deployment script (renders Jinja2 templates)
|
||||
├── pyproject.toml # Project metadata + dependencies
|
||||
├── .venv/ # Python virtual environment
|
||||
├── system/ # System file templates
|
||||
├── config/ # Declarative JSON configuration (source of truth)
|
||||
│ ├── dnsmasq/ # DHCP/DNS config
|
||||
│ ├── nginx/ # Proxy domain & SSL config
|
||||
│ └── wireguard/ # VPN interface & peer config
|
||||
├── data/ # Runtime artifacts & generated files
|
||||
│ ├── nginx/sites-enabled/ # Generated server blocks
|
||||
│ ├── dnsmasq/fragments/ # User config fragments
|
||||
│ ├── acme/ # ACME certificates
|
||||
│ └── firewall/ # Firewall rule backup
|
||||
├── system/ # System file templates (all Jinja2)
|
||||
│ ├── systemd/ # Service and timer unit files
|
||||
│ │ ├── vacuum-wall.service # Web UI service
|
||||
│ │ ├── vacuum-wall-acme.service # Certificate renewal service
|
||||
│ │ ├── vacuum-wall.service # Web UI service (rendered at install)
|
||||
│ │ ├── vacuum-wall-acme.service # Certificate renewal (rendered at install)
|
||||
│ │ └── vacuum-wall-acme.timer # Renewal schedule
|
||||
│ └── sudoers.d/ # Sudo whitelist for service account
|
||||
│ ├── sudoers.d/ # Sudo whitelist (rendered at install)
|
||||
│ ├── nginx/ # Nginx config templates (rendered at runtime)
|
||||
│ ├── dnsmasq.conf # Dnsmasq template (rendered at runtime)
|
||||
│ └── wireguard*.conf # WireGuard templates (rendered at runtime)
|
||||
├── lib/ # Subsystem abstraction layer
|
||||
│ ├── firewall.py # firewalld bindings
|
||||
│ ├── dnsmasq.py # DHCP/DNS configuration
|
||||
|
||||
+12
-9
@@ -2,13 +2,13 @@
|
||||
|
||||
## Privilege Model
|
||||
|
||||
The Vacuum Wall management WebUI (Flask application) runs as the unprivileged `vacuum-wall` system user. The application never runs as root. All privileged operations — firewall rule changes, nginx reloads, dnsmasq config writes, WireGuard tunnel management — are executed through a restricted sudo whitelist defined at `/etc/sudoers.d/vacuum-wall`. ACME certificate operations via `acme.sh` are the exception: they run directly as the `vacuum-wall` user without sudo escalation, using webroot validation that doesn't require binding to privileged ports.
|
||||
The Vacuum Wall management WebUI (Flask application) runs as an unprivileged system user (default name: `vacuum-wall`, configurable via the `USER_NAME` environment variable at install time). The application never runs as root. All privileged operations — firewall rule changes, nginx reloads, dnsmasq config writes, WireGuard tunnel management — are executed through a restricted sudo whitelist defined at `/etc/sudoers.d/vacuum-wall`. ACME certificate operations via `acme.sh` are the exception: they run directly as the application user without sudo escalation, using webroot validation that doesn't require binding to privileged ports.
|
||||
|
||||
This design follows the principle of least privilege: only explicitly enumerated commands are permitted to escalate. There is no path to a full root shell from the application or the `vacuum-wall` user. If the WebUI process is compromised, an attacker is confined to the sudo whitelist surface rather than gaining unrestricted system access.
|
||||
This design follows the principle of least privilege: only explicitly enumerated commands are permitted to escalate. There is no path to a full root shell from the application or the dedicated service user. If the WebUI process is compromised, an attacker is confined to the sudo whitelist surface rather than gaining unrestricted system access.
|
||||
|
||||
## Sudo Whitelist
|
||||
|
||||
The file `/etc/sudoers.d/vacuum-wall` grants the `vacuum-wall` user passwordless sudo access to a strict set of commands. Each entry is scoped to a single binary with allowed arguments. The categories are:
|
||||
The file `/etc/sudoers.d/vacuum-wall` grants the configured system user passwordless sudo access to a strict set of commands. Each entry is scoped to a single binary with allowed arguments. The categories are:
|
||||
|
||||
| Category | Whitelisted Command | Purpose |
|
||||
|---|---|---|
|
||||
@@ -19,7 +19,7 @@ The file `/etc/sudoers.d/vacuum-wall` grants the `vacuum-wall` user passwordless
|
||||
| Dnsmasq | `systemctl is-active dnsmasq` | Check dnsmasq service status |
|
||||
| WireGuard | `wg-quick *` | WireGuard tunnel lifecycle (up, down, save, show) |
|
||||
| WireGuard | `wg *` | WireGuard status and peer management |
|
||||
| Certificates | (none) | acme.sh runs as the unprivileged `vacuum-wall` user directly; no sudo escalation is needed for certificate operations (webroot validation is used instead of standalone/TLS-ALPN) |
|
||||
| Certificates | (none) | acme.sh runs as the unprivileged service user directly; no sudo escalation is needed for certificate operations (webroot validation is used instead of standalone/TLS-ALPN) |
|
||||
| File writes | `sudo cp` to `/etc/nginx/`, `/etc/nginx/conf.d/`, `/etc/nginx/snippets/`, `/etc/dnsmasq.d/`, `/etc/wireguard/` | Copy rendered config files to system paths |
|
||||
| File writes | `sudo tee` to `/etc/dnsmasq.d/vacuum-wall.conf` | Write dnsmasq configuration |
|
||||
| File removal | `sudo rm` for `/etc/nginx/conf.d/vacuum-wall.conf`, `/etc/nginx/snippets/vacuum-wall-ssl.conf` | Clean up generated nginx config files |
|
||||
@@ -32,13 +32,14 @@ Key safety properties:
|
||||
|
||||
- Each `Cmnd` entry specifies the full path to the binary (e.g., `/usr/bin/firewall-cmd`).
|
||||
- Wildcard entries exist only for commands where the full argument space is needed (`firewall-cmd *`, `wg-quick *`, `wg *`), but none grant shell access or arbitrary command execution.
|
||||
- `NOPASSWD` is used so the application never prompts for a password. `Defaults:vacuum-wall` restricts the secure path and disables TTY requirement.
|
||||
- `NOPASSWD` is used so the application never prompts for a password. `Defaults:<user>` restricts the secure path and disables TTY requirement.
|
||||
- The sudoers file is rendered from a Jinja2 template at install time, substituting the configured user name.
|
||||
|
||||
## Web Security
|
||||
|
||||
### Management Interface
|
||||
|
||||
The Flask WebUI binds exclusively to `127.0.0.1:9090`. It is not exposed directly to any network interface. All external access to the management UI is routed through an nginx reverse proxy on the designated management domain, which provides SSL termination and HTTP Basic Authentication. The `.htpasswd` file is stored at `data/nginx/.htpasswd`.
|
||||
The Flask WebUI binds exclusively to `127.0.0.1:9090`. It is not exposed directly to any network interface. All external access to the management UI is routed through an nginx reverse proxy on the designated management domain, which provides SSL termination and HTTP Basic Authentication. The `.htpasswd` file is stored at `$PROJECT_DIR/data/nginx/.htpasswd`.
|
||||
|
||||
### Proxy Domains
|
||||
|
||||
@@ -71,7 +72,7 @@ The `vacuum-wall.service` unit file applies a comprehensive set of systemd sandb
|
||||
|---|---|---|
|
||||
| `ProtectSystem` | `strict` | Mounts the entire file system as read-only, except explicitly allowed paths |
|
||||
| `ProtectHome` | `read-only` | Makes `/home`, `/root`, and `/run/user` inaccessible |
|
||||
| `ReadWritePaths` | `/home/wall/vacuum-wall/data /tmp` | Only the application data directory and `/tmp` are writable |
|
||||
| `ReadWritePaths` | `$PROJECT_DIR/config`, `$PROJECT_DIR/data`, and `/tmp` | Both the application config directory and data directory, plus `/tmp`, are writable. The project path is templated at install time. |
|
||||
| `PrivateTmp` | `yes` | Provides a private `/tmp` and `/var/tmp` namespace |
|
||||
| `NoNewPrivileges` | `yes` | Prevents the process from gaining new privileges via `setuid`/`setgid` |
|
||||
| `IPAddressDeny` | `all` | Drops all network traffic |
|
||||
@@ -88,7 +89,9 @@ The `vacuum-wall.service` unit file applies a comprehensive set of systemd sandb
|
||||
| `SystemCallFilter` | `@system-service` | Allows only a curated set of system calls safe for services |
|
||||
| `RestrictRealtime` | `yes` | Prevents the process from acquiring realtime scheduling priorities |
|
||||
|
||||
This hardening ensures that even if the Flask application is compromised, the attacker is confined to a sandboxed environment with no direct network access, no write access outside the data directory, and no ability to escalate privileges through kernel interfaces.
|
||||
The `User`, `Group`, `WorkingDirectory`, `ExecStart`, and `ReadWritePaths` directives in the service unit are rendered from a Jinja2 template at install time with the configured `USER_NAME` and `PROJECT_DIR`.
|
||||
|
||||
This hardening ensures that even if the Flask application is compromised, the attacker is confined to a sandboxed environment with no direct network access, no write access outside the config and data directories, and no ability to escalate privileges through kernel interfaces.
|
||||
|
||||
## Network Security
|
||||
|
||||
@@ -114,7 +117,7 @@ IP forwarding (`net.ipv4.ip_forward = 1`) is enabled system-wide to allow routin
|
||||
|
||||
### acme.sh Integration
|
||||
|
||||
Certificate management is handled by acme.sh, which stores all certificates and private keys in the `vacuum-wall` user's home directory under `~/.acme.sh/`. The directory is owned by and writable only by the `vacuum-wall` user.
|
||||
Certificate management is handled by acme.sh, which stores all certificates and private keys in the service user's home directory under `~/.acme.sh/`. The directory is owned by and writable only by the service user.
|
||||
|
||||
### Private Key Protection
|
||||
|
||||
|
||||
Reference in New Issue
Block a user