Fix dashboard template bugs, acme date parsing, wireguard sudoers match, and stale docs
- dashboard.html: Fix zones, leases, wg_status, cert key names, add services var
- server.py: Pass services to dashboard template via _get_service_status()
- lib/acme.py: Fix dead third date format (%Y%m%d%H%M%z) using astimezone(UTC)
- lib/wireguard.py: Add -- separator to cp command to match sudoers rule
- lib/nginx.py: Replace shallow dict.copy() with {**...} for DEFAULT_SSL
- AGENTS.md: Update test count 149 -> 154
- docs/api.md: Rename cert field expiry -> expires_at
This commit is contained in:
+86
-18
@@ -74,7 +74,7 @@ Return detailed configuration for a single zone.
|
||||
| `target` | `string` | Zone target (e.g., `"default"`, `"ACCEPT"`, `"REJECT"`) |
|
||||
| `interfaces` | `[string, ...]` | Interfaces assigned to this zone |
|
||||
| `services` | `[string, ...]` | Services allowed through the zone |
|
||||
| `ports` | `[{port: number, proto: string}, ...]` | Explicit port rules |
|
||||
| `ports` | `[string, ...]` | Explicit port rules (format: `"443/tcp"`) |
|
||||
| `masquerade` | `boolean` | Whether masquerade (NAT) is enabled |
|
||||
| `forward_ports` | `[{port: number, proto: string, toaddr: string, toport: number}, ...]` | Port forward rules |
|
||||
| `rich_rules` | `[string, ...]` | Rich rule definitions |
|
||||
@@ -130,7 +130,12 @@ Replace all interfaces assigned to the zone with the provided list.
|
||||
|-------|------|----------|-------------|
|
||||
| `interfaces` | `[string, ...]` | Yes | List of interface names |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `zone` | `string` | Zone name |
|
||||
| `interfaces` | `[string, ...]` | List of interface names now assigned to the zone |
|
||||
|
||||
---
|
||||
|
||||
@@ -148,7 +153,12 @@ Replace all services allowed in the zone with the provided list.
|
||||
|-------|------|----------|-------------|
|
||||
| `services` | `[string, ...]` | Yes | List of firewalld service names |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `zone` | `string` | Zone name |
|
||||
| `services` | `[string, ...]` | List of services now allowed in the zone |
|
||||
|
||||
### Firewall Rules
|
||||
|
||||
@@ -167,7 +177,12 @@ Add a firewalld rich rule to a zone.
|
||||
| `zone` | `string` | Yes | Zone to add the rule to |
|
||||
| `rule` | `string` | Yes | Full rich rule string |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `zone` | `string` | Zone name |
|
||||
| `rule` | `string` | Full rich rule string |
|
||||
|
||||
---
|
||||
|
||||
@@ -186,7 +201,12 @@ Remove an existing rich rule from a zone. The `rule` string must match exactly.
|
||||
| `zone` | `string` | Yes | Zone the rule belongs to |
|
||||
| `rule` | `string` | Yes | Exact rich rule string to remove |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `zone` | `string` | Zone name |
|
||||
| `rule` | `string` | Exact rich rule string that was removed |
|
||||
|
||||
---
|
||||
|
||||
@@ -221,7 +241,12 @@ Toggle masquerade (source NAT) for a zone.
|
||||
| `zone` | `string` | Yes | Zone to configure |
|
||||
| `enable` | `boolean` | Yes | `true` to enable, `false` to disable |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `zone` | `string` | Zone name |
|
||||
| `masquerade` | `boolean` | Whether masquerade is now enabled for the zone |
|
||||
|
||||
---
|
||||
|
||||
@@ -243,7 +268,13 @@ Add a port forwarding rule to a zone.
|
||||
| `toaddr` | `string` | No | Internal destination address |
|
||||
| `toport` | `number` | No | Internal destination port |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `zone` | `string` | Zone name |
|
||||
| `port` | `number` | External port |
|
||||
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -265,7 +296,13 @@ Remove a port forwarding rule. The body must match the original rule exactly.
|
||||
| `toaddr` | `string` | No | Internal destination address |
|
||||
| `toport` | `number` | No | Internal destination port |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `zone` | `string` | Zone name |
|
||||
| `port` | `number` | External port |
|
||||
| `proto` | `string` | Protocol (`"tcp"` or `"udp"`) |
|
||||
|
||||
### Info
|
||||
|
||||
@@ -403,7 +440,13 @@ Add a static (reserved) DHCP lease.
|
||||
| `ip` | `string` | Yes | Reserved IP address |
|
||||
| `hostname` | `string` | No | Hostname for the reservation |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `mac` | `string` | MAC address |
|
||||
| `ip` | `string` | Reserved IP address |
|
||||
| `hostname` | `string` | Hostname for the reservation |
|
||||
|
||||
---
|
||||
|
||||
@@ -442,7 +485,13 @@ Add a custom DNS A record served by dnsmasq.
|
||||
| `name` | `string` | Yes | Fully qualified domain name |
|
||||
| `address` | `string` | Yes | IP address to resolve to |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `string` | Fully qualified domain name |
|
||||
| `address` | `string` | IP address |
|
||||
| `hostname` | `string` | Short hostname |
|
||||
|
||||
---
|
||||
|
||||
@@ -505,7 +554,11 @@ Add a new reverse proxy domain.
|
||||
| `backend_port` | `number` | Yes | Backend server port |
|
||||
| `backend_proto` | `string` | No | Backend protocol (`"http"` or `"https"`); defaults to `"http"` |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `domain` | `string` | Domain name |
|
||||
|
||||
Returns HTTP `400` if the domain is already configured.
|
||||
|
||||
@@ -548,7 +601,11 @@ Update one or more fields of an existing domain entry. Only the fields present i
|
||||
| `backend_port` | `number` | No | Backend server port |
|
||||
| `backend_proto` | `string` | No | Backend protocol |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `domain` | `string` | Domain name |
|
||||
|
||||
Returns HTTP `404` if the domain is not configured.
|
||||
|
||||
@@ -562,7 +619,11 @@ DELETE /api/proxy/domains/<domain>
|
||||
|
||||
Remove a proxy domain and its nginx configuration.
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `domain` | `string` | Domain name |
|
||||
|
||||
Returns HTTP `404` if the domain is not configured.
|
||||
|
||||
@@ -648,7 +709,7 @@ Each certificate object:
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `domain` | `string` | Domain the certificate covers |
|
||||
| `expiry` | `string` | Expiration date (ISO 8601) |
|
||||
| `expires_at` | `string` | Expiration date (ISO 8601) |
|
||||
| `days_until_expiry` | `number` | Remaining days until expiration |
|
||||
| `cert_path` | `string` | Path to the certificate file |
|
||||
| `key_path` | `string` | Path to the private key file |
|
||||
@@ -668,7 +729,7 @@ Return details for a single certificate.
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `domain` | `string` | Domain |
|
||||
| `expiry` | `string` | Expiration date (ISO 8601) |
|
||||
| `expires_at` | `string` | Expiration date (ISO 8601) |
|
||||
| `days_until_expiry` | `number` | Remaining days |
|
||||
| `cert_path` | `string` | Certificate file path |
|
||||
| `key_path` | `string` | Private key file path |
|
||||
@@ -690,7 +751,6 @@ Request a new certificate for a domain.
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `domain` | `string` | Yes | Domain to issue the certificate for |
|
||||
| `standalone` | `boolean` | No | Use standalone (TCP) validation; defaults to `false` (HTTP-01 via existing webroot) |
|
||||
| `webroot` | `string` | No | Custom webroot path for HTTP-01 validation |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
@@ -741,7 +801,11 @@ Set or update the ACME account contact email (used by Let's Encrypt for expirati
|
||||
|-------|------|----------|-------------|
|
||||
| `email` | `string` | Yes | Contact email address |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `email` | `string` | Contact email address |
|
||||
|
||||
---
|
||||
|
||||
@@ -888,7 +952,11 @@ Remove a configured peer.
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | `string` | Yes | Peer name to remove |
|
||||
|
||||
**Response:** `data` is `null` on success.
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `string` | Peer name |
|
||||
|
||||
Returns HTTP `404` if the peer is not found.
|
||||
|
||||
|
||||
@@ -34,13 +34,13 @@ The following diagram summarizes how the Flask WebUI communicates with each mana
|
||||
```
|
||||
External Client ──→ nginx (SSL termination) ──→ Flask WebUI (127.0.0.1:9090)
|
||||
Flask WebUI ──→ lib/firewall.py ──→ sudo firewall-cmd ──→ firewalld / D-Bus ──→ nftables
|
||||
Flask WebUI ──→ lib/nginx.py ──→ write rendered .conf files ──→ sudo nginx -s reload
|
||||
Flask WebUI ──→ lib/dnsmasq.py ──→ render /etc/dnsmasq.d/vacuum-wall.conf ──→ sudo systemctl reload dnsmasq
|
||||
Flask WebUI ──→ lib/acme.py ──→ sudo acme.sh ──→ acme.sh CLI ──→ Let's Encrypt ACME
|
||||
Flask WebUI ──→ lib/wireguard.py ──→ render /etc/wireguard/wg0.conf ──→ sudo wg-quick up wg0 ──→ kernel module
|
||||
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
|
||||
```
|
||||
|
||||
Each `lib/` module encapsulates the command construction, sudo invocation, 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 through the sudo whitelist.
|
||||
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.
|
||||
|
||||
## State Management
|
||||
|
||||
@@ -79,9 +79,8 @@ The following file system locations are used for integration with system service
|
||||
|
||||
| Path | Purpose | Managed By |
|
||||
|---|---|---|
|
||||
| `/etc/nginx/conf.d/vacuum-wall.conf` | Include directive that pulls in `data/nginx/sites-enabled/*.conf`. Also contains the WebSocket proxy map shared by all server blocks. | Vacuum Wall (lib/nginx.py) |
|
||||
| `/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/nginx/sites-enabled/` | Symlinks or config files for Vacuum Wall-managed domains (if used alongside other sites). | Vacuum Wall / system |
|
||||
| `/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) |
|
||||
|
||||
+4
-5
@@ -133,16 +133,15 @@ The `domains` object maps domain names (keys) to proxy configurations. Each entr
|
||||
| `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.path` | object | Yes (if `file`) | Paths to certificate files. |
|
||||
| `cert.path.certificate` | string | Yes (if `file`) | Full path to the public certificate file (PEM). |
|
||||
| `cert.path.key` | string | Yes (if `file`) | Full path to the private key file (PEM). |
|
||||
| `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). |
|
||||
|
||||
### Certificate Types
|
||||
|
||||
| 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. |
|
||||
| `file` | Use a pre-existing certificate and private key from the local file system. The `path.certificate` and `path.key` fields must point to readable PEM files. Vacuum Wall will not attempt to renew these certificates. |
|
||||
| `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. |
|
||||
|
||||
### Management Domain
|
||||
@@ -224,7 +223,7 @@ Peers are stored in an object keyed by a human-readable identifier (e.g., `alice
|
||||
| `public_key` | string | Yes | The peer's public key. |
|
||||
| `private_key` | string | No | The peer's private key, stored for generating downloadable client configuration files. This value is stripped from all API responses — the WebUI never exposes peer private keys over the network. |
|
||||
| `endpoint` | string | No | The peer's public endpoint (IP:port). Required for server-initiated connections (e.g., the server reaching out to a peer behind a firewall). Leave empty or `null` for peer-initiated connections where the peer connects to the server. |
|
||||
| `allowed_ips` | array | Yes | CIDR blocks that traffic from this peer is allowed to route. `["0.0.0.0/0"]` allows all traffic. `["10.137.0.0/16"]` restricts traffic to the VPN subnet. |
|
||||
| `allowed_ips` | array | No | CIDR blocks that traffic from this peer is allowed to route. Defaults to `[]` (no routing restrictions from the server side). `["0.0.0.0/0"]` allows all traffic. `["10.137.0.0/16"]` restricts traffic to the VPN subnet. |
|
||||
| `persistent_keepalive` | integer | No | Keepalive interval in seconds. `25` is recommended for peers behind NAT. Set to `0` or `null` to disable. |
|
||||
| `preshared_key` | string | No | Optional pre-shared key for post-quantum resistance. Use `wg genpsk` to generate. |
|
||||
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ The installer performs the following steps automatically:
|
||||
- **Dnsmasq initialization**: Starts and enables dnsmasq for future DHCP/DNS serving on internal interfaces.
|
||||
- **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.
|
||||
- **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:
|
||||
- `vacuum-wall.service` — the Flask WebUI backend.
|
||||
- `vacuum-wall-acme.service` — the certificate renewal oneshot.
|
||||
@@ -251,7 +251,7 @@ Verify that:
|
||||
|
||||
1. Verify nginx is running: `systemctl status nginx`.
|
||||
2. Test nginx configuration: `nginx -t`.
|
||||
3. Check the management proxy configuration at `/etc/nginx/conf.d/vacuum-wall-mgmt.conf`.
|
||||
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.
|
||||
|
||||
|
||||
+12
-9
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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, TLS certificate issuance — are executed through a restricted sudo whitelist defined at `/etc/sudoers.d/vacuum-wall`.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -19,24 +19,26 @@ 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 | `acme.sh` (via `bash -c`) | Let's Encrypt certificate issuance and renewal |
|
||||
| File writes | `sudo cp` to `/etc/nginx/`, `/etc/dnsmasq.d/`, `/etc/wireguard/` | Copy rendered config files to system paths |
|
||||
| Logs | `sudo journalctl --unit=*` | Query systemd journal for managed services |
|
||||
| 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) |
|
||||
| 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 |
|
||||
| Directory creation | `sudo mkdir -p /etc/dnsmasq.d`, `sudo mkdir -p /etc/wireguard` | Ensure target directories exist |
|
||||
| Logs | `sudo journalctl --unit=* -n *` | Query systemd journal for managed services |
|
||||
| Logs | `sudo cat /var/log/nginx/*` | Read nginx access and error logs |
|
||||
| File writes | `sudo tee` | Write configuration data to protected paths |
|
||||
| Leases | `sudo cat /var/lib/dnsmasq/dnsmasq.leases` | Read dnsmasq lease table |
|
||||
|
||||
Key safety properties:
|
||||
|
||||
- Each `Cmnd` entry specifies the full path to the binary (e.g., `/usr/bin/firewall-cmd`).
|
||||
- No wildcard entries grant shell access or arbitrary command execution.
|
||||
- The `acme.sh` entry is restricted to certificate operations through an explicit `bash -c` wrapper that only passes acme-related arguments.
|
||||
- `DEFAULT!/usr/bin/sudo` and `NOPASSWD` are used so the application never prompts for a password and cannot chain sudo calls.
|
||||
- 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.
|
||||
|
||||
## 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 using an `.htpasswd` file.
|
||||
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`.
|
||||
|
||||
### Proxy Domains
|
||||
|
||||
@@ -84,6 +86,7 @@ The `vacuum-wall.service` unit file applies a comprehensive set of systemd sandb
|
||||
| `LockPersonality` | `yes` | Prevents changing the execution domain |
|
||||
| `MemoryDenyWriteExecute` | `yes` | Prevents creating memory regions that are both writable and executable |
|
||||
| `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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user