Portunus
Features

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, optional disabled flag. 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), and last_used_at.
  • Grant — per-user authorisation triple {client, listen-port range, protocol set}. client is either a named ClientName or wildcard "*".

First admin

Since v1.1.0, a fresh store is initialized through the Web UI:

  1. Start portunus-server serve.
  2. Read the one-time setup token from the server log / stderr.
  3. Open the Web UI and create the first superadmin with 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-base64

These 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,udp

Now 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_granted
  • port_outside_grant
  • protocol_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 request

Read-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 --temporary

The 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

CommandPurpose
user-add / user-list / user-get / user-removeManage users
reset-password <user_id>Local password recovery against the data dir
credential-issue / credential-list / credential-revoke / credential-rotateManage credentials
grant-add / grant-list / grant-revokeManage 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.

On this page