Bind-Aware Authentication

Gateway and UI automatically enforce stricter auth when bound to external interfaces

The gateway and chat UI change security behaviour based on the interface they bind to — permissive on loopback, strict on external. External binds also refuse a known-weak/placeholder auth_token — see Weak / placeholder secret guard.

python
from praisonaiagents import Agent

agent = Agent(
    name="Local Agent",
    instructions="You are a helpful assistant.",
)
# Serve on loopback — no token required
# $ praisonai gateway start --host 127.0.0.1
agent.start("hello")

The user opens the gateway UI or WebSocket client; bind-aware auth requires a token only when the server listens on a non-loopback host.

Diagram

Bind-Aware Auth

Yes 127.0.0.1

No 0.0.0.0

🔌 Bind Host

🔍 Loopback?

🟢 local mode
no token required

🔒 token mode
auth required

Quick Start

  1. Local development (loopback — permissive)
    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="Local Agent",
        instructions="You are a helpful assistant.",
    )
    
    # Serve via gateway on loopback — no token needed
    # $ praisonai gateway start --host 127.0.0.1
    agent.start("hello")
  2. External deployment (strict — token required)
    bash
    # Option A: Run onboarding (recommended)
    praisonai onboard
    
    # Option B: Set a token explicitly
    export GATEWAY_AUTH_TOKEN=$(openssl rand -hex 16)
    praisonai gateway start --host 0.0.0.0

How It Works

Diagram
WebSocket ClientsBindAwareAuthGatewayUserWebSocket ClientsBindAwareAuthGatewayUseralt[auth_token set][no token]alt[H is loopback (127.0.0.1, localhost,::1)][H is external (0.0.0.0, LAN, public)]praisonai gateway start –host <H>assert_external_bind_safe(config)OK (permissive, warn if no token)OK (strict)raise GatewayStartupErrorexit 1 + fix instructionsaccept connections
ModeMeaningTrigger
localPermissive — no token requiredLoopback bind (default)
tokenToken required (auto-generated if absent on loopback)External bind (default)
passwordUsername/password authChainlit UI
trusted-proxyAuth handled upstreamReverse proxy setups

Token Source Precedence

When GatewayConfig(auth_token=...) is set, it is exported to GATEWAY_AUTH_TOKEN at gateway startup. Config wins over env. All auth paths — HTTP login, magic-link verification, WebSocket handshake — read the same secret.

Diagram

Yes

No

Config token set?

Export to env

Fall back to env token

All auth paths read same secret

Implication: Rotating the token via GatewayConfig is sufficient; you don’t need to also unset GATEWAY_AUTH_TOKEN.

Flow — “I rotated my gateway token but magic-link login still fails”: Prior to PR #1744, a stale GATEWAY_AUTH_TOKEN env var could shadow a fresh GatewayConfig.auth_token. Since PR #1744, config wins — restart the gateway and try again.


Interface Detection

Hostis_loopback()Resolved mode
127.0.0.1Truelocal
127.255.255.255Truelocal
localhostTruelocal
::1Truelocal
0.0.0.0Falsetoken
192.168.1.xFalsetoken
10.0.0.xFalsetoken
8.8.8.8 (public)Falsetoken

Auth Bypass on Loopback

A loopback-bound gateway is permissive by default — local, non-proxied requests bypass auth without any environment variable.

Diagram

No — 0.0.0.0 / LAN / public

Yes — 127.0.0.1 / localhost / ::1

No — 10.0.0.5 / 2001:db8::1

Yes — 127.0.0.2 / ::1

Yes — x-forwarded-for / via

No

false / 0 / no

unset

true / 1 / yes

🔌 Request arrives

Bind host
is loopback?

🔒 Auth required

Client host
is_loopback?

Proxy header
present?

ALLOW_LOOPBACK_BYPASS

🟢 Bypass auth

Semantic loopback client matching. The client host is checked with is_loopback(), not a fixed 3-string list. Local peers reported as 127.0.0.2, 127.255.255.255, or the expanded IPv6 form 0:0:0:0:0:0:0:1 bypass correctly instead of getting a spurious 401. Non-loopback clients (10.0.0.5, 2001:db8::1) never bypass on a loopback-bound gateway.

Proxy-header guard. A request carrying x-forwarded-for, via, x-real-ip, or x-forwarded-host never inherits loopback trust — proxied requests always require auth. A missing client address also blocks the bypass.

Decision Table

bind_hostclient_hostALLOW_LOOPBACK_BYPASSProxy header?Bypass?
127.0.0.1127.0.0.1unsetnoyes (default)
localhost127.0.0.1unsetnoyes (default)
::1::1unsetnoyes (default)
127.0.0.1127.0.0.2 / 0:0:0:0:0:0:0:1unsetnoyes (semantic)
0.0.0.0127.0.0.1unsetnono
192.168.1.10127.0.0.1unsetnono
0.0.0.0127.0.0.1true / 1noyes (force-enable)
127.0.0.1127.0.0.1false / 0nono (force-disable)
127.0.0.110.0.0.5 / 2001:db8::1anynono
127.0.0.1127.0.0.1anyyesno
anyNoneanyanyno

User Flows

Flow A — “I want a quick local demo”: Run on 127.0.0.1, no config needed. Token auto-generated, fingerprint logged (gw_****abcd), saved to ~/.praisonai/.env.

Flow B — “I want to share on my LAN”: Run praisonai onboard (30s, 3 prompts) OR export GATEWAY_AUTH_TOKEN=$(openssl rand -hex 16) → praisonai gateway start --host 0.0.0.0.

Flow C — “I’m deploying to a VPS”: Same as B, but also set CHAINLIT_USERNAME / CHAINLIT_PASSWORD for the UI, and consider TLS.

Flow D — “Lab/demo — I accept the risk of admin/admin on external”: export PRAISONAI_ALLOW_DEFAULT_CREDS=1.


Environment Variables

VariableScopeEffect
GATEWAY_AUTH_TOKENGatewayAuth token. Required on external bind. Auto-generated + saved to ~/.praisonai/.env (mode 0600) on loopback when unset.
ALLOW_LOOPBACK_BYPASSGatewayOverride for the loopback auth bypass. true/1/yes force-enables bypass even on an external bind (unsafe). false/0/no force-disables it on a loopback bind (strict auth for local dev). Unset defaults to the bind-host rule (permissive on loopback).
CHAINLIT_HOSTUIHost the UI binds to (default 127.0.0.1). Drives UI auth mode resolution.
CHAINLIT_USERNAMEUIUsername (default admin).
CHAINLIT_PASSWORDUIPassword (default admin).
PRAISONAI_ALLOW_DEFAULT_CREDSUIEscape hatch. Set to 1/true/yes to allow admin/admin on external bind. Unsafe — demo only.
CHAINLIT_AUTH_SECRETUISession secret. Auto-generated if unset (ephemeral per-process).

Error Reference

GatewayStartupError — raised by assert_external_bind_safe() when binding externally without a token:

Cannot bind to 0.0.0.0 without an auth token.
Fix:  praisonai onboard         (30 seconds, 3 prompts)
Or:   export GATEWAY_AUTH_TOKEN=$(openssl rand -hex 16)

UIStartupError — raised by register_password_auth() when admin/admin used on external bind:

Cannot bind to 0.0.0.0 with default admin/admin credentials.
Fix:  export CHAINLIT_USERNAME=myuser CHAINLIT_PASSWORD=mypass
Lab:  export PRAISONAI_ALLOW_DEFAULT_CREDS=1  (demo only)

Token Fingerprinting

Logs now show gw_****XXXX (last 4 chars), never the raw token. This is implemented by get_auth_token_fingerprint() for safe logging. Retrieve the full token from ~/.praisonai/.env if needed.


Which Option When

Diagram

My laptop only

LAN for my team

Public internet

No

Yes

Where will it run?

127.0.0.1 — just run it

Have a token?

0.0.0.0 + token + TLS + custom creds

praisonai onboard

export GATEWAY_AUTH_TOKEN=…


Rotating the token

