This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new d455473 fix: remove default authentication secrets (#113)
d455473 is described below
commit d455473d8238a127c4c8178c23babedecab4278e
Author: Yijia Su <[email protected]>
AuthorDate: Wed Jul 29 19:53:21 2026 +0800
fix: remove default authentication secrets (#113)
---
.env.example | 23 +--
CHANGELOG.md | 2 +
README.md | 57 ++++---
docker-compose.yml | 6 +-
doris_mcp_server/auth/token_manager.py | 101 ++++--------
doris_mcp_server/auth/token_security_middleware.py | 9 ++
doris_mcp_server/utils/config.py | 88 ++++++++++-
doris_mcp_server/utils/db.py | 42 -----
doris_mcp_server/utils/secret_policy.py | 85 ++++++++++
test/security/test_effective_auth_config.py | 6 +-
test/security/test_static_token_bootstrap.py | 171 +++++++++++++++++++++
test/security/test_token_management_auth.py | 2 +-
tokens.json | 66 +-------
13 files changed, 435 insertions(+), 223 deletions(-)
diff --git a/.env.example b/.env.example
index bc9cd56..2b37f3d 100644
--- a/.env.example
+++ b/.env.example
@@ -59,7 +59,7 @@ DORIS_MAX_CONNECTION_AGE=3600
# Legacy configuration - kept for backward compatibility
# AUTH_TYPE is now deprecated - use individual switches above
-AUTH_TYPE=token
+# AUTH_TYPE=token
# Token Authentication (Default method - simple and effective)
ENABLE_TOKEN_AUTH=false
@@ -79,6 +79,9 @@ TOKEN_FILE_PATH=tokens.json
ENABLE_TOKEN_EXPIRY=true
DEFAULT_TOKEN_EXPIRY_HOURS=720
TOKEN_HASH_ALGORITHM=sha256
+# No static bearer token is shipped. Generate one outside source control:
+# python -c 'import secrets; print(secrets.token_urlsafe(32))'
+# TOKEN_ADMIN=<paste-generated-value-here>
# ===================================================================
# Token Management Security Configuration (NEW in v0.6.0) - CRITICAL SECURITY
SETTINGS
@@ -87,7 +90,7 @@ TOKEN_HASH_ALGORITHM=sha256
# HTTP Token Management Endpoints (DISABLED BY DEFAULT FOR SECURITY)
# WARNING: These endpoints allow creation, deletion, and management of
authentication tokens
# Only enable if you need HTTP-based token management and understand the
security implications
-ENABLE_HTTP_TOKEN_MANAGEMENT=true
+ENABLE_HTTP_TOKEN_MANAGEMENT=false
# Admin Authentication Token (REQUIRED if HTTP token management is enabled)
# This token is required to access HTTP token management endpoints
@@ -305,8 +308,8 @@ DORIS_OAUTH_TRUSTED_PROXY_CIDRS=
# DORIS_OAUTH_TRUST_PROXY_HEADERS=true
# DORIS_OAUTH_TRUSTED_PROXY_CIDRS=10.0.0.0/8,192.168.0.0/16
-# Legacy token settings (for backward compatibility)
-TOKEN_SECRET=your_secret_key_here
+# Legacy token settings (deprecated; no default secret is provided)
+TOKEN_SECRET=
TOKEN_EXPIRY=3600
# SQL security check
@@ -443,10 +446,10 @@ TEMP_FILES_DIR=tmp
# - NEW: Enable individual authentication methods using ENABLE_TOKEN_AUTH,
ENABLE_JWT_AUTH, ENABLE_OAUTH_AUTH
# - When all methods are disabled, ALL requests are allowed with anonymous
access
# - Authentication methods work independently - any one succeeding allows
access
-# - Token Auth: Change default tokens (DEFAULT_ADMIN_TOKEN, etc.) in
production
+# - Token Auth: Configure deployment-specific TOKEN_<ID> values before
enabling it
# - JWT Auth: Change JWT_SECRET_KEY and JWT_REFRESH_SECRET_KEY in production
# - OAuth Auth: Configure OAuth provider settings and secure client secrets
-# - Must change TOKEN_SECRET in production environment (legacy
compatibility)
+# - TOKEN_SECRET is deprecated and intentionally has no default value
# - Adjust BLOCKED_KEYWORDS according to business needs
# - Enable ENABLE_SECURITY_CHECK and ENABLE_MASKING
# - NEW v0.6.0: Token Management Security (CRITICAL):
@@ -488,7 +491,7 @@ TEMP_FILES_DIR=tmp
#
# Token Authentication (ENABLE_TOKEN_AUTH=true) - Recommended for most use
cases:
# - Simple and secure token-based authentication
-# - Configurable default tokens via environment variables
+# - No default credential; at least one active high-entropy token is
required
# - Support for custom tokens via TOKEN_* environment variables
# - Token file configuration via tokens.json
# - Built-in token management HTTP endpoints
@@ -576,9 +579,7 @@ TEMP_FILES_DIR=tmp
# # NEVER disable admin auth in production:
# # REQUIRE_ADMIN_AUTH=false # DANGEROUS!
#
-# # NEVER use weak admin tokens:
-# # TOKEN_MANAGEMENT_ADMIN_TOKEN=admin # DANGEROUS!
-# # TOKEN_MANAGEMENT_ADMIN_TOKEN=123456 # DANGEROUS!
+# # NEVER use short, repeated, or placeholder admin tokens.
#
# 📊 ENDPOINT SECURITY TESTING:
# # Test security (should fail):
@@ -590,7 +591,7 @@ TEMP_FILES_DIR=tmp
# # Expected: 401 Unauthorized (missing admin token)
#
# # Test with valid auth (should succeed if enabled):
-# curl -H "Authorization: Bearer your-admin-token"
http://127.0.0.1:3000/token/stats
+# curl -H "Authorization: Bearer $TOKEN_MANAGEMENT_ADMIN_TOKEN"
http://127.0.0.1:3000/token/stats
# # Expected: 200 OK with token statistics
#
# 🔍 MONITORING & AUDITING:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc211d7..6f50783 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,8 @@ under **Unreleased** until a new version is selected and
published.
- Corrected resource and prompt error semantics and tool `isError` results.
- Removed admin-token query authentication and dashboard URL propagation;
management endpoints now accept admin credentials only in headers.
+- Removed shipped static credentials and the legacy default secret; static and
+ management token modes now require explicit high-entropy credentials.
- Released Doris connections on SQL profile, data freshness, and access
analysis paths.
- Improved Doris 4 role metadata compatibility and query recovery behavior.
diff --git a/README.md b/README.md
index a2574a1..36fc9bc 100644
--- a/README.md
+++ b/README.md
@@ -135,11 +135,12 @@ Access the **Web-Based Token Management Dashboard** for
enterprise-grade token a
- **Admin Authentication**: Requires `TOKEN_MANAGEMENT_ADMIN_TOKEN` for access
- **Configuration Prerequisites**:
```bash
- # Required environment variables
- ENABLE_HTTP_TOKEN_MANAGEMENT=true
- ENABLE_TOKEN_AUTH=true
- TOKEN_MANAGEMENT_ADMIN_TOKEN=your_secure_admin_token
- TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
+ # Generate separate high-entropy credentials; do not commit them.
+ export TOKEN_ADMIN="$(python -c 'import secrets;
print(secrets.token_urlsafe(32))')"
+ export TOKEN_MANAGEMENT_ADMIN_TOKEN="$(python -c 'import secrets;
print(secrets.token_urlsafe(32))')"
+ export ENABLE_HTTP_TOKEN_MANAGEMENT=true
+ export ENABLE_TOKEN_AUTH=true
+ export TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
```
#### **Interface Access**
@@ -149,7 +150,7 @@ tokens are rejected and must not be placed in URLs, browser
history, or access
logs.
```bash
-curl -H "Authorization: Bearer your_secure_admin_token" \
+curl -H "Authorization: Bearer $TOKEN_MANAGEMENT_ADMIN_TOKEN" \
http://127.0.0.1:3000/token/stats
```
@@ -200,9 +201,10 @@ export DORIS_USER="root"
export DORIS_PASSWORD="your_password"
# Token Management Interface (Security-Critical)
+export TOKEN_ADMIN="$(python -c 'import secrets;
print(secrets.token_urlsafe(32))')"
+export TOKEN_MANAGEMENT_ADMIN_TOKEN="$(python -c 'import secrets;
print(secrets.token_urlsafe(32))')"
export ENABLE_HTTP_TOKEN_MANAGEMENT=true
export ENABLE_TOKEN_AUTH=true
-export TOKEN_MANAGEMENT_ADMIN_TOKEN="your_secure_admin_token"
export TOKEN_MANAGEMENT_ALLOWED_IPS="127.0.0.1,::1"
# Then start with simplified command
@@ -271,12 +273,10 @@ cp .env.example .env
* `DORIS_OAUTH_BASE_URL`: Public base URL used by Doris-backed OAuth
discovery and token endpoints
* `TOKEN_FILE_PATH`: Path to tokens.json file for token management
(default: tokens.json)
* `TOKEN_HOT_RELOAD`: Enable hot reloading of token configuration
(default: true)
- * `DEFAULT_ADMIN_TOKEN`: Default admin token (customizable via env)
- * `DEFAULT_ANALYST_TOKEN`: Default analyst token (customizable via env)
- * `DEFAULT_READONLY_TOKEN`: Default readonly token (customizable via env)
+ * `TOKEN_<ID>`: Explicit static bearer token; each active token must be a
+ securely generated value of at least 32 characters
* **Legacy Security Configuration**:
* `AUTH_TYPE`: Legacy authentication type (token/basic/oauth, deprecated
- use individual switches)
- * `TOKEN_SECRET`: Legacy token secret key (use token-based auth instead)
* `ENABLE_SECURITY_CHECK`: Enable/disable SQL security validation
(default: true)
* `BLOCKED_KEYWORDS`: Comma-separated list of blocked SQL keywords
* `ENABLE_MASKING`: Enable data masking (default: true)
@@ -600,6 +600,9 @@ The Doris MCP Server includes a comprehensive
enterprise-grade security framewor
Configure the new authentication system with granular control:
```bash
+# Generate a deployment-specific token before enabling static authentication.
+export TOKEN_ADMIN="$(python -c 'import secrets;
print(secrets.token_urlsafe(32))')"
+
# Individual Authentication Control (New in v0.6.0)
ENABLE_TOKEN_AUTH=true # Enable token-based authentication
ENABLE_JWT_AUTH=false # Enable JWT authentication
@@ -609,16 +612,14 @@ ENABLE_OAUTH_AUTH=false # Enable OAuth
authentication
TOKEN_FILE_PATH=tokens.json # Token configuration file
TOKEN_HOT_RELOAD=true # Enable hot reloading
-# Default Tokens (Customizable via environment)
-DEFAULT_ADMIN_TOKEN=doris_admin_token_123456
-DEFAULT_ANALYST_TOKEN=doris_analyst_token_123456
-DEFAULT_READONLY_TOKEN=doris_readonly_token_123456
-
# Legacy Configuration (Deprecated)
# AUTH_TYPE=token # Use individual switches instead
-# TOKEN_SECRET=your_secret_key # Use token-based auth instead
```
+The repository ships no usable static token or legacy secret. Startup fails
+when static token authentication is enabled without at least one active,
+high-entropy `TOKEN_<ID>` value or an equivalent entry in `TOKEN_FILE_PATH`.
+
### Doris-Backed OAuth Authentication
Doris-backed OAuth is a separate OAuth mode where Doris itself is the
authorization backend. The MCP client discovers this server's OAuth metadata,
the user signs in with a Doris username and password, the server validates
those credentials by creating a per-user Doris connection pool, and issued
`doa_` access tokens route tool calls through that Doris user's pool. MCP
scopes control which MCP operations can be called; Doris RBAC controls which
catalogs, databases, tables, and metadata t [...]
@@ -926,9 +927,9 @@ custom_rule = {
#### Environment Variables
```bash
-# .env file
-AUTH_TYPE=token
-TOKEN_SECRET=your_jwt_secret_key
+# Generate this outside source control, then inject it into the process.
+export TOKEN_SECURITY_ADMIN="$(python -c 'import secrets;
print(secrets.token_urlsafe(32))')"
+export ENABLE_TOKEN_AUTH=true
ENABLE_MASKING=true
MAX_RESULT_ROWS=10000
BLOCKED_SQL_OPERATIONS=DROP,DELETE,TRUNCATE,ALTER
@@ -1393,10 +1394,6 @@ DORIS_BE_WEBSERVER_PORT=8040
```bash
# Enable data masking
ENABLE_MASKING=true
-# Set authentication type
-AUTH_TYPE=token
-# Configure token secret
-TOKEN_SECRET=your_secret_key
# Set maximum result rows
MAX_RESULT_ROWS=10000
```
@@ -1677,7 +1674,7 @@ cat logs/doris_mcp_server_critical.log
"tokens": [
{
"token_id": "tenant-alpha",
- "token": "tenant_alpha_secure_token_123",
+ "token": "<generated-deployment-token-at-least-32-characters>",
"description": "Tenant Alpha database access",
"expires_hours": null,
"is_active": true,
@@ -1708,8 +1705,8 @@ cat logs/doris_mcp_server_critical.log
5. **Multi-Tenant Usage**:
```bash
# Different tokens access different databases automatically
- curl -H "Authorization: Bearer tenant_alpha_secure_token_123"
http://localhost:3000/mcp
- curl -H "Authorization: Bearer tenant_beta_secure_token_456"
http://localhost:3000/mcp
+ curl -H "Authorization: Bearer $TOKEN_TENANT_ALPHA"
http://localhost:3000/mcp
+ curl -H "Authorization: Bearer $TOKEN_TENANT_BETA" http://localhost:3000/mcp
```
### Q: How is Doris-backed OAuth different from external OAuth/OIDC?
@@ -1781,13 +1778,13 @@ tail -f logs/doris_mcp_server_info.log | grep "hot
reload"
# ✓ Admin authentication enabled in configuration
# Enable HTTP token management (disabled by default)
+export TOKEN_MANAGEMENT_ADMIN_TOKEN="$(python -c 'import secrets;
print(secrets.token_urlsafe(32))')"
export ENABLE_HTTP_TOKEN_MANAGEMENT=true
-export TOKEN_MANAGEMENT_ADMIN_TOKEN=your_secure_admin_token
export REQUIRE_ADMIN_AUTH=true
export TOKEN_MANAGEMENT_ALLOWED_IPS=127.0.0.1,::1
# Access with proper authentication
-curl -H "Authorization: Bearer your_secure_admin_token"
http://127.0.0.1:3000/token/stats
+curl -H "Authorization: Bearer $TOKEN_MANAGEMENT_ADMIN_TOKEN"
http://127.0.0.1:3000/token/stats
# Demo page (local access only, with authentication)
# Access: http://127.0.0.1:3000/token/demo
@@ -1803,7 +1800,7 @@ curl -H "Authorization: Bearer your_secure_admin_token"
http://127.0.0.1:3000/to
"tokens": [
{
"token_id": "dev-token",
- "token": "dev_secure_token_123",
+ "token": "<generated-development-token-at-least-32-characters>",
"description": "Development environment access",
"expires_hours": 24,
"is_active": true
diff --git a/docker-compose.yml b/docker-compose.yml
index 002cfdf..4127063 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -40,8 +40,8 @@ services:
- DORIS_MAX_CONNECTIONS=20
# Security configuration
- - AUTH_TYPE=token
- - TOKEN_SECRET=your_secret_key_here
+ - ENABLE_TOKEN_AUTH=true
+ - TOKEN_ADMIN=${MCP_STATIC_TOKEN:?Set MCP_STATIC_TOKEN to a securely
generated value of at least 32 characters}
- MAX_RESULT_ROWS=10000
# Performance configuration
@@ -215,4 +215,4 @@ networks:
driver: bridge
ipam:
config:
- - subnet: 172.20.0.0/16
\ No newline at end of file
+ - subnet: 172.20.0.0/16
diff --git a/doris_mcp_server/auth/token_manager.py
b/doris_mcp_server/auth/token_manager.py
index 4717744..bbd11a4 100644
--- a/doris_mcp_server/auth/token_manager.py
+++ b/doris_mcp_server/auth/token_manager.py
@@ -34,6 +34,10 @@ from typing import Dict, List, Optional, Any
from pathlib import Path
from ..utils.logger import get_logger
+from ..utils.secret_policy import (
+ is_static_token_environment_variable,
+ validate_high_entropy_secret,
+)
from ..utils.security import RESERVED_DORIS_OAUTH_TOKEN_PREFIX, SecurityLevel
@@ -103,11 +107,22 @@ class TokenManager:
self._file_last_modified = 0
self._hot_reload_task = None
- # Initialize with default tokens if none exist
- self._initialize_default_tokens()
-
# Load tokens from configuration
self._load_tokens()
+
+ effective_auth = getattr(config, "effective_auth", None)
+ token_auth_enabled = getattr(
+ effective_auth,
+ "enable_token_auth",
+ getattr(config.security, "enable_token_auth", False),
+ )
+ if token_auth_enabled and not any(
+ token_info.is_active for token_info in self._tokens.values()
+ ):
+ raise ValueError(
+ "Token authentication is enabled but no active static token is
configured. "
+ "Set a high-entropy TOKEN_<ID> environment variable or provide
one in tokens.json."
+ )
# Start hot reload monitoring
if self.enable_hot_reload:
@@ -122,64 +137,6 @@ class TokenManager:
f"'{RESERVED_DORIS_OAUTH_TOKEN_PREFIX}'"
)
- def _initialize_default_tokens(self):
- """Initialize default tokens for basic authentication (configurable
via environment)"""
- # Default token configurations (can be overridden by environment
variables)
- default_tokens = [
- {
- 'token_id': 'admin-token',
- 'token': os.getenv('DEFAULT_ADMIN_TOKEN',
'doris_admin_token_123456'),
- 'description': os.getenv('DEFAULT_ADMIN_DESCRIPTION', 'Default
admin API access token'),
- 'expires_hours': None # Never expires
- },
- {
- 'token_id': 'analyst-token',
- 'token': os.getenv('DEFAULT_ANALYST_TOKEN',
'doris_analyst_token_123456'),
- 'description': os.getenv('DEFAULT_ANALYST_DESCRIPTION',
'Default data analysis API access token'),
- 'expires_hours': None # Never expires
- },
- {
- 'token_id': 'readonly-token',
- 'token': os.getenv('DEFAULT_READONLY_TOKEN',
'doris_readonly_token_123456'),
- 'description': os.getenv('DEFAULT_READONLY_DESCRIPTION',
'Default read-only API access token'),
- 'expires_hours': None # Never expires
- }
- ]
-
-
- # Only add default tokens if no custom tokens are defined via
environment variables
- # Check if any TOKEN_* environment variables exist (excluding system
and legacy configs)
- excluded_prefixes = ('DEFAULT_', 'TOKEN_FILE_PATH', 'TOKEN_HASH_')
- excluded_vars = {'TOKEN_SECRET', 'TOKEN_EXPIRY'}
-
- custom_tokens_exist = any(
- key.startswith('TOKEN_') and
- not key.startswith(excluded_prefixes) and
- not key.endswith(('_EXPIRES_HOURS', '_DESCRIPTION')) and
- key not in excluded_vars
- for key in os.environ.keys()
- )
-
- # Also check if token file exists and has content
- token_file_exists = False
- if os.path.exists(self.token_file_path):
- try:
- with open(self.token_file_path, 'r') as f:
- content = f.read().strip()
- if content and content != '{}':
- token_file_exists = True
- except:
- pass
-
- # Add default tokens only if no custom configuration exists
- if not custom_tokens_exist and not token_file_exists:
- for token_config in default_tokens:
- self._add_token_from_config(token_config)
-
- self.logger.info(f"Initialized {len(default_tokens)} default
tokens (no custom config found)")
- else:
- self.logger.info("Skipped default tokens initialization (custom
tokens detected)")
-
def _add_token_from_config(self, token_config: Dict[str, Any]):
"""Add token from configuration with optional database binding"""
try:
@@ -216,6 +173,10 @@ class TokenManager:
# Hash the token
raw_token = token_config['token']
self._validate_static_token_prefix(raw_token)
+ validate_high_entropy_secret(
+ raw_token,
+ setting=f"static token '{token_info.token_id}'",
+ )
token_hash = self._hash_token(raw_token)
# Store token
@@ -251,17 +212,8 @@ class TokenManager:
token_prefixes = set()
# Find all TOKEN_ environment variables (exclude legacy and system
variables)
- excluded_token_vars = {
- 'TOKEN_SECRET', # Legacy token secret
- 'TOKEN_EXPIRY', # Legacy token expiry
- 'TOKEN_FILE_PATH', # System config
- 'TOKEN_HASH_ALGORITHM' # System config
- }
-
for key in os.environ:
- if (key.startswith('TOKEN_') and
- not key.endswith(('_EXPIRES_HOURS', '_DESCRIPTION')) and
- key not in excluded_token_vars):
+ if is_static_token_environment_variable(key):
token_id = key[6:] # Remove 'TOKEN_' prefix
token_prefixes.add(token_id)
@@ -398,6 +350,10 @@ class TokenManager:
else:
raw_token = self.generate_token()
self._validate_static_token_prefix(raw_token)
+ validate_high_entropy_secret(
+ raw_token,
+ setting=f"static token '{token_id}'",
+ )
# Calculate expiration
expires_at = None
@@ -816,9 +772,6 @@ class TokenManager:
self._tokens.clear()
self._token_ids.clear()
- # Reinitialize default tokens
- self._initialize_default_tokens()
-
# Load from file
self._load_tokens_from_file()
diff --git a/doris_mcp_server/auth/token_security_middleware.py
b/doris_mcp_server/auth/token_security_middleware.py
index d9dbc3b..b737e76 100644
--- a/doris_mcp_server/auth/token_security_middleware.py
+++ b/doris_mcp_server/auth/token_security_middleware.py
@@ -33,6 +33,7 @@ from starlette.responses import JSONResponse
from ..utils.logger import get_logger
from ..utils.config import DorisConfig
+from ..utils.secret_policy import validate_high_entropy_secret
class TokenSecurityMiddleware:
@@ -44,6 +45,14 @@ class TokenSecurityMiddleware:
# Initialize admin token hash if provided
self._admin_token_hash = None
+ if (
+ config.security.enable_http_token_management
+ and config.security.require_admin_auth
+ ):
+ validate_high_entropy_secret(
+ config.security.token_management_admin_token,
+ setting="TOKEN_MANAGEMENT_ADMIN_TOKEN",
+ )
if config.security.token_management_admin_token:
self._admin_token_hash =
self._hash_token(config.security.token_management_admin_token)
diff --git a/doris_mcp_server/utils/config.py b/doris_mcp_server/utils/config.py
index 7031aca..d2af765 100644
--- a/doris_mcp_server/utils/config.py
+++ b/doris_mcp_server/utils/config.py
@@ -37,6 +37,10 @@ except ImportError:
load_dotenv = None
from .logger import get_logger
+from .secret_policy import (
+ is_static_token_environment_variable,
+ validate_high_entropy_secret,
+)
class AuthConfigError(ValueError):
@@ -276,7 +280,7 @@ class SecurityConfig:
# Legacy configuration (kept for backward compatibility)
auth_type: str = "token" # jwt, token, basic, oauth (deprecated: use
individual switches)
- token_secret: str = "default_secret" # Legacy token secret for backward
compatibility
+ token_secret: str = "" # Deprecated legacy field; no usable default secret
token_expiry: int = 3600
# Enhanced Token Authentication Configuration
@@ -1290,6 +1294,73 @@ def build_auth_config_inputs(config: DorisConfig,
requested_workers: int | None
)
+def _configured_static_tokens(
+ security_config: SecurityConfig,
+) -> list[tuple[str, Any, bool]]:
+ """Load source labels, raw values, and active flags for configured static
tokens."""
+ configured: list[tuple[str, Any, bool]] = []
+ for name, value in os.environ.items():
+ if is_static_token_environment_variable(name):
+ configured.append((name, value, True))
+
+ token_file = Path(security_config.token_file_path)
+ if not token_file.exists():
+ return configured
+
+ try:
+ token_data = json.loads(token_file.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError) as exc:
+ raise AuthConfigError(f"Unable to load static token file {token_file}:
{exc}") from exc
+
+ if isinstance(token_data, dict) and "tokens" in token_data:
+ token_entries = token_data["tokens"]
+ elif isinstance(token_data, list):
+ token_entries = token_data
+ else:
+ raise AuthConfigError(
+ f"Static token file {token_file} must contain a tokens array"
+ )
+ if not isinstance(token_entries, list):
+ raise AuthConfigError(
+ f"Static token file {token_file} must contain a tokens array"
+ )
+
+ for index, token_entry in enumerate(token_entries):
+ if not isinstance(token_entry, dict):
+ raise AuthConfigError(
+ f"Static token file {token_file} entry {index} must be an
object"
+ )
+ token_id = str(token_entry.get("token_id") or "").strip()
+ if not token_id:
+ raise AuthConfigError(
+ f"Static token file {token_file} entry {index} requires
token_id"
+ )
+ raw_token = token_entry.get("token")
+ configured.append(
+ (
+ f"{token_file}:{token_id}",
+ raw_token,
+ _str_to_bool(token_entry.get("is_active", True)),
+ )
+ )
+ return configured
+
+
+def _validate_static_token_bootstrap(config: DorisConfig) -> None:
+ """Require at least one active high-entropy token when static auth is
enabled."""
+ configured = _configured_static_tokens(config.security)
+ for setting, raw_token, _active in configured:
+ try:
+ validate_high_entropy_secret(raw_token, setting=setting)
+ except ValueError as exc:
+ raise AuthConfigError(str(exc)) from exc
+ if not any(active for _setting, _raw_token, active in configured):
+ raise AuthConfigError(
+ "Token authentication requires at least one active high-entropy
credential. "
+ "Set TOKEN_<ID> or populate TOKEN_FILE_PATH before enabling it."
+ )
+
+
def normalize_effective_auth_config(
config: DorisConfig,
requested_workers: int | None = None,
@@ -1367,6 +1438,21 @@ def normalize_effective_auth_config(
if enable_doris_oauth_auth and enable_external_oauth_auth:
raise AuthConfigError("Doris OAuth and external OAuth cannot be
enabled together")
+ if enable_token_auth:
+ _validate_static_token_bootstrap(config)
+
+ if (
+ config.security.enable_http_token_management
+ and config.security.require_admin_auth
+ ):
+ try:
+ validate_high_entropy_secret(
+ config.security.token_management_admin_token,
+ setting="TOKEN_MANAGEMENT_ADMIN_TOKEN",
+ )
+ except ValueError as exc:
+ raise AuthConfigError(str(exc)) from exc
+
transport = str(inputs.transport.value or "stdio")
requested = int(inputs.workers.value if inputs.workers.value is not None
else 1)
effective_workers = multiprocessing.cpu_count() if requested == 0 else
requested
diff --git a/doris_mcp_server/utils/db.py b/doris_mcp_server/utils/db.py
index 32d849c..ac06b4b 100644
--- a/doris_mcp_server/utils/db.py
+++ b/doris_mcp_server/utils/db.py
@@ -433,38 +433,6 @@ class DorisConnectionManager:
return (not self._is_config_empty(self.original_db_config['host']) and
not self._is_config_empty(self.original_db_config['user']))
- def _find_available_token_with_db_config(self) -> str:
- """Find the first available token with database configuration
-
- Returns:
- Raw token string if found, empty string if not found
- """
- if not self.token_manager:
- return ""
-
- try:
- for token_hash, token_info in self.token_manager._tokens.items():
- if (token_info.database_config and
- token_info.is_active and
- not self._is_config_empty(token_info.database_config.host)
and
- not
self._is_config_empty(token_info.database_config.user)):
-
- # We need to find the raw token from the hash
- # This is a bit tricky since we only store hashes
- # We'll need to use the admin token from tokens.json if it
has db config
- if token_info.token_id == 'admin-token':
- # Try the known admin token
- return 'doris_admin_token_123456'
- elif 'tenant' in token_info.token_id:
- # For tenant tokens, we'll need a different approach
- # For now, skip these as we don't know the raw token
- continue
-
- return ""
- except Exception as e:
- self.logger.error(f"Error finding available token: {e}")
- return ""
-
def _get_token_hash(self, token: str) -> str:
"""Get hash of token for use as dictionary key"""
import hashlib
@@ -1914,16 +1882,6 @@ class DorisConnectionManager:
if not self.pool:
self.logger.warning("Connection pool is not available,
attempting recovery...")
- # Try to use token-bound configuration if available
- if self.token_manager and not
self._has_valid_global_config():
- available_token =
self._find_available_token_with_db_config()
- if available_token:
- self.logger.info(f"Using token-bound configuration
for pool creation: {available_token}")
- try:
- await self.configure_for_token(available_token)
- except Exception as e:
- self.logger.error(f"Failed to configure with
token-bound config: {e}")
-
# Fallback to recovery
if not self.pool:
await self._recover_pool_with_lock()
diff --git a/doris_mcp_server/utils/secret_policy.py
b/doris_mcp_server/utils/secret_policy.py
new file mode 100644
index 0000000..0bdc850
--- /dev/null
+++ b/doris_mcp_server/utils/secret_policy.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Shared policy for operator-provided authentication secrets."""
+
+MIN_SECRET_LENGTH = 32
+MIN_DISTINCT_CHARACTERS = 10
+
+_INSECURE_MARKERS = (
+ "123456",
+ "changeme",
+ "defaultsecret",
+ "exampletoken",
+ "password",
+ "yoursecret",
+ "yoursecure",
+)
+
+_RESERVED_TOKEN_ENV_NAMES = frozenset(
+ {
+ "TOKEN_EXPIRY",
+ "TOKEN_EXPIRY_HOURS",
+ "TOKEN_FILE_PATH",
+ "TOKEN_HASH_ALGORITHM",
+ "TOKEN_HOT_RELOAD",
+ "TOKEN_SECRET",
+ }
+)
+
+_RESERVED_TOKEN_ENV_PREFIXES = (
+ "TOKEN_HASH_",
+ "TOKEN_MANAGEMENT_",
+)
+
+
+def validate_high_entropy_secret(value: str, *, setting: str) -> str:
+ """Reject missing, short, repetitive, or placeholder authentication
secrets."""
+ if not isinstance(value, str) or not value:
+ raise ValueError(f"{setting} is required")
+ if value != value.strip():
+ raise ValueError(f"{setting} must not contain leading or trailing
whitespace")
+ if len(value) < MIN_SECRET_LENGTH:
+ raise ValueError(
+ f"{setting} must be at least {MIN_SECRET_LENGTH} characters; "
+ "generate it with a cryptographically secure random generator"
+ )
+ if value.startswith("<") or value.endswith(">"):
+ raise ValueError(f"{setting} must not be a placeholder")
+ compact = "".join(
+ character for character in value.casefold() if character.isalnum()
+ )
+ if any(marker in compact for marker in _INSECURE_MARKERS):
+ raise ValueError(
+ f"{setting} must not contain a known weak or placeholder value"
+ )
+ if len(set(value)) < MIN_DISTINCT_CHARACTERS:
+ raise ValueError(
+ f"{setting} must contain at least {MIN_DISTINCT_CHARACTERS}
distinct characters"
+ )
+ return value
+
+
+def is_static_token_environment_variable(name: str) -> bool:
+ """Return whether an environment variable defines a static bearer token."""
+ if not name.startswith("TOKEN_"):
+ return False
+ if name in _RESERVED_TOKEN_ENV_NAMES:
+ return False
+ if name.startswith(_RESERVED_TOKEN_ENV_PREFIXES):
+ return False
+ return not name.endswith(("_DESCRIPTION", "_EXPIRES_HOURS"))
diff --git a/test/security/test_effective_auth_config.py
b/test/security/test_effective_auth_config.py
index c2f0b2c..e3b229c 100644
--- a/test/security/test_effective_auth_config.py
+++ b/test/security/test_effective_auth_config.py
@@ -9,6 +9,8 @@ from doris_mcp_server.utils.config import (
)
from doris_mcp_server.utils.security import AuthenticationProvider
+STATIC_TOKEN = "V4nK8qR2mT7xP5cL9sD3hF6jY1uB0eG4iW8aN2zQ"
+
def test_default_auth_methods_remain_disabled():
config = DorisConfig()
@@ -35,10 +37,12 @@ def
test_explicit_oauth_false_conflicts_with_oauth_enabled_true():
normalize_effective_auth_config(config)
-def test_legacy_auth_type_only_applies_when_explicit():
+def test_legacy_auth_type_only_applies_when_explicit(tmp_path, monkeypatch):
config = DorisConfig()
+ config.security.token_file_path = str(tmp_path / "tokens.json")
config.security.auth_type = "token"
_mark_source(config, "auth_type", "env")
+ monkeypatch.setenv("TOKEN_TEST", STATIC_TOKEN)
effective = normalize_effective_auth_config(config)
diff --git a/test/security/test_static_token_bootstrap.py
b/test/security/test_static_token_bootstrap.py
new file mode 100644
index 0000000..4cab88a
--- /dev/null
+++ b/test/security/test_static_token_bootstrap.py
@@ -0,0 +1,171 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+import os
+from pathlib import Path
+
+import pytest
+
+from doris_mcp_server.auth.token_manager import TokenManager
+from doris_mcp_server.utils.config import (
+ AuthConfigError,
+ DorisConfig,
+ _mark_source,
+ normalize_effective_auth_config,
+)
+from doris_mcp_server.utils.secret_policy import (
+ is_static_token_environment_variable,
+)
+
+STATIC_TOKEN = "V4nK8qR2mT7xP5cL9sD3hF6jY1uB0eG4iW8aN2zQ"
+MANAGEMENT_TOKEN = "Q7cM2vR9kL4xT8pD5sF1hJ6uB3eG0iW9aN2zY8qP"
+
+
+def _config(tmp_path: Path) -> DorisConfig:
+ config = DorisConfig()
+ config.security.token_file_path = str(tmp_path / "tokens.json")
+ return config
+
+
+def _clear_static_token_environment(monkeypatch: pytest.MonkeyPatch) -> None:
+ for name in list(os.environ):
+ if is_static_token_environment_variable(name):
+ monkeypatch.delenv(name, raising=False)
+
+
+def test_repository_token_template_contains_no_credentials():
+ token_file = Path(__file__).parents[2] / "tokens.json"
+
+ token_data = json.loads(token_file.read_text(encoding="utf-8"))
+
+ assert token_data["tokens"] == []
+ assert "No usable credential is shipped" in token_data["notes"][0]
+
+
+def test_legacy_token_secret_has_no_default():
+ assert DorisConfig().security.token_secret == ""
+
+
+def test_static_auth_rejects_missing_active_credentials(tmp_path, monkeypatch):
+ _clear_static_token_environment(monkeypatch)
+ config = _config(tmp_path)
+ config.security.enable_token_auth = True
+ _mark_source(config, "enable_token_auth", "env")
+
+ with pytest.raises(AuthConfigError, match="at least one active
high-entropy"):
+ normalize_effective_auth_config(config)
+
+
+def test_static_auth_rejects_weak_environment_token(tmp_path, monkeypatch):
+ _clear_static_token_environment(monkeypatch)
+ config = _config(tmp_path)
+ config.security.enable_token_auth = True
+ _mark_source(config, "enable_token_auth", "env")
+ monkeypatch.setenv("TOKEN_ADMIN", "short-token")
+
+ with pytest.raises(AuthConfigError, match="at least 32 characters"):
+ normalize_effective_auth_config(config)
+
+
+def test_management_settings_are_not_loaded_as_static_tokens(tmp_path,
monkeypatch):
+ _clear_static_token_environment(monkeypatch)
+ config = _config(tmp_path)
+ config.security.enable_token_auth = True
+ _mark_source(config, "enable_token_auth", "env")
+ monkeypatch.setenv("TOKEN_MANAGEMENT_ADMIN_TOKEN", MANAGEMENT_TOKEN)
+ monkeypatch.setenv("TOKEN_MANAGEMENT_ALLOWED_IPS", "127.0.0.1")
+
+ with pytest.raises(AuthConfigError, match="at least one active
high-entropy"):
+ normalize_effective_auth_config(config)
+
+
[email protected]
+async def test_strong_environment_token_bootstraps_static_auth(tmp_path,
monkeypatch):
+ _clear_static_token_environment(monkeypatch)
+ config = _config(tmp_path)
+ config.security.enable_token_auth = True
+ _mark_source(config, "enable_token_auth", "env")
+ monkeypatch.setenv("TOKEN_ADMIN", STATIC_TOKEN)
+
+ normalize_effective_auth_config(config)
+ manager = TokenManager(config)
+ try:
+ result = await manager.validate_token(STATIC_TOKEN)
+ assert result.is_valid is True
+ assert result.token_info.token_id == "admin"
+ assert len(manager._tokens) == 1
+ finally:
+ manager.stop_hot_reload()
+
+
[email protected]
+async def test_manager_does_not_create_credentials_when_auth_is_disabled(
+ tmp_path,
+ monkeypatch,
+):
+ _clear_static_token_environment(monkeypatch)
+ manager = TokenManager(_config(tmp_path))
+ try:
+ assert manager._tokens == {}
+ assert manager._token_ids == {}
+ finally:
+ manager.stop_hot_reload()
+
+
[email protected]
+async def test_custom_static_token_must_follow_secret_policy(tmp_path,
monkeypatch):
+ _clear_static_token_environment(monkeypatch)
+ manager = TokenManager(_config(tmp_path))
+ try:
+ with pytest.raises(ValueError, match="at least 32 characters"):
+ await manager.create_token("weak", custom_token="short-token")
+
+ generated = await manager.create_token("generated")
+ assert len(generated) >= 32
+ assert (await manager.validate_token(generated)).is_valid is True
+ finally:
+ manager.stop_hot_reload()
+
+
[email protected](
+ ("admin_token", "error"),
+ [
+ ("", "is required"),
+ ("short-token", "at least 32 characters"),
+ ("a" * 40, "distinct characters"),
+ ],
+)
+def test_http_token_management_rejects_weak_admin_secret(admin_token, error):
+ config = DorisConfig()
+ config.security.enable_http_token_management = True
+ config.security.require_admin_auth = True
+ config.security.token_management_admin_token = admin_token
+
+ with pytest.raises(AuthConfigError, match=error):
+ normalize_effective_auth_config(config)
+
+
+def test_http_token_management_accepts_high_entropy_admin_secret():
+ config = DorisConfig()
+ config.security.enable_http_token_management = True
+ config.security.require_admin_auth = True
+ config.security.token_management_admin_token = MANAGEMENT_TOKEN
+
+ effective = normalize_effective_auth_config(config)
+
+ assert effective.auth_methods == ()
diff --git a/test/security/test_token_management_auth.py
b/test/security/test_token_management_auth.py
index 79ef623..287c94e 100644
--- a/test/security/test_token_management_auth.py
+++ b/test/security/test_token_management_auth.py
@@ -27,7 +27,7 @@ from doris_mcp_server.auth.token_handlers import TokenHandlers
from doris_mcp_server.auth.token_security_middleware import
TokenSecurityMiddleware
from doris_mcp_server.utils.config import DorisConfig
-ADMIN_TOKEN = "test-admin-token"
+ADMIN_TOKEN = "J8pR4mX2vN7qL5sT9kW3cF6hY1uD8aB0eG4iC2oP"
def _config() -> DorisConfig:
diff --git a/tokens.json b/tokens.json
index 878e2f1..aae0b8f 100644
--- a/tokens.json
+++ b/tokens.json
@@ -1,64 +1,10 @@
{
"version": "1.0",
- "description": "Doris MCP Server Token configuration file",
- "created_at": "2025-09-01T00:00:00Z",
- "tokens": [
- {
- "token_id": "admin-token",
- "token": "doris_admin_token_123456",
- "description": "Doris admin API access token",
- "expires_hours": null,
- "is_active": true,
- "database_config": {
- "host": "127.0.0.1",
- "port": 9030,
- "user": "root",
- "password": "",
- "database": "information_schema",
- "charset": "UTF8",
- "fe_http_port": 8030
- }
- },
- {
- "token_id": "analyst-token",
- "token": "doris_analyst_token_123456",
- "description": "Doris analyst API access token",
- "expires_hours": 8760,
- "is_active": true,
- "database_config": {
- "host": "127.0.0.1",
- "port": 9030,
- "user": "root",
- "password": "",
- "database": "information_schema",
- "charset": "UTF8",
- "fe_http_port": 8030
- }
- },
- {
- "token_id": "readonly-token",
- "token": "doris_readonly_token_123456",
- "description": "Doris readonly API access token",
- "expires_hours": 4320,
- "is_active": true,
- "database_config": {
- "host": "127.0.0.1",
- "port": 9030,
- "user": "root",
- "password": "",
- "database": "information_schema",
- "charset": "UTF8",
- "fe_http_port": 8030
- }
- }
- ],
+ "description": "Credential-free Doris MCP Server token configuration
template",
+ "tokens": [],
"notes": [
- "The admin_token, analyst_token, readonly_token is default token,Please
change the token before using in production!",
- "The token_id is the key of the token,Please use the token_id to identify
the token",
- "The token is the value of the token,Please use the token to identify the
token",
- "The description is the description of the token,Please use the
description to identify the token",
- "The expires_hours is the expires hours of the token,Please use the
expires_hours to identify the token",
- "The is_active is the is active of the token,Please use the is_active to
identify the token",
- "The token_id, token, description, expires_hours, is_active is the
metadata of the token,Please use the metadata to identify the token"
+ "No usable credential is shipped with the server.",
+ "Before enabling static token authentication, configure at least one
active token with 32 or more characters generated by a cryptographically secure
random generator.",
+ "Do not commit deployment credentials to source control."
]
-}
\ No newline at end of file
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]