Authentication
Manager authentication
Purpose
Authenticate administrators to the Manager console and /api/v1, issue a JWT for API calls, and keep the browser session on an HttpOnly cookie that is not returned in the login JSON.
Zwall does not ship OIDC, LDAP, or SAML. Do not configure those as if they were Manager features.
Audience
Operators who log in, enable TOTP, create additional administrator users, or point zwallctl at a Manager.
Edition
SHARED login, roles, and TOTP. Enterprise production rejects literal JWT/database/admin password authority in YAML and expects systemd credentials (jwt_secret, database_password, first-install admin_initial_password). Community uses operator-managed config/environment secrets.
Secret custody via scripts/setup.sh --manager-secret-custody fresh|migrate|rotate is implemented but not certified. Do not call it certified.
Status
SUPPORTED for username/password login, optional TOTP, JWT + session cookie, and the three operator-facing roles. Most operator APIs still require admin.
Prerequisites
- Manager
/health/readyreturns ready - TLS enabled in the production example (
server.tls.enabled: true); browser cookie issuance requires acceptable HTTPS per the cookie policy - Initial administrator credential from the supported secret contract (not from YAML in Enterprise production)
Required permissions
Scroll sideways to see the full table. Use the arrow keys when focused.
| Action | Auth |
|---|---|
POST /api/v1/auth/login | none (rate limited) |
GET /api/v1/auth/status | session cookie |
POST /api/v1/auth/refresh | session |
POST /api/v1/auth/logout | Bearer and/or cookie (handler still reads them) |
POST /api/v1/auth/change-password | admin |
GET/POST /api/v1/auth/2fa/* | admin |
GET/POST/DELETE /api/v1/users | admin + RoleAuth("admin") |
Dependencies
PostgreSQL (user rows). Enterprise: systemd credential jwt_secret for token signing. Optional TOTP for accounts that have 2FA enabled.
Configuration
From configs/manager.example.yml:
Scroll sideways to see the full table. Use the arrow keys when focused.
| Key | Example | Secret | Notes |
|---|---|---|---|
auth.jwt_secret | "" | yes | systemd jwt_secret in Enterprise production |
auth.token_expiry | 8h | no | administrator/API JWT |
auth.agent_token_ttl | 8h | no | comment range 4h–8h; Agent JWT, not admin |
auth.agent_refresh_before | 20m | no | Agent rotation |
auth.agent_expired_grace | 2m | no | heartbeat-only grace |
auth.default_admin.username | admin | no | |
auth.default_admin.password | "" | yes | first install only; systemd admin_initial_password |
agents.auto_register must remain false in production (enrollment is a separate procedure).
Procedure — login (API)
curl -sS -X POST https://manager.example.com:8443/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"YOUR_PASSWORD"}'If the account has TOTP enabled, include two_factor_code.
Request fields (LoginRequest):
Scroll sideways to see the full table. Use the arrow keys when focused.
| Field | Required |
|---|---|
username | yes |
password | yes |
two_factor_code | when 2FA is enabled on the account |
Success (200) JSON:
{
"token": "<jwt>",
"expires_at": "2026-08-21T12:00:00Z",
"username": "admin",
"role": "admin"
}The HttpOnly session_token cookie is issued for browser session bootstrap (GET /api/v1/auth/status, WebSocket). It is not placed in the JSON body. Frontends must not persist session_token in localStorage.
Use the JWT:
curl -sS https://manager.example.com:8443/api/v1/agents \
-H "Authorization: Bearer TOKEN"Procedure — login (zwallctl)
zwallctl login --manager-url https://manager.example.com:8443 --username adminThe password is read from the terminal when --password is omitted. --password exists; do not put it on shared shells.
Default CLI config: ~/.zwallctl/config.yml (mode 0600 when saved). Environment: ZWALL_MANAGER_URL, ZWALL_AUTH_TOKEN.
Unconfigured zwallctl defaults to http://localhost:8080. That is not the Manager example listen port (8443). Set --manager-url.
zwallctl logoutProcedure — default administrator
On Manager start, CreateDefaultAdmin creates the configured default username only if no row exists. If the row already exists, the in-memory bootstrap password is cleared and startup continues.
Enterprise first install: password comes from systemd credential admin_initial_password, then the credential is detached after readiness. YAML stays empty.
If first boot needs a password and none was supplied through the resolved credential contract, bootstrap fails closed.
Change the password after first login (POST /api/v1/auth/change-password, admin). Field names for that body are not a published DTO in this package; use the console form or inspect a live 4xx rather than inventing old_password/new_password here.
Procedure — TOTP (2FA)
Administrators can require TOTP. Endpoints (all admin-authenticated except as noted on login):
Scroll sideways to see the full table. Use the arrow keys when focused.
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/auth/2fa/status | whether 2FA is enabled for the current admin |
| POST | /api/v1/auth/2fa/setup | body { "password": "<current>" }; response includes secret, qr_code, backup_codes — treat as secrets |
| POST | /api/v1/auth/2fa/verify | body { "code": "<totp>" } |
| POST | /api/v1/auth/2fa/disable | body requires password and code |
| POST | /api/v1/auth/2fa/regenerate | regenerates 2FA secrets; existence only, not a public cookbook |
Login without a code on a 2FA account returns 401 {"error":"Two-factor authentication code required","requires_2fa":true}.
Procedure — users
GET /api/v1/users lists users. User JSON includes id, username, email, role, active, two_factor_enabled, created_at, last_login. Password hashes are omitted (json:"-").
POST /api/v1/users (admin) binds:
Scroll sideways to see the full table. Use the arrow keys when focused.
| Field | Required |
|---|---|
username | yes |
email | yes (JSON bind) |
password | yes (strength-validated) |
role | yes |
active | optional bind field |
Operator-facing roles: admin, viewer, release_manager. Most /api/v1 operator routes still require admin. This is not a full RBAC product.
The validator also accepts campaign_operator. That string exists for in-product updater campaign permissions, which are not a supported production upgrade path. Do not staff a deployment around campaign_operator.
DELETE /api/v1/users/:id refuses deletion of the caller’s own account.
Verification
- Login returns 200 with
token,expires_at,username,role. Set-Cookieincludes HttpOnlysession_tokenon HTTPS; JSON does not containsession_token.GET /api/v1/agentswithAuthorization: Bearersucceeds foradmin.zwallctl pingafter interactive login reaches the Manager.- Wrong password → 401
{"error":"Invalid credentials"}. Inactive account → 403{"error":"Account is inactive"}.
Expected result
An administrator JWT for API/CLI and a separate HttpOnly cookie for the console. Agents use a different JWT issued at registration (auth.agent_token_ttl); do not reuse administrator tokens on Agent hosts.
Failure modes
Scroll sideways to see the full table. Use the arrow keys when focused.
| Status / symptom | Meaning | Action |
|---|---|---|
400 Invalid request format | JSON bind failure | send username/password |
400 Secure HTTPS session required | cookie policy rejected issuance | use HTTPS; CLI still uses Bearer JWT |
401 Invalid credentials | unknown user, bad password, or lookup failure | do not distinguish these in logs that leave the host |
401 requires_2fa | TOTP required | send two_factor_code |
401 Invalid two-factor authentication code | TOTP/backup fail | retry with current code |
403 Account is inactive | active == false | another admin must reactivate |
| 429 / login rejected | LoginRateLimiter | wait; do not spray passwords |
| Manager will not start | missing jwt_secret / invalid JWT material | Enterprise: systemd credential, YAML empty |
| Default admin missing after install | bootstrap password never provided | restore admin_initial_password only through the secret contract |
Troubleshooting
- CLI connection refused on port 8080: set
--manager-urlto the TLS listener. - Console login works, API 401: Authorization header missing or expired;
auth.token_expiryexample is8h. - Cookie login fails, curl JWT works: cookie policy requires HTTPS.
- Cannot create users: caller is not
admin(viewer JWT is insufficient for/api/v1/users).
Security considerations
- Do not log passwords, JWTs, TOTP secrets, or backup codes.
- Do not store
session_tokenin localStorage. - Prefer interactive
zwallctl loginover--password. - Enterprise production: keep
auth.jwt_secretandauth.default_admin.passwordempty in YAML.zwallctl secret-rotateon Enterprise defers toscripts/setup.sh --manager-secret-custody rotate(not certified). - Community
secret-rotate [jwt|db|all]still talks to the Manager API; still do not commit secrets.
Limitations
- No OIDC/LDAP/SSO.
- Named roles exist; most mutating operator APIs require
admin. - Change-password JSON field names are not published as a DTO in this package.
- Secret-custody installer is implemented pending final security acceptance.
Related documentation
- API authentication
- Console
- Identity
- Secret custody (
setup.sh --manager-secret-custody) is implemented pending final security acceptance. Details are provided separately to Enterprise customers. - zwallctl auth
- Agent/Manager trust