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

@@ -8,6 +8,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import ValidationError
from aegis_gitea_mcp.audit import get_audit_logger
from aegis_gitea_mcp.auth import get_validator
from aegis_gitea_mcp.config import get_settings
from aegis_gitea_mcp.gitea_client import GiteaClient
from aegis_gitea_mcp.mcp_protocol import (
@@ -38,9 +39,10 @@ app = FastAPI(
version="0.1.0",
)
# Global settings and audit logger
# Global settings, audit logger, and auth validator
settings = get_settings()
audit = get_audit_logger()
auth_validator = get_validator()
# Tool dispatcher mapping
@@ -52,6 +54,44 @@ TOOL_HANDLERS = {
}
# Authentication middleware
@app.middleware("http")
async def authenticate_request(request: Request, call_next):
"""Authenticate all requests except health checks and root."""
# Skip authentication for health check and root endpoints
if request.url.path in ["/", "/health"]:
return await call_next(request)
# Only authenticate MCP endpoints
if not request.url.path.startswith("/mcp/"):
return await call_next(request)
# Extract client information
client_ip = request.client.host if request.client else "unknown"
user_agent = request.headers.get("user-agent", "unknown")
# Extract Authorization header
auth_header = request.headers.get("authorization")
api_key = auth_validator.extract_bearer_token(auth_header)
# Validate API key
is_valid, error_message = auth_validator.validate_api_key(api_key, client_ip, user_agent)
if not is_valid:
return JSONResponse(
status_code=401,
content={
"error": "Authentication failed",
"message": error_message,
"detail": "Please provide a valid API key in the Authorization header: Bearer <api-key>",
},
)
# Authentication successful - continue to endpoint
response = await call_next(request)
return response
@app.on_event("startup")
async def startup_event() -> None:
"""Initialize server on startup."""
@@ -59,6 +99,13 @@ async def startup_event() -> None:
logger.info(f"Connected to Gitea instance: {settings.gitea_base_url}")
logger.info(f"Audit logging enabled: {settings.audit_log_path}")
# Log authentication status
if settings.auth_enabled:
key_count = len(settings.mcp_api_keys)
logger.info(f"API key authentication ENABLED ({key_count} key(s) configured)")
else:
logger.warning("API key authentication DISABLED - server is open to all requests!")
# Test Gitea connection
try:
async with GiteaClient() as gitea: