Authentication and TLS
Admin password
wpg has one admin account (admin). Its password comes from the first of these sources that is set:
wpg serve --password <PASSWORD>. Other users can see it in the process list, so use it only for quick tests.wpg serve --password-file <FILE>. The file holds the plain-text password or an argon2 hash.- The environment variable
WPG_PASSWORD, holding the plain-text password or an argon2 hash. auth.password_hashinwpg.toml.wpg config set-passwordprompts for the password and writes this key. Use--stdinin scripts.auth.password_fileinwpg.toml, holding the plain-text password or an argon2 hash.- The first-run setup screen, when none of the above is set.
wpg hashes plain-text passwords with argon2id at startup (19 MiB, 2 passes; the OWASP recommendation) and stores the hash in <data_dir>/wpg.sqlite. If a configured password changes, the next start ends all existing sessions.
Without a password
Until a password exists:
- every API endpoint except
/api/health,/api/version,/api/auth/*and/api/docsanswers401 setup_required; wpg serverefuses to listen on a non-loopback address and exits with status 2. Pass--allow-insecureto override this. wpg then logs a loud warning, and anyone who can reach the server can set the password;- the setup screen accepts requests only from the machine itself (loopback), unless the server runs with
--allow-insecure.
wpg serve # 127.0.0.1:8080: open it and set the password
wpg config set-password # or: store a hash in wpg.toml first
wpg serve --bind 0.0.0.0:8080 --tls # now any address is fine
Sessions and CSRF
- After a login, the
wpg_sessioncookie identifies the session. It holds a random 256-bit token and is set withHttpOnly; SameSite=Strict, plusSecureover HTTPS. The database stores only the token’s SHA-256. - Sessions last
auth.session_ttl_secs(default 24 h). Every request extends the session. - POST, PUT and DELETE requests must send the session’s CSRF token in
X-CSRF-Token. The server returns the token inGET /api/auth/statusand in the readablewpg_csrfcookie. Login and setup take no CSRF token, but they accept onlyContent-Type: application/json. - WebSocket connections (
/api/ws, and/api/terminal/wsof the web terminal) need the session cookie. The server rejects them when theirOriginnames another host. See Web terminal for the terminal’s safeguards.
API: GET /api/auth/status, POST /api/auth/setup {password, confirm}, POST /api/auth/login {password, username?}, POST /api/auth/logout. See the API reference.
API tokens
Scripts, CI jobs and wpg remote authenticate with an API token instead of a session:
# on the server (also while it runs); prints only the token
wpg config token create ci --expires-days 90
# or on the web UI: Settings → API tokens
export WPG_SERVER=https://wpg.example.net:8080 WPG_TOKEN=wpg_...
wpg remote jobs start examples-arp-sweep --backend null --count 100 --wait
curl -H "Authorization: Bearer $WPG_TOKEN" "$WPG_SERVER/api/jobs"
- A token is
wpg_followed by 64 hex digits (256 random bits). wpg shows it once, when you create it. The database stores only its SHA-256, and the first 12 characters (wpg_+ 8 hex digits) to tell tokens apart. A fast hash is enough because the token is random and long; argon2 would cost every API request about 19 MiB and tens of milliseconds. - A request with
Authorization: Bearer <token>acts as the user who created the token, with that user’s role. There are no finer scopes. It needs no CSRF token and gets no cookies. - When a request carries an
Authorizationheader, wpg ignores the session cookie. A wrong, expired or revoked token answers401 unauthorized. Failures count against the login rate limit of the client address. - A token can expire after 1 to 3650 days, or never. It survives password changes. Revoking it (web UI,
wpg config token revoke <id>,DELETE /api/tokens/{id}) deletes it; the next request with it fails. - Tokens cannot manage tokens:
/api/tokensanswers403 forbiddento bearer requests. Create and revoke tokens with a session (the web UI) or withwpg config tokenon the server. Admins see and revoke every user’s tokens; other users see only their own. - The audit log records token creation, revocation and rejected tokens. wpg records each token’s last use, at most once a minute.
API: GET /api/tokens, POST /api/tokens {name, expires_in_days?} (returns {token, secret}), DELETE /api/tokens/{id}.
wpg remote checks the server certificate. For a server with a self-signed certificate, pass --insecure.
Login rate limit
wpg allows 5 failed logins per minute per client address. After that, the client is locked out for 1 minute. The lock-out doubles each time it happens again, up to 15 minutes. While a client is locked out, logins answer 429 rate_limited with details.retry_after_secs and a Retry-After header.
wpg records every attempt in the login_attempts table and keeps it for 30 days. Behind a reverse proxy, all clients share the proxy’s address.
Audit log
Logins, failed logins, logouts, the setup, API token creation, revocation and rejection, and every job action (create, start, pause, resume, stop, delete) are logged with the wpg::audit target, the user and the client address. To log only these events, set RUST_LOG=wpg::audit=info.
TLS
wpg serve --tls, orserver.self_signed_tls = true, generates a self-signed ECDSA certificate into<data_dir>/tls/and reuses it on later starts. wpg renews it about a month before it expires; certificates are valid for 800 days. It coverslocalhost,127.0.0.1,::1, the bind address and the host name. wpg prints the certificate’s SHA-256 fingerprint at startup, so you can compare it with the one your browser shows.- To use your own certificate, set
[server.tls] cert = "...", key = "..."(PEM files) inwpg.toml. This takes precedence over the self-signed certificate. - The server does not answer plain HTTP on a TLS port and does not redirect to HTTPS.
Behind a reverse proxy (nginx, Caddy), keep wpg on 127.0.0.1 and let the proxy handle TLS. Forward the WebSocket upgrade for /api/ws.
Security headers
Every response carries a Content Security Policy (default-src 'self'; scripts only from the server plus 'wasm-unsafe-eval' and the hashes of the inline bootstrap script; inline styles; connect-src 'self' ws: wss:; frame-ancestors 'none'), plus X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer and a restrictive Permissions-Policy. The Swagger UI under /api/docs gets a relaxed policy that allows its inline scripts.