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.
176 lines
5.0 KiB
Python
Executable File
176 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Rotate API key for AegisGitea MCP."""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
# Add src to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from aegis_gitea_mcp.auth import generate_api_key
|
|
|
|
|
|
def main() -> None:
|
|
"""Rotate API key in .env file."""
|
|
print("=" * 70)
|
|
print("AegisGitea MCP - API Key Rotation")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
# Find .env file
|
|
env_file = Path(__file__).parent.parent / ".env"
|
|
|
|
if not env_file.exists():
|
|
print("❌ Error: .env file not found")
|
|
print(f" Expected location: {env_file}")
|
|
print()
|
|
print(" Please create .env from .env.example first")
|
|
sys.exit(1)
|
|
|
|
# Read current .env
|
|
with open(env_file, "r") as f:
|
|
env_content = f.read()
|
|
|
|
# Check if MCP_API_KEYS exists
|
|
if "MCP_API_KEYS" not in env_content:
|
|
print("❌ Error: MCP_API_KEYS not found in .env file")
|
|
print()
|
|
print(" Please add MCP_API_KEYS to your .env file first")
|
|
sys.exit(1)
|
|
|
|
# Extract current key
|
|
match = re.search(r"MCP_API_KEYS=([^\n]+)", env_content)
|
|
if not match:
|
|
print("❌ Error: Could not parse MCP_API_KEYS from .env")
|
|
sys.exit(1)
|
|
|
|
current_keys = match.group(1).strip()
|
|
key_list = [k.strip() for k in current_keys.split(",") if k.strip()]
|
|
|
|
print(f"Found {len(key_list)} existing key(s)")
|
|
print()
|
|
|
|
# Show current keys (first 8 chars only)
|
|
for i, key in enumerate(key_list, 1):
|
|
key_hint = f"{key[:8]}...{key[-4:]}" if len(key) >= 12 else "invalid"
|
|
print(f" {i}. {key_hint}")
|
|
|
|
print()
|
|
print("-" * 70)
|
|
print()
|
|
|
|
# Generate new key
|
|
new_key = generate_api_key(length=64)
|
|
created_at = datetime.now(timezone.utc)
|
|
expires_at = created_at + timedelta(days=90)
|
|
|
|
print("✓ New API key generated!")
|
|
print()
|
|
print(f"Created: {created_at.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
print(f"Expires: {expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')} (90 days)")
|
|
print()
|
|
print("-" * 70)
|
|
print()
|
|
|
|
# Ask what to do with old keys
|
|
print("Rotation Strategy:")
|
|
print()
|
|
print(" 1. Replace all keys with new key (recommended)")
|
|
print(" 2. Add new key, keep old keys (grace period)")
|
|
print(" 3. Cancel")
|
|
print()
|
|
|
|
choice = input("Choose option [1/2/3]: ").strip()
|
|
|
|
if choice == "3":
|
|
print("\nRotation cancelled.")
|
|
sys.exit(0)
|
|
elif choice == "2":
|
|
# Add new key to list
|
|
key_list.append(new_key)
|
|
new_keys_str = ",".join(key_list)
|
|
print("\n✓ New key will be added (total: {} keys)".format(len(key_list)))
|
|
print("\n⚠️ IMPORTANT: Remove old keys manually after updating ChatGPT config")
|
|
elif choice == "1":
|
|
# Replace with only new key
|
|
new_keys_str = new_key
|
|
print("\n✓ All old keys will be replaced with new key")
|
|
else:
|
|
print("\n❌ Invalid choice. Operation cancelled.")
|
|
sys.exit(1)
|
|
|
|
print()
|
|
print("-" * 70)
|
|
print()
|
|
|
|
# Update .env file
|
|
new_env_content = re.sub(
|
|
r"MCP_API_KEYS=([^\n]+)",
|
|
f"MCP_API_KEYS={new_keys_str}",
|
|
env_content
|
|
)
|
|
|
|
# Backup old .env
|
|
backup_file = env_file.with_suffix(f".env.backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}")
|
|
with open(backup_file, "w") as f:
|
|
f.write(env_content)
|
|
|
|
print(f"✓ Backed up old .env to: {backup_file.name}")
|
|
|
|
# Write new .env
|
|
with open(env_file, "w") as f:
|
|
f.write(new_env_content)
|
|
|
|
print(f"✓ Updated .env file with new key(s)")
|
|
print()
|
|
print("-" * 70)
|
|
print()
|
|
print("📋 Next Steps:")
|
|
print()
|
|
print("1. Restart the MCP server:")
|
|
print()
|
|
print(" docker-compose restart aegis-mcp")
|
|
print()
|
|
print("2. Update ChatGPT Business configuration:")
|
|
print()
|
|
print(" - Go to ChatGPT Settings > MCP Servers")
|
|
print(" - Update Authorization header:")
|
|
print(f" Authorization: Bearer {new_key}")
|
|
print()
|
|
print("3. Test the connection:")
|
|
print()
|
|
print(" Ask ChatGPT: 'List my Gitea repositories'")
|
|
print()
|
|
print("4. If using grace period (option 2):")
|
|
print()
|
|
print(" - After confirming ChatGPT works with new key")
|
|
print(" - Manually remove old keys from .env")
|
|
print(" - Restart server again")
|
|
print()
|
|
print("-" * 70)
|
|
print()
|
|
print("⚠️ IMPORTANT:")
|
|
print()
|
|
print(f" • New API Key: {new_key}")
|
|
print(" • Store this securely - it won't be shown again")
|
|
print(" • Set a reminder to rotate in 90 days")
|
|
print(" • Old .env backed up to:", backup_file.name)
|
|
print()
|
|
print("=" * 70)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
print("\n\nOperation cancelled.")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"\n❌ Error: {e}", file=sys.stderr)
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|