style: format docs, fix user_permissions variable scoping in auth middleware
Apply ruff line-wrapping formatting to docs and test files. Clarify auth middleware: extract user_permissions once before subsystem check, removing conditional variable scoping.
This commit is contained in:
+43
-2
@@ -11,6 +11,8 @@ ACME certificate operations via `acme.sh` run as the daemon user — not as root
|
||||
|
||||
This design follows the principle of least privilege: only the daemon process holds sudo access, and only for explicitly enumerated commands. The WebUI user is completely isolated from sudo.
|
||||
|
||||
Authentication (JWT validation, token blacklist check, permission verification) is performed at the Flask layer — not the daemon. The daemon only receives requests from the Flask process via authenticated Unix socket connections. WebSocket connections to the daemon require a JWT access token as a query parameter for validation before upgrade.
|
||||
|
||||
## Communication Between WebUI and Daemon
|
||||
|
||||
The WebUI communicates with the daemon via synchronous HTTP requests over a Unix socket (`data/daemon.sock`), owned by `vacuum-walld:<group>` with mode `0660`. The shared group membership allows the WebUI user to connect to the socket. The daemon runs an `aiohttp` server that routes requests to handler modules (`daemon/handlers/*.py`), which execute the privileged commands.
|
||||
@@ -68,9 +70,11 @@ The `daemon/client.py` module resolves `<param>` placeholders in URL paths befor
|
||||
|
||||
### 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. Authentication is handled at the Flask layer via JWT validation — no nginx-level `auth_basic` is applied to the management domain.
|
||||
|
||||
The management interface does not set security hardening headers (e.g., `X-Content-Type-Options`, `X-Frame-Options`, HSTS). It relies on nginx basic authentication, SSL termination, and the systemd sandbox for its security boundary.
|
||||
JWT tokens are stored in browser `localStorage` and injected as `Authorization: Bearer <token>` headers. The API **never** reads cookies — authentication is header-only. This eliminates CSRF concerns: cross-origin requests cannot set custom headers.
|
||||
|
||||
The management interface does not set security hardening headers (e.g., `X-Content-Type-Options`, `X-Frame-Options`, HSTS) on proxied responses, as the SPA requires flexibility for its operation. It relies on JWT authentication, SSL termination, and the systemd sandbox for its security boundary.
|
||||
|
||||
### Proxy Domains
|
||||
|
||||
@@ -86,6 +90,43 @@ Every proxied domain configured in Vacuum Wall enforces:
|
||||
|
||||
Additional proxy headers (`headers` in the path-level config) are delivered to the upstream backend via nginx `proxy_set_header` directives — they are not sent as response headers to clients.
|
||||
|
||||
### JWT Authentication Lifecycle
|
||||
|
||||
JWT-based authentication replaces HTTP Basic Auth for the management WebUI. The token lifecycle is:
|
||||
|
||||
1. **Login**: User submits credentials via `POST /api/auth/login`. The daemon verifies the password hash (Argon2id) against `data/auth.db`. On success, an access token (15 min) and refresh token (7 days) are issued.
|
||||
2. **Validation**: Every request to Flask includes `Authorization: Bearer <token>`. The `before_request` middleware validates the token signature, checks expiry, queries the SQLite `token_blacklist` table, and verifies per-subsystem permissions.
|
||||
3. **Auto-refresh**: Before the access token expires, the frontend's `refreshScheduler()` calls `POST /api/auth/refresh` with the refresh token. The old refresh token is blacklisted and a new pair is issued.
|
||||
4. **Blacklist**: On logout (`POST /api/auth/logout`) or password change, the current token's `jti` is inserted into `token_blacklist`. The expired blacklist entries are cleaned on every refresh operation via `Q_DELETE_EXPIRED`.
|
||||
|
||||
Token theft protection:
|
||||
- Short-lived access tokens (15 min) limit the window of exploitation
|
||||
- Token blacklist prevents reuse after logout or password change
|
||||
- XSS mitigations: CSP headers, `X-XSS-Protection` header on management domain
|
||||
|
||||
### WebAuthn Security
|
||||
|
||||
WebAuthn (passkeys) provides passwordless authentication via the browser's Web Authentication API. Security properties:
|
||||
|
||||
- **Credential binding**: Each credential is cryptographically bound to the specific `rp_id` (management domain) and `origin` (HTTPS URL). Credentials cannot be phished to a different domain.
|
||||
- **Private key protection**: The private key never leaves the authenticator device. The server only stores the public key and signature counter in the `webauthn_creds` table.
|
||||
- **Assertion verification**: Each authentication attempt verifies the signature against the stored public key and checks that the signature count has increased (replay prevention).
|
||||
- **RP configuration**: `rp_id` and `origin` are configurable per deployment in `config/auth/config.json`.
|
||||
- **Fallback**: Password authentication always remains available as a fallback. Losing a WebAuthn credential does not lock the user out.
|
||||
|
||||
### Header-Only Authentication and CSRF
|
||||
|
||||
The API exclusively reads the `Authorization` header — never cookies. This architecture eliminates CSRF risk:
|
||||
|
||||
- Cross-site requests cannot set custom HTTP headers due to browser CORS restrictions
|
||||
- No cookie-based session to exploit
|
||||
- No SameSite, double-submit, or origin checking needed
|
||||
|
||||
**XSS as the primary attack surface**: With header-only auth, XSS is the primary attack vector since `localStorage` is accessible to page scripts. Mitigations include:
|
||||
- CSP headers on the management domain (configured in nginx)
|
||||
- `X-XSS-Protection` header
|
||||
- Short-lived access tokens (15 min) with blacklist on logout
|
||||
|
||||
### TLS Configuration
|
||||
|
||||
The default nginx SSL configuration enforces modern TLS only:
|
||||
|
||||
Reference in New Issue
Block a user