feat: add ACME account management with validation pipeline

- Register, view, and deactivate ACME accounts via API and UI
- 16-check validation framework for certificate issuance readiness
- DNS resolution, port, nginx, and firewall pre-flight checks
- External IP detection with NAT support and fallback providers
- Account card and settings modal in certificates page
- Guard certificate issuance behind account registration
- Update modal CSS to overlay-based approach
- 1000+ lines of tests for validation and account handlers
This commit is contained in:
2026-06-23 14:24:19 +00:00
parent 3a325504ec
commit 5025dfaf30
19 changed files with 2073 additions and 141 deletions
+68 -3
View File
@@ -905,19 +905,19 @@ Returns HTTP `404` if no certificate is found for the domain.
POST /api/certs/issue
```
Request a new certificate for a domain.
Request a new certificate for a domain. Returns HTTP `500` if issuance fails.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `domain` | `string` | Yes | Domain to issue the certificate for |
| `email` | `string` | No | ACME contact email |
| `email` | `string` | No | ACME contact email **deprecated**, ignored in favor of the registered account email |
| `webroot` | `string` | No | Custom webroot path for HTTP-01 validation |
**Response:** `data` is `null` on success.
Returns HTTP `400` if the domain is missing. Returns HTTP `500` if issuance fails.
Returns HTTP `400` if the domain is missing. An ACME account must be registered before issuance (verified by the `account_registered` blocking check in the validation pipeline).
---
@@ -949,6 +949,71 @@ Returns HTTP `404` if the certificate is not found.
### Account
#### Get ACME Account Status
```
GET /api/certs/account
```
Return the ACME account registration status.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `registered` | `boolean` | Whether an ACME account is registered |
| `email` | `string` | Registered contact email (empty if unregistered) |
| `ca` | `string` | CA provider (e.g., `"let's encrypt"`, `"ZeroSSL"`) (empty if unregistered) |
Returns HTTP `500` if the account status cannot be determined.
---
#### Register ACME Account
```
POST /api/certs/account/register
```
Register a new ACME account with the specified email and CA provider.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | `string` | Yes | Contact email address |
| `server` | `string` | No | CA provider: `"letsencrypt"` or `"zerossl"`. Default: `"letsencrypt"` |
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `registered` | `boolean` | Always `true` on success |
| `email` | `string` | Registered contact email |
| `ca` | `string` | CA provider |
Returns HTTP `400` if the email is missing or invalid. Returns HTTP `500` if registration fails. An ACME account must be registered before certificates can be issued.
---
#### Deactivate ACME Account
```
DELETE /api/certs/account
```
Deactivate the ACME account via `acme.sh --deactivate-account`. Clears the `email` and `ca` fields from `config/acme/config.json`.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `email` | `string` | Empty string indicating the account was deactivated |
Returns HTTP `500` if deactivation fails.
---
#### Set ACME Contact Email
```
+90
View File
@@ -167,6 +167,96 @@ The `ssl` block defines TLS parameters applied to all HTTPS server blocks via th
| `ciphers` | string | No | nginx `ssl_ciphers` directive value. Default is a curated AEAD-only cipher string. |
| `prefer_server_ciphers` | boolean | No | Whether to prefer server cipher order. Default: `false`. |
## ACME (Certificate) Configuration
**File**: `config/acme/config.json`
This file stores the ACME account settings used by acme.sh for certificate provisioning. Account registration, modification, and deactivation are performed through the WebUI at the Certificates page — not by editing this file directly.
```json
{
"email": "admin@example.com",
"ca": "letsencrypt"
}
```
### ACME Fields
| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string | No | Contact email for the ACME account. Used for certificate expiry notifications and recovery. Populated automatically when an account is registered via the WebUI. Default: `""`. |
| `ca` | string | No | ACME CA provider. One of: `"letsencrypt"` (Let's Encrypt), `"zerossl"` (ZeroSSL). Populated automatically when an account is registered. Default: `""`. |
### Account Registration
ACME account registration is handled entirely through the WebUI. When the user registers an account:
1. The user navigates to the Certificates page and clicks "Register Account".
2. Provides an email address and selects a CA provider (Let's Encrypt or ZeroSSL).
3. The backend calls `acme.sh --register-account` with the provided parameters.
4. On success, the `email` and `ca` fields in `config/acme/config.json` are populated, and acme.sh writes its `.account.conf` file under `data/acme/`.
Before any certificate can be issued, an ACME account must be registered. The certificate validation flow includes a blocking check (`account_registered`) that prevents issuance if no account exists.
### Account Management
After registration, the account can be managed from the WebUI:
- **Update email**: The Settings modal allows changing the contact email, which triggers an update via `acme.sh --register-account -u`.
- **Deactivate account**: The Settings modal includes a button to deactivate the account via `acme.sh --deactivate-account`, which clears the `email` and `ca` fields and removes the ACME account.
### ACME Home Directory
acme.sh stores its state under `data/acme/` (the ACME home directory). Key files:
- `.account.conf` — ACME account credentials and settings (contains `ACME_LEEMAIL`, `ACME_MCA`).
- `<domain>/` — Per-domain certificate and key files issued by acme.sh.
The application reads `.account.conf` to determine registration status. If the file is missing or lacks required keys, the account is considered unregistered.
## ACME Configuration
**File**: `config/acme/config.json`
This file stores the ACME account settings used by vacuum-wall for automatic certificate issuance via acme.sh. Account registration is performed exclusively through the WebUI — the Certificates page provides a "Register Account" modal where the user enters an email and selects a CA provider.
```json
{
"email": "",
"ca": ""
}
```
### ACME Config Fields
| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string | No (WebUI) | Contact email for the ACME account, used for certificate expiry notifications and recovery. Populated when the user registers an account via the WebUI. Default: `""`. |
| `ca` | string | No (defaults to `letsencrypt`) | ACME CA provider. One of: `"letsencrypt"`, `"zerossl"`. Populated during account registration. Default: `""` (acme.sh defaults to Let's Encrypt if omitted). |
### Account Registration Flow
1. User navigates to the Certificates page in the WebUI.
2. Clicks "Register Account" and provides an email address, optionally selecting a CA provider.
3. The application calls `acme.sh --register-account -m <email> --server <ca>` as a privileged operation via the daemon.
4. On success, `config/acme/config.json` is updated with the email and CA. acme.sh writes its own state to `data/acme/.account.conf`.
5. The `account_registered` check in the certificate validation pipeline transitions from blocking to passing, enabling certificate issuance.
The `account_registered` check is **blocking** — certificate issuance and validation will fail until an ACME account is registered. The `email_configured` check is **non-blocking** — it produces a warning if the email is empty but does not prevent issuance.
### Account Management API
| Endpoint | Method | Description |
|---|---|---|
| `/api/certs/account` | `GET` | Returns account status: registered, email, CA provider. |
| `/api/certs/account/register` | `POST` | Registers a new ACME account. Body: `{ "email": "..." }`. Optional: `{ "server": "letsencrypt" }`. |
| `/api/certs/account` | `DELETE` | Deactivates the ACME account via `acme.sh --deactivate-account`. Clears email and CA from config. |
| `/api/certs/email` | `POST` | Updates the contact email on an existing account. Body: `{ "email": "..." }`. |
### ACME Home Directory
acme.sh stores its operational state under `data/acme/`. The application reads `data/acme/.account.conf` to determine whether an account is registered. Required keys: `ACME_LEEMAIL` and `ACME_MCA`. Their absence or the file's absence means the account is unregistered.
## WireGuard Configuration
**File**: `config/wireguard/config.json`
+6 -9
View File
@@ -25,14 +25,13 @@ Download the Vacuum Wall repository onto the target machine, then run the instal
# 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"
./install.sh --dev --mgmt-pass strongpassword
# mDNS (LAN-only, no DNS record needed)
./install.sh --mgmt-domain vacuum-wall.local --mgmt-pass strongpass --acme-email "me@example.com"
./install.sh --mgmt-domain vacuum-wall.local --mgmt-pass strongpass
```
### Options
@@ -45,7 +44,6 @@ All settings that can be passed as an environment variable also have a CLI flag
| `--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. |
@@ -73,7 +71,7 @@ In dev mode, the ownership model preserves the developer's ability to work with
### Running the Installer in Dev Mode
```bash
./install.sh --dev --mgmt-pass strongpassword --acme-email "dev@example.com"
./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.
@@ -91,8 +89,7 @@ You can deploy Vacuum Wall in a container or at any custom path. Use `--path` (o
```bash
# 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"
--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.
@@ -130,7 +127,7 @@ The installer performs the following steps automatically:
- `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.
- **ACME account**: No account registration during install. Register the account via the WebUI after first login.
### Idempotent Re-Runs
@@ -308,7 +305,7 @@ 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:
- 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"
+4 -4
View File
@@ -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 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.
The nginx reverse proxy handles HTTPS termination for user-defined domains, with certificates automatically provisioned and renewed via acme.sh and an ACME provider (Let's Encrypt 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.
### Network (systemd-networkd)
@@ -39,7 +39,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 certificate management (ZeroSSL by default)
- acme.sh for ACME certificate management (Let's Encrypt by default)
## Quick Start
@@ -47,10 +47,10 @@ To install Vacuum Wall on a Debian 13 system, run `install.sh` as root with requ
```bash
# Production
./install.sh --mgmt-pass yourpassword --acme-email "admin@example.com"
./install.sh --mgmt-pass yourpassword
# Development (auto-detects your user)
./install.sh --dev --mgmt-pass yourpassword --acme-email "admin@example.com"
./install.sh --dev --mgmt-pass yourpassword
```
After installation, access the management interface at `https://<hostname>.local` using the credentials you configured. The `install.sh` script auto-detects the system hostname, network interfaces, and provisions nginx, authentication, an initial self-signed certificate, and all services. Run `./install.sh --help` for all options.