Multi-user RBAC
Users, local Web passwords, API credentials, and per-tenant grants scoped to clients, port ranges, and protocols.
Available since v0.5.0. Replaces the single-operator model with a proper RBAC layer above the unchanged data plane.
Concepts
- User — id (lower-snake
[a-z][a-z0-9_-]{0,31}), role (superadmin/user), display name, optionaldisabledflag. Reserved_-prefix is rejected via the public constructor. - Password — since v1.1.0, Web UI login uses a local password hashed in PHC/Argon2id format. Admins create users manually and can require a password change on first login.
- Credential — a bearer API token tied to a user.
blake3-hashed at rest; raw token visible only at issuance / rotation. Carries an optional label, status (active/revoked), andlast_used_at. - Grant — per-user authorisation triple
{client, listen-port range, protocol set}.clientis either a namedClientNameor wildcard"*".
First admin
Since v1.1.0, a fresh store is initialized through the Web UI:
- Start
portunus-server serve. - Read the one-time setup token from the server log / stderr.
- Open the Web UI and create the first
superadminwith a local password.
The setup token expires after 30 minutes. If onboarding is not completed, each
server process start rotates the token and invalidates the previous one. After
the first active superadmin exists, onboarding stays closed.
The older bearer bootstrap paths remain available for automation and upgrade compatibility.
Path A — bootstrap-superadmin
portunus-server --data-dir ./srv bootstrap-superadmin --name ops
# → superadmin user_id=_superadmin token=<paste-into-PORTUNUS_OPERATOR_TOKEN>The bearer prints exactly once. Idempotent — refuses to bootstrap twice and exits non-zero.
Path B — server.toml shortcut
operator_token = "<43-char URL-safe-base64 token>"On first start with no superadmin present, the server auto-mints a
reserved _legacy superadmin keyed to that token. Idempotent — leave
the line in across restarts.
Generate a token out-of-band:
portunus-server gen-token
# → 43-char URL-safe-base64These paths create API-token access, not a browser password. For the Web UI, prefer the onboarding flow.
Add a constrained tenant
portunus-server user-add alice --display-name Alice
portunus-server credential-issue alice --label laptop
# → bearer token printed once, store securely
portunus-server grant-add --user-id alice --client edge-01 \
--listen-port-start 30000 --listen-port-end 30050 \
--protocols tcp,udpNow alice can push-rule only on edge-01, only on ports
30000..=30050, only TCP or UDP.
In the Web UI, a superadmin can create the same user with an initial password
and force-change flag. Public self-registration is intentionally not supported,
because grants are operator-managed.
Closed-set matching
A single grant must cover the entire requested listen range — rules straddling two grants are rejected. Outside the envelope, alice gets HTTP 403 with one of:
client_not_grantedport_outside_grantprotocol_not_granted
A --client * grant matches any client. A grant whose
--listen-port-end equals --listen-port-start is a single-port grant.
API token rotation
A user can rotate their own credential without superadmin help:
PORTUNUS_OPERATOR_TOKEN=<alice's old token> \
portunus-server credential-rotate alice <credential_id>
# → new bearer printed once; old bearer 401s on next requestRead-side filtering
GET /v1/rules projects only the caller's owned rules to non-superadmin
users. Superadmin gets ?owner=<user_id> to filter by owner. Every
rule response carries an owner field stamped at push time.
Audit log
Every operator request emits one structured event = "operator.allow"
(INFO) or "operator.deny" (WARN) line carrying actor, method,
path, outcome, and (on deny) the RbacError::code() reason.
Raw bearer tokens never reach the audit code path — the audit
emitter takes only the post-verify OperatorIdentity.
Password-reset audit entries use action = "operator.password_reset" and record
actor, target user, outcome, and whether Web sessions / API tokens were
revoked. Raw passwords, temporary passwords, setup tokens, and bearer tokens are
never written to audit.
Password recovery
Normal users do not self-reset through a public "forgot password" form. A
superadmin resets passwords from the Web UI. Resetting revokes Web sessions
and revokes API tokens by default unless the admin explicitly keeps them.
If the last superadmin loses the password, stop the server and run the local
break-glass command on the host that owns the data dir. Use the actual
superadmin user ID. For bootstrap-superadmin installs that ID is
_superadmin; for Web onboarding it is the ID chosen during setup, for example
admin.
portunus-server --data-dir /var/lib/portunus reset-password admin --temporaryThe command prints a temporary password once and requires a password change at
next Web login. It never creates a new superadmin and there is no remote reset
backdoor.
CLI surface
| Command | Purpose |
|---|---|
user-add / user-list / user-get / user-remove | Manage users |
reset-password <user_id> | Local password recovery against the data dir |
credential-issue / credential-list / credential-revoke / credential-rotate | Manage credentials |
grant-add / grant-list / grant-revoke | Manage grants |
All take the operator token from PORTUNUS_OPERATOR_TOKEN
(exit 4 if missing).
SIGHUP reload
On SIGHUP the operator-store reloads identity.json (or the SQLite
state, since v0.8) from disk. On validation failure the prior in-memory
snapshot is kept; one structured log line is emitted either way.
Cascade ordering
On user removal or grant revocation, identity flush commits first, then dependent rules are removed. A crash mid-cascade leaves a coherent identity state.
Last-superadmin protection refuses the removal that would orphan the cluster.