The gateway now ships in the
praisonai-bot package. praisonai serve gateway still works exactly as documented here; for a standalone install see praisonai-bot Migration.Quick Start
How It Works
At connect time the gateway stamps each authenticated session with a truncated SHA-256 fingerprint of the active secret. When the reload path notices a newauth_token, it adopts it and closes every session whose stamp doesn’t match the new fingerprint. Sessions on the new secret are left untouched. The raw secret is never logged — only the digest.
The CREDENTIALS_ROTATED close reason is described in the SDK as:
The shared gateway secret this session authenticated under is no longer the active secret (an operator rotated auth_token and hot-reloaded, or otherwise revoked it). The server force-closes every session stamped with a stale secret so a leaked/revoked credential stops working within one reload cycle, without a full process restart. Clients should re-authenticate and reconnect with fresh credentials rather than backing off as if the server were down.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
gateway.auth_token | str | — | Shared secret. Env substitution (${VAR}) supported. Non-string values are coerced to str, so unquoted YAML integers don’t crash reload. |
gateway.revoke_on_secret_rotation | bool / str | true | When true (default), rotating auth_token force-closes every live session on the old secret. When false (also accepts "false", "0", "no", "off"), the new secret is adopted only for new connections; existing sessions stay connected under the old secret. |
Rotation Behaviour — Decision Matrix
When does rotation actually happen?Situation in reloaded gateway.yaml | Outcome |
|---|---|
auth_token absent | Full no-op. Running secret + live sessions untouched. |
auth_token present, resolves to empty ("" or unset ${VAR}) | Running secret kept. A warning is logged so an operator “clearing the secret” isn’t left guessing. |
auth_token present, string, same as running secret | No-op (returns 0 revoked). |
auth_token present, differs from running secret | Adopt new secret, export to GATEWAY_AUTH_TOKEN, revoke stale live sessions (unless opted out). |
auth_token is not a string (e.g. auth_token: 12345) | Coerced to str before env assignment, avoiding a mid-reload TypeError. |
revoke_on_secret_rotation: false | Secret still adopted for new connections; live sessions left connected under the old secret. |
revoke_on_secret_rotation: "false" / "0" / "no" / "off" | Also disables revocation. Normalised via str(...).strip().lower() in {"1","true","yes","on"}. |
revoke_on_secret_rotation truthy (default) | Every session whose stamped fingerprint no longer matches is force-closed with (4001, "credentials_rotated"). |
No auth_token configured at all (loopback mode) | Sessions map to the sentinel "no-auth" and are handled consistently. |
Client-Side — How to Re-authenticate
Force-closed sessions receive:| Field | Value |
|---|---|
| WebSocket close code | 4001 |
| WebSocket close reason | credentials_rotated (from GatewayCloseCode.CREDENTIALS_ROTATED) |
| Recovery hint | ConnectRecoveryStep.REAUTHENTICATE |
(4001, "credentials_rotated") as “re-authenticate with fresh credentials” — not as “server is down, back off”. A well-behaved client:
- Reads the new secret from its own credential source (env, secrets manager,
praisonai auth login). - Reconnects immediately with the new token.
- Reports back to the operator if the fresh token also fails.
Security Notes
- The raw
auth_tokenis never logged, exported, or persisted by this feature — only a 16-char truncated SHA-256 stamp is kept per session for comparison. - When rotation adopts a new secret, it also updates
GATEWAY_AUTH_TOKENin the environment so all auth paths (HTTP, magic-link, WS) read the same value — matching Bind-Aware Auth (config wins over env). - If
auth_tokenis declared but resolves to empty (e.g. an unset${VAR}), the previous secret is kept and a warning is logged instead of silently clearing auth. - Loopback-only gateways (no
auth_token) map to the sentinel"no-auth"— rotation is meaningless in that mode.
Common Patterns
Scheduled quarterly rotation
Suspected leak — revoke now
Coordinated cutover — adopt now, evict later
revoke_on_secret_rotation back to true and reload to evict any laggards still on the old secret.
Best Practices
Keep revocation on by default
Keep revocation on by default
Instant revocation is the whole point. Opt out only for staged cutovers where clients need a grace window on the old secret.
Quote your token values
Quote your token values
Write
auth_token: "12345" with quotes. The non-string coercion path exists as a safety net, not a style guide.Check the warning if nothing rotates
Check the warning if nothing rotates
If you see
auth_token present in reloaded config but resolved to empty; keeping the previous secret, your ${VAR} isn’t set — the old secret is still active.Branch on the wire code, not the message
Branch on the wire code, not the message
4001 plus reason credentials_rotated is stable; the human-readable close message is not. Branch on the code and reason.Related
Gateway Config Reload
Hot-reload architecture this feature plugs into
Bind-Aware Auth
Where the shared
auth_token is required at allGateway
Overview plus all other gateway features
Gateway Edge Protections
Other WebSocket close codes (4028, 4029) and pre-auth guards

