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:
+68
-3
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user