Files
AegisGitea-MCP/scripts/check_key_age.py
latte eeaad748a6 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.
2026-01-29 20:05:49 +01:00

141 lines
4.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Check API key age and alert if rotation needed."""
import os
import re
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
def main() -> None:
"""Check API key age from keys/ metadata directory."""
print("=" * 70)
print("AegisGitea MCP - API Key Age Check")
print("=" * 70)
print()
# Check keys directory
keys_dir = Path(__file__).parent.parent / "keys"
if not keys_dir.exists():
print("⚠️ No keys/ directory found")
print()
print(" Key metadata is not being tracked.")
print(" Run scripts/generate_api_key.py and save metadata to track key age.")
print()
sys.exit(0)
# Find all key metadata files
metadata_files = list(keys_dir.glob("key-*.txt"))
if not metadata_files:
print("⚠️ No key metadata files found in keys/")
print()
print(" Run scripts/generate_api_key.py and save metadata to track key age.")
print()
sys.exit(0)
print(f"Found {len(metadata_files)} key metadata file(s)")
print()
print("-" * 70)
print()
now = datetime.now(timezone.utc)
warnings = []
critical = []
for metadata_file in sorted(metadata_files):
with open(metadata_file, "r") as f:
content = f.read()
# Extract metadata
key_id_match = re.search(r"Key ID:\s+([^\n]+)", content)
created_match = re.search(r"Created:\s+([^\n]+)", content)
expires_match = re.search(r"Expires:\s+([^\n]+)", content)
if not all([key_id_match, created_match, expires_match]):
print(f"⚠️ Could not parse: {metadata_file.name}")
continue
key_id = key_id_match.group(1).strip()
created_str = created_match.group(1).strip()
expires_str = expires_match.group(1).strip()
# Parse dates
try:
created_at = datetime.fromisoformat(created_str.replace("Z", "+00:00"))
expires_at = datetime.fromisoformat(expires_str.replace("Z", "+00:00"))
except ValueError as e:
print(f"⚠️ Could not parse dates in: {metadata_file.name}")
print(f" Error: {e}")
continue
# Calculate age and time to expiration
age_days = (now - created_at).days
days_to_expiration = (expires_at - now).days
# Status
if days_to_expiration < 0:
status = "❌ EXPIRED"
critical.append(key_id)
elif days_to_expiration <= 7:
status = "⚠️ CRITICAL"
critical.append(key_id)
elif days_to_expiration <= 14:
status = "⚠️ WARNING"
warnings.append(key_id)
else:
status = "✓ OK"
print(f"Key: {key_id}")
print(f" Created: {created_at.strftime('%Y-%m-%d')} ({age_days} days ago)")
print(f" Expires: {expires_at.strftime('%Y-%m-%d')} (in {days_to_expiration} days)")
print(f" Status: {status}")
print()
print("-" * 70)
print()
# Summary
if critical:
print("❌ CRITICAL: {} key(s) require immediate rotation!".format(len(critical)))
print()
print(" Keys:")
for key_id in critical:
print(f"{key_id}")
print()
print(" Action: Run scripts/rotate_api_key.py NOW")
print()
sys.exit(2)
elif warnings:
print("⚠️ WARNING: {} key(s) will expire soon".format(len(warnings)))
print()
print(" Keys:")
for key_id in warnings:
print(f"{key_id}")
print()
print(" Action: Schedule key rotation in the next few days")
print(" Run: scripts/rotate_api_key.py")
print()
sys.exit(1)
else:
print("✓ All keys are valid and within rotation schedule")
print()
print(" Next check: Run this script again in 1 week")
print()
sys.exit(0)
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)