Complete security hardening implementation including HTTPS enforcement, security headers, rate limiting, and comprehensive security test suite. Key features: - HTTPS enforcement with HSTS support - Security headers (CSP, X-Frame-Options, X-Content-Type-Options) - Rate limiting for all critical endpoints - Enhanced email template security - 87% test coverage with security-specific tests Architect approval: 9.5/10 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
183 lines
6.4 KiB
Python
183 lines
6.4 KiB
Python
"""
|
|
Configuration management for Gondulf IndieAuth server.
|
|
|
|
Loads configuration from environment variables with GONDULF_ prefix.
|
|
Validates required settings on startup and provides sensible defaults.
|
|
"""
|
|
|
|
import os
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables from .env file if present
|
|
load_dotenv()
|
|
|
|
|
|
class ConfigurationError(Exception):
|
|
"""Raised when configuration is invalid or missing required values."""
|
|
|
|
pass
|
|
|
|
|
|
class Config:
|
|
"""Application configuration loaded from environment variables."""
|
|
|
|
# Required settings - no defaults
|
|
SECRET_KEY: str
|
|
BASE_URL: str
|
|
|
|
# Database
|
|
DATABASE_URL: str
|
|
|
|
# SMTP Configuration
|
|
SMTP_HOST: str
|
|
SMTP_PORT: int
|
|
SMTP_USERNAME: str | None
|
|
SMTP_PASSWORD: str | None
|
|
SMTP_FROM: str
|
|
SMTP_USE_TLS: bool
|
|
|
|
# Token and Code Expiry (seconds)
|
|
TOKEN_EXPIRY: int
|
|
CODE_EXPIRY: int
|
|
|
|
# Token Cleanup (Phase 3)
|
|
TOKEN_CLEANUP_ENABLED: bool
|
|
TOKEN_CLEANUP_INTERVAL: int
|
|
|
|
# Security Configuration (Phase 4b)
|
|
HTTPS_REDIRECT: bool
|
|
TRUST_PROXY: bool
|
|
SECURE_COOKIES: bool
|
|
|
|
# Logging
|
|
LOG_LEVEL: str
|
|
DEBUG: bool
|
|
|
|
@classmethod
|
|
def load(cls) -> None:
|
|
"""
|
|
Load and validate configuration from environment variables.
|
|
|
|
Raises:
|
|
ConfigurationError: If required settings are missing or invalid
|
|
"""
|
|
# Required - SECRET_KEY must exist and be sufficiently long
|
|
secret_key = os.getenv("GONDULF_SECRET_KEY")
|
|
if not secret_key:
|
|
raise ConfigurationError(
|
|
"GONDULF_SECRET_KEY is required. Generate with: "
|
|
"python -c \"import secrets; print(secrets.token_urlsafe(32))\""
|
|
)
|
|
if len(secret_key) < 32:
|
|
raise ConfigurationError(
|
|
"GONDULF_SECRET_KEY must be at least 32 characters for security"
|
|
)
|
|
cls.SECRET_KEY = secret_key
|
|
|
|
# Required - BASE_URL must exist for OAuth metadata
|
|
base_url = os.getenv("GONDULF_BASE_URL")
|
|
if not base_url:
|
|
raise ConfigurationError(
|
|
"GONDULF_BASE_URL is required for OAuth 2.0 metadata endpoint. "
|
|
"Examples: https://auth.example.com or http://localhost:8000 (development only)"
|
|
)
|
|
# Normalize: remove trailing slash if present
|
|
cls.BASE_URL = base_url.rstrip("/")
|
|
|
|
# Database - with sensible default
|
|
cls.DATABASE_URL = os.getenv(
|
|
"GONDULF_DATABASE_URL", "sqlite:///./data/gondulf.db"
|
|
)
|
|
|
|
# SMTP Configuration
|
|
cls.SMTP_HOST = os.getenv("GONDULF_SMTP_HOST", "localhost")
|
|
cls.SMTP_PORT = int(os.getenv("GONDULF_SMTP_PORT", "587"))
|
|
cls.SMTP_USERNAME = os.getenv("GONDULF_SMTP_USERNAME") or None
|
|
cls.SMTP_PASSWORD = os.getenv("GONDULF_SMTP_PASSWORD") or None
|
|
cls.SMTP_FROM = os.getenv("GONDULF_SMTP_FROM", "noreply@example.com")
|
|
cls.SMTP_USE_TLS = os.getenv("GONDULF_SMTP_USE_TLS", "true").lower() == "true"
|
|
|
|
# Token and Code Expiry
|
|
cls.TOKEN_EXPIRY = int(os.getenv("GONDULF_TOKEN_EXPIRY", "3600"))
|
|
cls.CODE_EXPIRY = int(os.getenv("GONDULF_CODE_EXPIRY", "600"))
|
|
|
|
# Token Cleanup Configuration
|
|
cls.TOKEN_CLEANUP_ENABLED = os.getenv("GONDULF_TOKEN_CLEANUP_ENABLED", "false").lower() == "true"
|
|
cls.TOKEN_CLEANUP_INTERVAL = int(os.getenv("GONDULF_TOKEN_CLEANUP_INTERVAL", "3600"))
|
|
|
|
# Security Configuration (Phase 4b)
|
|
cls.HTTPS_REDIRECT = os.getenv("GONDULF_HTTPS_REDIRECT", "true").lower() == "true"
|
|
cls.TRUST_PROXY = os.getenv("GONDULF_TRUST_PROXY", "false").lower() == "true"
|
|
cls.SECURE_COOKIES = os.getenv("GONDULF_SECURE_COOKIES", "true").lower() == "true"
|
|
|
|
# Logging
|
|
cls.DEBUG = os.getenv("GONDULF_DEBUG", "false").lower() == "true"
|
|
# If DEBUG is true, default LOG_LEVEL to DEBUG, otherwise INFO
|
|
default_log_level = "DEBUG" if cls.DEBUG else "INFO"
|
|
cls.LOG_LEVEL = os.getenv("GONDULF_LOG_LEVEL", default_log_level).upper()
|
|
|
|
# Validate log level
|
|
valid_log_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
|
if cls.LOG_LEVEL not in valid_log_levels:
|
|
raise ConfigurationError(
|
|
f"GONDULF_LOG_LEVEL must be one of: {', '.join(valid_log_levels)}"
|
|
)
|
|
|
|
@classmethod
|
|
def validate(cls) -> None:
|
|
"""
|
|
Validate configuration after loading.
|
|
|
|
Performs additional validation beyond initial loading.
|
|
"""
|
|
# Validate BASE_URL is a valid URL
|
|
if not cls.BASE_URL.startswith(("http://", "https://")):
|
|
raise ConfigurationError(
|
|
"GONDULF_BASE_URL must start with http:// or https://"
|
|
)
|
|
|
|
# Warn if using http:// in production-like settings
|
|
if cls.BASE_URL.startswith("http://") and "localhost" not in cls.BASE_URL:
|
|
import warnings
|
|
warnings.warn(
|
|
"GONDULF_BASE_URL uses http:// for non-localhost domain. "
|
|
"HTTPS is required for production IndieAuth servers.",
|
|
UserWarning
|
|
)
|
|
|
|
# Validate SMTP port is reasonable
|
|
if cls.SMTP_PORT < 1 or cls.SMTP_PORT > 65535:
|
|
raise ConfigurationError(
|
|
f"GONDULF_SMTP_PORT must be between 1 and 65535, got {cls.SMTP_PORT}"
|
|
)
|
|
|
|
# Validate expiry times are positive and within bounds
|
|
if cls.TOKEN_EXPIRY < 300: # Minimum 5 minutes
|
|
raise ConfigurationError(
|
|
"GONDULF_TOKEN_EXPIRY must be at least 300 seconds (5 minutes)"
|
|
)
|
|
if cls.TOKEN_EXPIRY > 86400: # Maximum 24 hours
|
|
raise ConfigurationError(
|
|
"GONDULF_TOKEN_EXPIRY must be at most 86400 seconds (24 hours)"
|
|
)
|
|
if cls.CODE_EXPIRY <= 0:
|
|
raise ConfigurationError(
|
|
f"GONDULF_CODE_EXPIRY must be positive, got {cls.CODE_EXPIRY}"
|
|
)
|
|
|
|
# Validate cleanup interval if enabled
|
|
if cls.TOKEN_CLEANUP_ENABLED and cls.TOKEN_CLEANUP_INTERVAL < 600:
|
|
raise ConfigurationError(
|
|
"GONDULF_TOKEN_CLEANUP_INTERVAL must be at least 600 seconds (10 minutes)"
|
|
)
|
|
|
|
# Disable HTTPS redirect in development mode
|
|
if cls.DEBUG:
|
|
cls.HTTPS_REDIRECT = False
|
|
|
|
|
|
# Configuration is loaded lazily or explicitly by the application
|
|
# Tests should call Config.load() explicitly in fixtures
|
|
# Production code should call Config.load() at startup
|