fix(auth): share internal gateway token across workers (#3184)

* fix(auth): share internal gateway token across workers

* fix: restore deploy script executable bit

* Update deploy.sh

to skip the auth_token setup for the down command

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Stellar鱼
2026-05-26 23:19:57 +08:00
committed by GitHub
parent e344be8d94
commit b00749a8a6
6 changed files with 92 additions and 5 deletions
+5
View File
@@ -50,6 +50,11 @@ INFOQUEST_API_KEY=your-infoquest-api-key
# Set to "false" to disable Swagger UI, ReDoc, and OpenAPI schema in production # Set to "false" to disable Swagger UI, ReDoc, and OpenAPI schema in production
# GATEWAY_ENABLE_DOCS=false # GATEWAY_ENABLE_DOCS=false
# Shared internal Gateway auth token for multi-worker deployments.
# `make up` generates and persists this automatically; set it manually only
# when you run Gateway workers outside the bundled deploy script.
# DEER_FLOW_INTERNAL_AUTH_TOKEN=your-shared-internal-token
# ── Frontend SSR → Gateway wiring ───────────────────────────────────────────── # ── Frontend SSR → Gateway wiring ─────────────────────────────────────────────
# The Next.js server uses these to reach the Gateway during SSR (auth checks, # The Next.js server uses these to reach the Gateway during SSR (auth checks,
# /api/* rewrites). They default to localhost values that match `make dev` and # /api/* rewrites). They default to localhost values that match `make dev` and
+15 -4
View File
@@ -1,23 +1,34 @@
"""Process-local authentication for Gateway internal callers.""" """Authentication for trusted Gateway internal callers."""
from __future__ import annotations from __future__ import annotations
import os
import secrets import secrets
from types import SimpleNamespace from types import SimpleNamespace
from deerflow.runtime.user_context import DEFAULT_USER_ID from deerflow.runtime.user_context import DEFAULT_USER_ID
INTERNAL_AUTH_HEADER_NAME = "X-DeerFlow-Internal-Token" INTERNAL_AUTH_HEADER_NAME = "X-DeerFlow-Internal-Token"
_INTERNAL_AUTH_TOKEN = secrets.token_urlsafe(32) INTERNAL_AUTH_ENV_VAR = "DEER_FLOW_INTERNAL_AUTH_TOKEN"
def _load_internal_auth_token() -> str:
token = os.environ.get(INTERNAL_AUTH_ENV_VAR)
if token:
return token
return secrets.token_urlsafe(32)
_INTERNAL_AUTH_TOKEN = _load_internal_auth_token()
def create_internal_auth_headers() -> dict[str, str]: def create_internal_auth_headers() -> dict[str, str]:
"""Return headers that authenticate same-process Gateway internal calls.""" """Return headers that authenticate trusted Gateway internal calls."""
return {INTERNAL_AUTH_HEADER_NAME: _INTERNAL_AUTH_TOKEN} return {INTERNAL_AUTH_HEADER_NAME: _INTERNAL_AUTH_TOKEN}
def is_valid_internal_auth_token(token: str | None) -> bool: def is_valid_internal_auth_token(token: str | None) -> bool:
"""Return True when *token* matches the process-local internal token.""" """Return True when *token* matches this Gateway worker's internal token."""
return bool(token) and secrets.compare_digest(token, _INTERNAL_AUTH_TOKEN) return bool(token) and secrets.compare_digest(token, _INTERNAL_AUTH_TOKEN)
+35
View File
@@ -0,0 +1,35 @@
"""Tests for Gateway internal auth token handling."""
from __future__ import annotations
import importlib
def test_internal_auth_uses_shared_env_token(monkeypatch):
import app.gateway.internal_auth as internal_auth
monkeypatch.setenv("DEER_FLOW_INTERNAL_AUTH_TOKEN", "shared-token")
reloaded = importlib.reload(internal_auth)
try:
headers = reloaded.create_internal_auth_headers()
assert headers[reloaded.INTERNAL_AUTH_HEADER_NAME] == "shared-token"
assert reloaded.is_valid_internal_auth_token("shared-token") is True
assert reloaded.is_valid_internal_auth_token("other-token") is False
finally:
monkeypatch.delenv("DEER_FLOW_INTERNAL_AUTH_TOKEN", raising=False)
importlib.reload(reloaded)
def test_internal_auth_generates_process_local_fallback(monkeypatch):
import app.gateway.internal_auth as internal_auth
monkeypatch.delenv("DEER_FLOW_INTERNAL_AUTH_TOKEN", raising=False)
reloaded = importlib.reload(internal_auth)
try:
token = reloaded.create_internal_auth_headers()[reloaded.INTERNAL_AUTH_HEADER_NAME]
assert token
assert reloaded.is_valid_internal_auth_token(token) is True
finally:
importlib.reload(reloaded)
+1
View File
@@ -168,6 +168,7 @@ services:
- DEER_FLOW_HOME=/app/backend/.deer-flow - DEER_FLOW_HOME=/app/backend/.deer-flow
- DEER_FLOW_CHANNELS_LANGGRAPH_URL=${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api} - DEER_FLOW_CHANNELS_LANGGRAPH_URL=${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api}
- DEER_FLOW_CHANNELS_GATEWAY_URL=${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001} - DEER_FLOW_CHANNELS_GATEWAY_URL=${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001}
- DEER_FLOW_INTERNAL_AUTH_TOKEN=${DEER_FLOW_INTERNAL_AUTH_TOKEN:-}
- DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_ROOT}/backend/.deer-flow - DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_ROOT}/backend/.deer-flow
- DEER_FLOW_HOST_SKILLS_PATH=${DEER_FLOW_ROOT}/skills - DEER_FLOW_HOST_SKILLS_PATH=${DEER_FLOW_ROOT}/skills
- DEER_FLOW_SANDBOX_HOST=host.docker.internal - DEER_FLOW_SANDBOX_HOST=host.docker.internal
+2
View File
@@ -16,6 +16,7 @@
# DEER_FLOW_DOCKER_SOCKET — Docker socket path, default /var/run/docker.sock # DEER_FLOW_DOCKER_SOCKET — Docker socket path, default /var/run/docker.sock
# DEER_FLOW_REPO_ROOT — repo root (used for skills host path in DooD) # DEER_FLOW_REPO_ROOT — repo root (used for skills host path in DooD)
# BETTER_AUTH_SECRET — required for frontend auth/session security # BETTER_AUTH_SECRET — required for frontend auth/session security
# DEER_FLOW_INTERNAL_AUTH_TOKEN — shared internal Gateway auth token for multi-worker IM channels
# #
# LangSmith tracing is disabled by default (LANGSMITH_TRACING=false). # LangSmith tracing is disabled by default (LANGSMITH_TRACING=false).
# Set LANGSMITH_TRACING=true and LANGSMITH_API_KEY in .env to enable it. # Set LANGSMITH_TRACING=true and LANGSMITH_API_KEY in .env to enable it.
@@ -101,6 +102,7 @@ services:
- DEER_FLOW_EXTENSIONS_CONFIG_PATH=/app/backend/extensions_config.json - DEER_FLOW_EXTENSIONS_CONFIG_PATH=/app/backend/extensions_config.json
- DEER_FLOW_CHANNELS_LANGGRAPH_URL=${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api} - DEER_FLOW_CHANNELS_LANGGRAPH_URL=${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api}
- DEER_FLOW_CHANNELS_GATEWAY_URL=${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001} - DEER_FLOW_CHANNELS_GATEWAY_URL=${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001}
- DEER_FLOW_INTERNAL_AUTH_TOKEN=${DEER_FLOW_INTERNAL_AUTH_TOKEN}
# DooD path/network translation # DooD path/network translation
- DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_HOME} - DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_HOME}
- DEER_FLOW_HOST_SKILLS_PATH=${DEER_FLOW_REPO_ROOT}/skills - DEER_FLOW_HOST_SKILLS_PATH=${DEER_FLOW_REPO_ROOT}/skills
+34 -1
View File
@@ -71,7 +71,7 @@ if [ -z "$DEER_FLOW_CONFIG_PATH" ]; then
export DEER_FLOW_CONFIG_PATH="$REPO_ROOT/config.yaml" export DEER_FLOW_CONFIG_PATH="$REPO_ROOT/config.yaml"
fi fi
if [ ! -f "$DEER_FLOW_CONFIG_PATH" ]; then if [ "$CMD" != "down" ] && [ ! -f "$DEER_FLOW_CONFIG_PATH" ]; then
# Try to seed from repo (config.example.yaml is the canonical template) # Try to seed from repo (config.example.yaml is the canonical template)
if [ -f "$REPO_ROOT/config.example.yaml" ]; then if [ -f "$REPO_ROOT/config.example.yaml" ]; then
cp "$REPO_ROOT/config.example.yaml" "$DEER_FLOW_CONFIG_PATH" cp "$REPO_ROOT/config.example.yaml" "$DEER_FLOW_CONFIG_PATH"
@@ -140,6 +140,38 @@ if [ -z "$BETTER_AUTH_SECRET" ]; then
fi fi
fi fi
# ── DEER_FLOW_INTERNAL_AUTH_TOKEN ────────────────────────────────────────────
# Shared by all Gateway workers so channel workers can call internal Gateway
# APIs even when the request is handled by a different Uvicorn worker.
_internal_auth_token_file="$DEER_FLOW_HOME/.internal-auth-token"
if [ "$CMD" != "down" ] && [ -z "$DEER_FLOW_INTERNAL_AUTH_TOKEN" ]; then
if [ -f "$_internal_auth_token_file" ]; then
export DEER_FLOW_INTERNAL_AUTH_TOKEN
DEER_FLOW_INTERNAL_AUTH_TOKEN="$(cat "$_internal_auth_token_file")"
echo -e "${GREEN}✓ DEER_FLOW_INTERNAL_AUTH_TOKEN loaded from $_internal_auth_token_file${NC}"
else
export DEER_FLOW_INTERNAL_AUTH_TOKEN
if command -v python3 > /dev/null 2>&1 && \
DEER_FLOW_INTERNAL_AUTH_TOKEN="$(python3 -c 'import sys; sys.version_info >= (3, 6) or sys.exit(1); import secrets; print(secrets.token_urlsafe(32))' 2>/dev/null)"; then
true
elif command -v python > /dev/null 2>&1 && \
DEER_FLOW_INTERNAL_AUTH_TOKEN="$(python -c 'import sys; sys.version_info >= (3, 6) or sys.exit(1); import secrets; print(secrets.token_urlsafe(32))' 2>/dev/null)"; then
true
elif command -v openssl > /dev/null 2>&1 && \
DEER_FLOW_INTERNAL_AUTH_TOKEN="$(openssl rand -hex 32)"; then
true
else
echo -e "${RED}✗ Cannot generate DEER_FLOW_INTERNAL_AUTH_TOKEN: python3, python, and openssl are all unavailable.${NC}" >&2
echo -e "${RED} Set DEER_FLOW_INTERNAL_AUTH_TOKEN manually before running make up.${NC}" >&2
exit 1
fi
echo "$DEER_FLOW_INTERNAL_AUTH_TOKEN" > "$_internal_auth_token_file"
chmod 600 "$_internal_auth_token_file"
echo -e "${GREEN}✓ DEER_FLOW_INTERNAL_AUTH_TOKEN generated → $_internal_auth_token_file${NC}"
fi
fi
# ── detect_sandbox_mode ─────────────────────────────────────────────────────── # ── detect_sandbox_mode ───────────────────────────────────────────────────────
detect_sandbox_mode() { detect_sandbox_mode() {
@@ -186,6 +218,7 @@ if [ "$CMD" = "down" ]; then
export DEER_FLOW_DOCKER_SOCKET="${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}" export DEER_FLOW_DOCKER_SOCKET="${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}"
export DEER_FLOW_REPO_ROOT="${DEER_FLOW_REPO_ROOT:-$REPO_ROOT}" export DEER_FLOW_REPO_ROOT="${DEER_FLOW_REPO_ROOT:-$REPO_ROOT}"
export BETTER_AUTH_SECRET="${BETTER_AUTH_SECRET:-placeholder}" export BETTER_AUTH_SECRET="${BETTER_AUTH_SECRET:-placeholder}"
export DEER_FLOW_INTERNAL_AUTH_TOKEN="${DEER_FLOW_INTERNAL_AUTH_TOKEN:-placeholder}"
"${COMPOSE_CMD[@]}" down "${COMPOSE_CMD[@]}" down
exit 0 exit 0
fi fi