When the operator changes auth_token and reloads, every session authenticated under the previous secret is force-closed with WebSocket close code 4001 and reason credentials_rotated. Clients should re-authenticate rather than backing off. Defaults on; opt out with gateway.revoke_on_secret_rotation: false.

Gateway Credential Rotation

Full rotation behaviour, the opt-out toggle, and client-side recovery


Weak / placeholder secret guard

A publicly-known placeholder like change-me is no protection at all — external binds refuse to start on a weak auth_token, and loopback prints a warning.

Diagram

No — strong secret

Yes

External 0.0.0.0 / LAN / public

Loopback 127.0.0.1 / ::1

🔑 auth_token

Known-weak?

🟢 Start normally

Bind host?

🛑 Fail closed
WeakGatewaySecretError
exit 78

⚠️ Start + warn

Behaviour matrix

Bindauth_token weak?Behaviour
External (0.0.0.0, LAN, public IP)YesFail closed — raise WeakGatewaySecretError; gateway does not start. Exits EX_CONFIG (78) — the supervisor should not restart.
ExternalNoStart normally.
Loopback (127.0.0.1, localhost, ::1)YesWarn — gateway starts (permissive-loopback posture) but logs a warning naming the field.
LoopbackNoStart normally, no warning.

The denylist

KNOWN_WEAK_SECRETS is a frozenset of well-known placeholder values. The lookup is whitespace-stripped and case-insensitive, so CHANGE-ME, change-me , and Change-Me all match. Empty and None also count as weak.

Known-weak values
change-me, changeme, change-me-now, changemenow
your-token-here, your_token_here, your-secret-here
secret, password, passwd, test, token, admin
default, example, placeholder, none, null, todo
$(openssl rand -hex 16), $(openssl rand -hex 32)

The refusal message

On an external bind with a weak token, the gateway raises WeakGatewaySecretError with this exact message:

bash
Refusing to start: gateway.auth_token is a known-weak/placeholder value.
A publicly-known secret provides no real authentication.
Fix:  praisonai onboard         (30 seconds, 3 prompts)
Or:   export GATEWAY_AUTH_TOKEN="$(openssl rand -hex 16)"  (run in a shell so the command is expanded, not pasted literally)

Fix a weak token

  1. Option A — run onboarding (recommended)
    bash
    praisonai onboard
  2. Option B — generate a strong token
    bash
    export GATEWAY_AUTH_TOKEN="$(openssl rand -hex 16)"
    praisonai gateway start --host 0.0.0.0
  3. Option C — let doctor repair it (fastest if gateway.yaml is open)
    bash
    praisonai gateway doctor --config gateway.yaml --fix --dry-run   # preview
    praisonai gateway doctor --config gateway.yaml --fix             # apply

    --fix mints a strong token, persists it to ~/.praisonai/.env, and rewrites an explicit weak auth_token in gateway.yaml in place (${ENV} references are preserved). See Gateway CLI › Auto-repair.

Programmatic use

python
from praisonaiagents.gateway import (
    is_weak_secret,
    assert_gateway_secret_strong,
    WeakGatewaySecretError,
)

assert not is_weak_secret("my-strong-token")

try:
    assert_gateway_secret_strong("change-me", field="gateway.auth_token")
except WeakGatewaySecretError as e:
    print(e)  # → the multi-line refusal message

assert_gateway_secret_strong is not bind-aware itself — it raises on any weak value. The bind-aware policy (fail-closed external / warn loopback) is applied at the wrapper call sites.


Best Practices

Use praisonai onboard for token setup

Prefer praisonai onboard over manual token creation. It handles all the setup automatically and saves the token securely.

Never commit ~/.praisonai/.env

The auto-generated .env file contains sensitive tokens. Add it to your .gitignore and never commit it to version control.

Set custom credentials before external binding

Always set CHAINLIT_USERNAME and CHAINLIT_PASSWORD before binding to external interfaces. Never use admin/admin in production.

Use PRAISONAI_ALLOW_DEFAULT_CREDS only for demos

The PRAISONAI_ALLOW_DEFAULT_CREDS=1 escape hatch should only be used for ephemeral demos or testing. Never in production.


Related