Skip to content

fix: fail container startup when JWT_SECRET is unset instead of falling back to 'change-me' in nginx config - #15

Merged
man4ish merged 1 commit into
mainfrom
fix/dockerfile-jwt-secret-fallback
Sep 1, 2026
Merged

fix: fail container startup when JWT_SECRET is unset instead of falling back to 'change-me' in nginx config#15
man4ish merged 1 commit into
mainfrom
fix/dockerfile-jwt-secret-fallback

Conversation

@man4ish

@man4ish man4ish commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

What

Follow-up to PR #14. That PR fixed the silent auth bypass on the Python/JWT-validation side (api/auth.py now refuses to start if AUTH_ENABLED=true and JWT_SECRET is unset). This PR fixes the same class of problem in the architecturally separate Dockerfile/nginx-config-generation path.

Where and when this happens

Dockerfile's CMD is a bash -c "..." script that runs at container start time (not build time — env vars like JWT_SECRET aren't available during docker build at all; this is plain shell parameter expansion evaluated when the container process actually launches). It previously did:

printf 'server { ... X-Devhub-Internal "%s"; ... }\n' "${JWT_SECRET:-change-me}" > /etc/nginx/conf.d/devhub.conf && \
nginx && \
uvicorn api.main:app ...

If JWT_SECRET was unset, this silently wrote the literal string "change-me" into nginx's config as the internal UI-proxy trust header, then continued straight into starting nginx and serving traffic — no error, no warning, nothing.

The fix

Added a fail-fast check as the very first thing the CMD script does — before the Ollama-readiness wait, before the index build, and before nginx starts:

_auth_enabled=$(printf '%s' "${AUTH_ENABLED:-}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
if [ "$_auth_enabled" = 'true' ] && [ -z "${JWT_SECRET:-}" ]; then
  echo '❌ AUTH_ENABLED is true but JWT_SECRET is not set -- refusing to start ...' >&2
  exit 1
fi

Also dropped the :-change-me fallback entirely from the printf line — it's now "${JWT_SECRET:-}", so if this check somehow doesn't fire (i.e. AUTH_ENABLED isn't true), an unset secret becomes an empty string in the config rather than a plausible-looking fake credential.

Design choice — gated on AUTH_ENABLED, matching PR #14, not unconditional: I considered making this unconditional (always require JWT_SECRET), but that would break every existing local/dev deployment that runs with AUTH_ENABLED unset/false and never sets JWT_SECRET (the documented default, per README's Authentication section) — the internal header is provably irrelevant in that case since require_auth() short-circuits to "system" before ever looking at it. Gating on AUTH_ENABLED=true keeps this consistent with PR #14's exact same condition and avoids an unnecessary breaking change. Flagging this choice explicitly in case you intended unconditional enforcement.

Before / after

BeforeAUTH_ENABLED=true, JWT_SECRET unset: container started, nginx served traffic with X-Devhub-Internal: change-me baked in, looked fully operational.

After — same misconfiguration: container refuses to start.

$ docker run --name devhub-jwt-fallback-test -e AUTH_ENABLED=true omnibioai-dev-hub:jwt-fallback-test
❌ AUTH_ENABLED is true but JWT_SECRET is not set -- refusing to start with the internal UI-proxy auth header silently falling back to a placeholder.
   Set JWT_SECRET before starting this container.
$ echo $?
1
$ docker inspect devhub-jwt-fallback-test --format "ExitCode: {{.State.ExitCode}}, Running: {{.State.Running}}"
ExitCode: 1, Running: false

No "Waiting for Ollama..." line even printed — the check fires before any other startup work, and confirmed via docker inspect: container is not running, exited 1. nginx never started.

Correctly configuredAUTH_ENABLED=true, JWT_SECRET=a-real-shared-secret — starts normally, works end-to-end through the nginx proxy:

$ docker run -d --name devhub-jwt-ok-test --add-host=ollama:host-gateway \
    -p 18083:8082 -p 15174:5173 \
    -v "$(pwd)/data/faiss_index:/app/data/faiss_index:ro" \
    -e AUTH_ENABLED=true -e JWT_SECRET=a-real-shared-secret \
    omnibioai-dev-hub:jwt-fallback-test
$ docker logs devhub-jwt-ok-test
⏳ Waiting for Ollama...
✅ Ollama is ready
✅ Index already exists, skipping build
🌐 nginx started on port 5173
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8082

$ docker exec devhub-jwt-ok-test cat /etc/nginx/conf.d/devhub.conf | grep X-Devhub-Internal
    location /rag/ { proxy_pass http://127.0.0.1:8082; proxy_set_header X-Devhub-Internal "a-real-shared-secret"; }
    # (confirms the REAL secret is baked in, not "change-me")

$ curl -X POST http://127.0.0.1:15174/rag/query -H "Authorization: Bearer <valid-jwt>" -d '{"query":"What is FAISS used for?"}'
{"query":"What is FAISS used for?","answer":"According to the provided context, FAISS ...","context_used":5,"version":"v6-faiss","api_version":"v6"} 
HTTP_STATUS:200

$ curl -X POST http://127.0.0.1:18083/rag/query -d '{"query":"test"}'   # direct to FastAPI, bypassing nginx, no token
{"detail":"Authorization header is missing"}
HTTP_STATUS:401   # (confirms the external-JWT path, bypassing nginx, is unaffected too)

(Note: a request through nginx succeeds without a client-supplied JWT even in the "no token" case — that's pre-existing, correct behavior, not something this PR changes: nginx unconditionally attaches the trusted internal header to every /rag/ request it proxies, since that's how same-origin browser UI traffic authenticates. Only requests that bypass nginx entirely need a real JWT, confirmed above.)

pytest results

pytest -q199 passed — unaffected, as expected (no Python code touched, Dockerfile-only change).

Scope

1 file: Dockerfile (CMD step only).

⚠️ Do not auto-merge

This changes container startup behavior. Per instructions, not merged automatically — results reported above; merge only after explicit confirmation.

…ng back to 'change-me' in nginx config

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RttwcxBgsnXH2TwQGQsrp
@man4ish
man4ish merged commit 1a0e78a into main Sep 1, 2026
2 of 3 checks passed
@man4ish
man4ish deleted the fix/dockerfile-jwt-secret-fallback branch September 1, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant