Repository navigation
fix(nginx): stop Control UI Bearer tokens from clobbering basic auth - #97
Conversation
The Control UI authenticates to the gateway with `Authorization: Bearer <token>`. A request carries only one Authorization header, so on every fetch the UI makes itself, that Bearer replaces the browser's Basic credentials. auth_basic cannot parse it, returns 401 + WWW-Authenticate, and the browser prompts again. Entering the correct password cannot help: the next UI fetch overwrites the header again. This is the same class of bug as #37 (webhook Authorization header overwritten by the nginx proxy), and it is fixed the same way PR #38 fixed that one: skip basic auth for the affected routes and pass the client's token through so the gateway validates it. Scoped to routes the gateway itself authenticates (/api/, /__openclaw__/, /rpc, /control-ui-config.json). "/" is deliberately excluded: the gateway serves the UI shell unauthenticated, so exempting it would let any bogus Bearer past basic auth. The nginx-injected `token=` query parameter is suppressed on passthrough requests for the same reason, otherwise the carve-out would hand out authenticated access instead of delegating it. When AUTH_PASSWORD is unset the generated config is byte-identical to before. Refs #65
Skip basic auth only for the exact paths the Control UI fetches with Authorization: Bearer, and forward the client's header instead of injecting the gateway token. Anchor every regex. Keep /, the WebSocket, and /browser/ on basic auth, and leave the generated config unchanged when AUTH_PASSWORD is unset. Trust the loopback nginx hop via gateway.trustedProxies and overwrite X-Forwarded-For with the TCP peer so auth rate limits are not stuck on 127.0.0.1. Co-authored-by: Andras Bacsai <andrasbacsai@users.noreply.github.com>
…openclaw Co-authored-by: Andras Bacsai <andrasbacsai@users.noreply.github.com>
A fixed password literal in nginx-bearer.test.js was flagged by secret scanners. The harness only needs any non-empty basic-auth password, so build one from random bytes on each run. Co-authored-by: Andras Bacsai <andrasbacsai@users.noreply.github.com>
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36526964 | Triggered | Generic Password | dcaa92b | scripts/nginx-bearer.test.js | View secret |
| 36526964 | Triggered | Generic Password | 7d742c6 | scripts/nginx-bearer.test.js | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
Will this be needed everytime one logs into the control UI? i.e. need to always do the device approval |
|
@batmanscode device approval should be done once, unless you clear cookies from what I tested |
Thanks :D Off the top of your head do you see a way for login via any browser (without cookie based device approval?) |
Fixes #65
Replacement for #87. This branch merges @codezz's
fix/control-ui-bearer-vs-basic-auth(headdcaa92bd) into currentmainwith a merge commit, so @codezz's commits stay in the history. Please merge this PR with a merge commit, not squash or rebase. Squashing would dropdcaa92bd, and GitHub would not mark #87 as merged.#87
What landed
@codezz fixed the repeated basic-auth prompt: the Control UI sends
Authorization: Bearer, which replaces the browser's Basic credentials, and nginx answers401+WWW-Authenticate: Basic. nginx now skips basic auth only for the exact gateway routes that Bearer request is for, forwards the client's header, and does not append its owntoken=query parameter./, the WebSocket,/rpc, and/browser/stay on basic auth.configure.jssetsgateway.trustedProxiesto127.0.0.1and::1when that list is not already set. nginx overwritesX-Forwarded-Forwith the TCP peer while basic auth is on, so OpenClaw's auth rate limit is not stuck on loopback.gateway.auth.modestaystoken.The merge onto current
mainwas clean. Both sides are in the tree:COOLIFY_URL/COOLIFY_FQDN(fix: normalize Coolify FQDN for allowed origins #79)scripts/nginx-bearer.test.jsbuilds its basic-auth password withcrypto.randomByteson each run, so the file no longer contains a password literal.Tests
node scripts/utils.test.js: 292 passednode scripts/nginx-bearer.test.js: 55 passednode scripts/smoke.jsinside the image built fromOPENCLAW_GIT_REF=v2026.10.5:openclaw ok: OpenClaw 2026.10.5 (1b58cb9)Image build used
Dockerfile.basethenDockerfile, same as CI.pnpm build && pnpm ui:buildcompleted in one layer. The build log has noEXDEV.With basic auth, Playwright loaded the Control UI, submitted the gateway token, and a one-time device approval connected the session. No browser dialog, and no
401withWWW-Authenticate: Basicafter the page loaded.