Add deployment-safe backend config and production-safe CORS defaults

This commit is contained in:
2026-04-06 20:47:31 +02:00
parent 1a7096b276
commit c2982116a8
4 changed files with 52 additions and 9 deletions

View File

@@ -8,6 +8,12 @@ BANGUI_DATABASE_PATH=bangui.db
# Path to the fail2ban Unix domain socket.
BANGUI_FAIL2BAN_SOCKET=/var/run/fail2ban/fail2ban.sock
# Path to the fail2ban configuration directory used by the web UI.
BANGUI_FAIL2BAN_CONFIG_DIR=/config/fail2ban
# Shell command used to start fail2ban during recovery operations.
BANGUI_FAIL2BAN_START_COMMAND=fail2ban-client start
# Secret key used to sign session tokens. Use a long, random string.
# Generate with: python -c "import secrets; print(secrets.token_hex(64))"
BANGUI_SESSION_SECRET=replace-this-with-a-long-random-secret
@@ -23,4 +29,5 @@ BANGUI_LOG_LEVEL=info
# Comma-separated list of allowed CORS origins when the frontend is served
# from a different origin than the backend.
# Leave this blank in production when the UI is served from the same origin.
BANGUI_CORS_ALLOWED_ORIGINS=http://localhost:5173

View File

@@ -4,9 +4,15 @@ Follows pydantic-settings patterns: all values are prefixed with BANGUI_
and validated at startup via the Settings singleton.
"""
from pydantic import Field
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.utils.constants import (
DEFAULT_DATABASE_PATH,
DEFAULT_FAIL2BAN_SOCKET,
DEFAULT_SESSION_DURATION_MINUTES,
)
class Settings(BaseSettings):
"""BanGUI runtime configuration.
@@ -18,11 +24,11 @@ class Settings(BaseSettings):
"""
database_path: str = Field(
default="bangui.db",
default=DEFAULT_DATABASE_PATH,
description="Filesystem path to the BanGUI SQLite application database.",
)
fail2ban_socket: str = Field(
default="/var/run/fail2ban/fail2ban.sock",
default=DEFAULT_FAIL2BAN_SOCKET,
description="Path to the fail2ban Unix domain socket.",
)
session_secret: str = Field(
@@ -33,7 +39,7 @@ class Settings(BaseSettings):
),
)
session_duration_minutes: int = Field(
default=60,
default=DEFAULT_SESSION_DURATION_MINUTES,
ge=1,
description="Number of minutes a session token remains valid after creation.",
)
@@ -41,14 +47,24 @@ class Settings(BaseSettings):
default="UTC",
description="IANA timezone name used when displaying timestamps in the UI.",
)
cors_allowed_origins: list[str] = Field(
default_factory=lambda: ["http://localhost:5173"],
cors_allowed_origins: str | list[str] = Field(
default_factory=list,
description=(
"Comma-separated list of allowed CORS origins when the frontend is "
"served from a different origin than the backend. "
"Leave empty to disable cross-origin requests in production."
),
)
@field_validator("cors_allowed_origins", mode="before")
@classmethod
def _normalize_cors_origins(cls, value: str | list[str] | None) -> list[str]:
if value is None:
return []
if isinstance(value, str):
return [origin.strip() for origin in value.split(",") if origin.strip()]
return value
log_level: str = Field(
default="info",
description="Application log level: debug | info | warning | error | critical.",
@@ -72,8 +88,8 @@ class Settings(BaseSettings):
default="fail2ban-client start",
description=(
"Shell command used to start (not reload) the fail2ban daemon during "
"recovery rollback. Split by whitespace to build the argument list — "
"no shell interpretation is performed. "
"recovery rollback. Split by whitespace to build the argument list — "
"no shell interpretation is performed. "
"Example: 'systemctl start fail2ban' or 'fail2ban-client start'."
),
)

View File

@@ -48,3 +48,23 @@ def test_create_app_skips_cors_when_no_origins_are_configured() -> None:
]
assert cors_middleware == []
def test_create_app_disables_cors_by_default() -> None:
"""The FastAPI app does not add CORS middleware when no origins are configured by environment."""
settings = Settings(
database_path="/tmp/test.db",
fail2ban_socket="/tmp/fake_fail2ban.sock",
fail2ban_config_dir="/tmp/fail2ban",
session_secret="test-secret-key-do-not-use-in-production",
session_duration_minutes=60,
timezone="UTC",
log_level="debug",
)
app = create_app(settings=settings)
cors_middleware = [
middleware for middleware in app.user_middleware if middleware.cls is CORSMiddleware
]
assert cors_middleware == []