| """
|
| Secret Manager for AegisLM
|
|
|
| Provides secure secret management and retrieval.
|
| Loads secrets from environment variables at runtime.
|
| """
|
|
|
| import os
|
| from typing import Any, Dict, Optional
|
| from functools import lru_cache
|
|
|
| from pydantic import BaseModel
|
|
|
|
|
| class SecretManagerConfig(BaseModel):
|
| """Configuration for secret manager."""
|
|
|
| jwt_secret_key: str
|
| jwt_algorithm: str = "HS256"
|
| jwt_expiration_hours: int = 24
|
|
|
|
|
| database_url: str
|
|
|
|
|
| encryption_key: str
|
|
|
|
|
| worker_secret_key: str
|
|
|
|
|
| hf_token: Optional[str] = None
|
| openai_api_key: Optional[str] = None
|
| anthropic_api_key: Optional[str] = None
|
|
|
|
|
| secret_prefix: str = "AEGISLM_"
|
|
|
|
|
| class SecretManager:
|
| """
|
| Centralized secret management.
|
|
|
| Loads secrets from environment variables at runtime.
|
| Secrets should NEVER be hardcoded in source files.
|
| """
|
|
|
| _instance: Optional["SecretManager"] = None
|
| _config: Optional[SecretManagerConfig] = None
|
|
|
| def __new__(cls) -> "SecretManager":
|
| """Singleton pattern for secret manager."""
|
| if cls._instance is None:
|
| cls._instance = super().__new__(cls)
|
| return cls._instance
|
|
|
| def __init__(self):
|
| """Initialize the secret manager."""
|
| if self._config is None:
|
| self._load_config()
|
|
|
| def _load_config(self) -> None:
|
| """Load configuration from environment variables."""
|
| prefix = os.getenv("AEGISLM_SECRET_PREFIX", "AEGISLM_")
|
|
|
|
|
| self._is_production = os.getenv("AEGISLM_ENV", "development").lower() == "production"
|
|
|
|
|
| jwt_secret = os.getenv(f"{prefix}JWT_SECRET_KEY")
|
| if not jwt_secret:
|
| jwt_secret = os.getenv("JWT_SECRET_KEY")
|
| if not jwt_secret:
|
| if self._is_production:
|
| raise ValueError(
|
| "CRITICAL: JWT_SECRET_KEY must be set in environment variables. "
|
| "Production mode requires AEGISLM_JWT_SECRET_KEY to be set. "
|
| "Cannot start without secure JWT secret."
|
| )
|
| else:
|
| jwt_secret = os.getenv("JWT_SECRET_KEY", "dev-secret-key-change-in-production")
|
|
|
|
|
| if self._is_production and jwt_secret == "dev-secret-key-change-in-production":
|
| raise ValueError(
|
| "CRITICAL: Production mode detected but using default dev secret key. "
|
| "This is a security violation. Set AEGISLM_JWT_SECRET_KEY to a secure value. "
|
| "Refusing to start in production with insecure default key."
|
| )
|
|
|
| db_url = os.getenv(f"{prefix}DATABASE_URL")
|
| if not db_url:
|
| db_url = os.getenv("DATABASE_URL")
|
| if not db_url:
|
| if self._is_production:
|
| raise ValueError(
|
| "CRITICAL: DATABASE_URL must be set in environment variables. "
|
| "Production mode requires PostgreSQL database. Cannot start without DATABASE_URL."
|
| )
|
| else:
|
| db_url = "sqlite+aiosqlite:///./aegislm_dev.db"
|
|
|
|
|
| if self._is_production and "sqlite" in db_url.lower():
|
| raise ValueError(
|
| "CRITICAL: Production mode does not support SQLite. "
|
| "Use PostgreSQL with asyncpg driver: postgresql+asyncpg://user:pass@host:5432/db"
|
| )
|
|
|
|
|
| redis_url = os.getenv(f"{prefix}REDIS_URL")
|
| if not redis_url:
|
| redis_url = os.getenv("REDIS_URL")
|
| if not redis_url:
|
| if self._is_production:
|
| raise ValueError(
|
| "CRITICAL: REDIS_URL must be set in environment variables. "
|
| "Production mode requires Redis for rate limiting. Cannot start without REDIS_URL."
|
| )
|
| else:
|
| redis_url = "redis://localhost:6379/0"
|
|
|
| if self._is_production and redis_url == "redis://localhost:6379/0":
|
|
|
| pass
|
|
|
| encryption_key = os.getenv(f"{prefix}ENCRYPTION_KEY")
|
| if not encryption_key:
|
| import warnings
|
| warnings.warn(
|
| "ENCRYPTION_KEY not set. Using default (insecure). "
|
| "Set AEGISLM_ENCRYPTION_KEY for production.",
|
| DeprecationWarning
|
| )
|
| encryption_key = "default-encryption-key-32-bytes!!"
|
|
|
| worker_secret = os.getenv(f"{prefix}WORKER_SECRET_KEY")
|
| if not worker_secret:
|
| worker_secret = os.getenv("WORKER_SECRET_KEY", "worker-secret-key-change-in-production")
|
|
|
| self._config = SecretManagerConfig(
|
| jwt_secret_key=jwt_secret,
|
| database_url=db_url,
|
| encryption_key=encryption_key,
|
| worker_secret_key=worker_secret,
|
| hf_token=os.getenv(f"{prefix}HF_TOKEN") or os.getenv("HF_TOKEN"),
|
| openai_api_key=os.getenv(f"{prefix}OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY"),
|
| anthropic_api_key=os.getenv(f"{prefix}ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_API_KEY"),
|
| secret_prefix=prefix,
|
| )
|
|
|
| @property
|
| def config(self) -> SecretManagerConfig:
|
| """Get the secret manager configuration."""
|
| if self._config is None:
|
| self._load_config()
|
| return self._config
|
|
|
| def get_jwt_secret(self) -> str:
|
| """Get JWT secret key."""
|
| return self.config.jwt_secret_key
|
|
|
| def get_jwt_algorithm(self) -> str:
|
| """Get JWT algorithm."""
|
| return self.config.jwt_algorithm
|
|
|
| def get_jwt_expiration_hours(self) -> int:
|
| """Get JWT token expiration in hours."""
|
| return self.config.jwt_expiration_hours
|
|
|
| def get_database_url(self) -> str:
|
| """Get database URL."""
|
| return self.config.database_url
|
|
|
| def get_encryption_key(self) -> str:
|
| """Get encryption key."""
|
| return self.config.encryption_key
|
|
|
| def get_worker_secret(self) -> str:
|
| """Get worker secret key."""
|
| return self.config.worker_secret_key
|
|
|
| def get_hf_token(self) -> Optional[str]:
|
| """Get HuggingFace token."""
|
| return self.config.hf_token
|
|
|
| def get_openai_api_key(self) -> Optional[str]:
|
| """Get OpenAI API key."""
|
| return self.config.openai_api_key
|
|
|
| def get_anthropic_api_key(self) -> Optional[str]:
|
| """Get Anthropic API key."""
|
| return self.config.anthropic_api_key
|
|
|
| def get_secret(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
| """
|
| Get a secret by name.
|
|
|
| Args:
|
| name: Secret name (without prefix)
|
| default: Default value if not found
|
|
|
| Returns:
|
| Secret value or default
|
| """
|
| prefix = self.config.secret_prefix
|
| full_name = f"{prefix}{name}"
|
|
|
|
|
| value = os.getenv(full_name)
|
| if value:
|
| return value
|
|
|
|
|
| value = os.getenv(name)
|
| if value:
|
| return value
|
|
|
| return default
|
|
|
| def get_required_secret(self, name: str) -> str:
|
| """
|
| Get a required secret by name.
|
|
|
| Args:
|
| name: Secret name (without prefix)
|
|
|
| Returns:
|
| Secret value
|
|
|
| Raises:
|
| ValueError: If secret is not found
|
| """
|
| value = self.get_secret(name)
|
| if not value:
|
| raise ValueError(f"Required secret '{name}' not found in environment variables")
|
| return value
|
|
|
| def is_production_mode(self) -> bool:
|
| """Check if running in production mode."""
|
| return os.getenv("AEGISLM_ENV", "development").lower() == "production"
|
|
|
| def validate_secrets(self) -> Dict[str, bool]:
|
| """
|
| Validate that all required secrets are set.
|
|
|
| Returns:
|
| Dictionary of secret names and whether they're set
|
| """
|
| required_secrets = [
|
| "JWT_SECRET_KEY",
|
| "DATABASE_URL",
|
| "ENCRYPTION_KEY",
|
| "WORKER_SECRET_KEY",
|
| ]
|
|
|
| return {
|
| secret: self.get_secret(secret) is not None
|
| for secret in required_secrets
|
| }
|
|
|
|
|
| @lru_cache()
|
| def get_secret_manager() -> SecretManager:
|
| """
|
| Get the singleton secret manager instance.
|
|
|
| Returns:
|
| SecretManager instance
|
| """
|
| return SecretManager()
|
|
|
|
|
|
|
| def get_jwt_secret() -> str:
|
| """Get JWT secret key."""
|
| return get_secret_manager().get_jwt_secret()
|
|
|
|
|
| def get_jwt_algorithm() -> str:
|
| """Get JWT algorithm."""
|
| return get_secret_manager().get_jwt_algorithm()
|
|
|
|
|
| def get_database_url() -> str:
|
| """Get database URL."""
|
| return get_secret_manager().get_database_url()
|
|
|
|
|
| def get_encryption_key() -> str:
|
| """Get encryption key."""
|
| return get_secret_manager().get_encryption_key()
|
|
|
|
|
| def get_worker_secret() -> str:
|
| """Get worker secret key."""
|
| return get_secret_manager().get_worker_secret()
|
|
|