feat: add API key authentication system for ChatGPT Business
Implements comprehensive Bearer token authentication to ensure only authorized ChatGPT workspaces can access the MCP server. Core Features: - API key validation with constant-time comparison - Multi-key support for rotation grace periods - Rate limiting (5 failures per IP per 5 min) - Comprehensive audit logging of all auth attempts - IP-based failed attempt tracking Key Management: - generate_api_key.py: Create secure 64-char keys - rotate_api_key.py: Guided key rotation with backup - check_key_age.py: Automated expiration monitoring Infrastructure: - Traefik labels for HTTPS and rate limiting - Security headers (HSTS, CSP, X-Frame-Options) - Environment-based configuration - Docker secrets support Documentation: - AUTH_SETUP.md: Complete authentication setup guide - CHATGPT_SETUP.md: ChatGPT Business integration guide - KEY_ROTATION.md: Key rotation procedures and automation Security: - Read-only operations enforced - No write access to Gitea possible - All auth attempts logged with correlation IDs - Failed attempts trigger IP rate limits - Keys never logged in full (only hints) Breaking Changes: - AUTH_ENABLED defaults to true - MCP_API_KEYS environment variable now required - Minimum key length: 32 characters (64 recommended) Migration: 1. Generate API key: make generate-key 2. Add to .env: MCP_API_KEYS=<generated-key> 3. Restart: docker-compose restart aegis-mcp 4. Configure ChatGPT with Authorization header Closes requirements for ChatGPT Business exclusive access.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Configuration management for AegisGitea MCP server."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import Field, HttpUrl, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
@@ -67,6 +67,26 @@ class Settings(BaseSettings):
|
||||
ge=1,
|
||||
)
|
||||
|
||||
# Authentication configuration
|
||||
auth_enabled: bool = Field(
|
||||
default=True,
|
||||
description="Enable API key authentication (disable only for testing)",
|
||||
)
|
||||
mcp_api_keys: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of valid API keys for MCP access (comma-separated in env)",
|
||||
)
|
||||
max_auth_failures: int = Field(
|
||||
default=5,
|
||||
description="Maximum authentication failures before rate limiting",
|
||||
ge=1,
|
||||
)
|
||||
auth_failure_window: int = Field(
|
||||
default=300, # 5 minutes
|
||||
description="Time window for counting auth failures (in seconds)",
|
||||
ge=1,
|
||||
)
|
||||
|
||||
@field_validator("log_level")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
@@ -85,6 +105,41 @@ class Settings(BaseSettings):
|
||||
raise ValueError("gitea_token cannot be empty or whitespace")
|
||||
return v.strip()
|
||||
|
||||
@field_validator("mcp_api_keys", mode="before")
|
||||
@classmethod
|
||||
def parse_api_keys(cls, v: object) -> List[str]:
|
||||
"""Parse API keys from comma-separated string or list."""
|
||||
if isinstance(v, str):
|
||||
# Split by comma and strip whitespace
|
||||
keys = [key.strip() for key in v.split(",") if key.strip()]
|
||||
return keys
|
||||
elif isinstance(v, list):
|
||||
return v
|
||||
return []
|
||||
|
||||
@field_validator("mcp_api_keys")
|
||||
@classmethod
|
||||
def validate_api_keys(cls, v: List[str], info) -> List[str]:
|
||||
"""Validate API keys if authentication is enabled."""
|
||||
# Get auth_enabled from values (it's already been processed)
|
||||
auth_enabled = info.data.get("auth_enabled", True)
|
||||
|
||||
if auth_enabled and not v:
|
||||
raise ValueError(
|
||||
"At least one API key must be configured when auth_enabled=True. "
|
||||
"Set MCP_API_KEYS environment variable or disable auth with AUTH_ENABLED=false"
|
||||
)
|
||||
|
||||
# Validate key format (at least 32 characters for security)
|
||||
for key in v:
|
||||
if len(key) < 32:
|
||||
raise ValueError(
|
||||
f"API keys must be at least 32 characters long. "
|
||||
f"Use scripts/generate_api_key.py to generate secure keys."
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
@property
|
||||
def gitea_base_url(self) -> str:
|
||||
"""Get Gitea base URL as string."""
|
||||
|
||||
Reference in New Issue
Block a user