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:
2026-01-29 20:05:49 +01:00
parent a9708b33e2
commit eeaad748a6
13 changed files with 2263 additions and 4 deletions

View File

@@ -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."""