TASK-026: Disable API docs in production, protect with BANGUI_ENABLE_DOCS setting
Addresses security concern where FastAPI's default behavior exposes interactive API documentation (/docs, /redoc) without authentication, allowing attackers to enumerate endpoints and understand API schemas. Changes: - Add BANGUI_ENABLE_DOCS boolean setting (default: false) to Settings - Modify create_app() to conditionally set docs_url, redoc_url, openapi_url - Add docs endpoints to SetupRedirectMiddleware allowlist (/api/docs, /api/redoc, /api/openapi.json) - Set BANGUI_ENABLE_DOCS=true in Docker/compose.debug.yml for development - Production compose files leave it unset (defaults to false, docs disabled) - Add comprehensive tests for docs configuration - Document the new setting in Backend-Development.md Security Impact: - API documentation is now disabled by default in production - Development environments can enable docs by setting BANGUI_ENABLE_DOCS=true - Docs endpoints are inaccessible in production without manual configuration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -162,6 +162,14 @@ class Settings(BaseSettings):
|
||||
"Example: 'systemctl start fail2ban' or 'fail2ban-client start'."
|
||||
),
|
||||
)
|
||||
enable_docs: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Enable FastAPI interactive API documentation at /api/docs (Swagger UI) "
|
||||
"and /api/redoc (ReDoc). Should be true only in development environments. "
|
||||
"In production, leave unset (defaults to false) to avoid exposing API schema."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("fail2ban_start_command", mode="after")
|
||||
@classmethod
|
||||
|
||||
@@ -401,7 +401,7 @@ async def _service_unavailable_handler(
|
||||
|
||||
# Paths that are always reachable, even before setup is complete.
|
||||
_ALWAYS_ALLOWED: frozenset[str] = frozenset(
|
||||
{"/api/setup", "/api/health"},
|
||||
{"/api/setup", "/api/health", "/api/docs", "/api/redoc", "/api/openapi.json"},
|
||||
)
|
||||
|
||||
|
||||
@@ -469,11 +469,20 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
"""
|
||||
resolved_settings: Settings = settings if settings is not None else get_settings()
|
||||
|
||||
# Configure API docs based on enable_docs setting.
|
||||
# In production, docs are disabled (None). In development, docs are served at /api/*.
|
||||
docs_url = "/api/docs" if resolved_settings.enable_docs else None
|
||||
redoc_url = "/api/redoc" if resolved_settings.enable_docs else None
|
||||
openapi_url = "/api/openapi.json" if resolved_settings.enable_docs else None
|
||||
|
||||
app: FastAPI = FastAPI(
|
||||
title="BanGUI",
|
||||
description="Web interface for monitoring, managing, and configuring fail2ban.",
|
||||
version=__version__,
|
||||
lifespan=_lifespan,
|
||||
docs_url=docs_url,
|
||||
redoc_url=redoc_url,
|
||||
openapi_url=openapi_url,
|
||||
)
|
||||
|
||||
# Store immutable configuration and the dedicated runtime state manager on
|
||||
|
||||
@@ -143,6 +143,46 @@ def test_create_app_disables_cors_by_default() -> None:
|
||||
assert cors_middleware == []
|
||||
|
||||
|
||||
def test_create_app_disables_api_docs_by_default() -> None:
|
||||
"""API documentation endpoints are disabled when enable_docs is false."""
|
||||
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",
|
||||
enable_docs=False,
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
|
||||
assert app.docs_url is None
|
||||
assert app.redoc_url is None
|
||||
assert app.openapi_url is None
|
||||
|
||||
|
||||
def test_create_app_enables_api_docs_when_configured() -> None:
|
||||
"""API documentation endpoints are enabled at /api/* when enable_docs is true."""
|
||||
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",
|
||||
enable_docs=True,
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
|
||||
assert app.docs_url == "/api/docs"
|
||||
assert app.redoc_url == "/api/redoc"
|
||||
assert app.openapi_url == "/api/openapi.json"
|
||||
|
||||
|
||||
async def test_lifespan_initialises_and_cleans_up_shared_resources(tmp_path: Path) -> None:
|
||||
"""The app lifespan creates and shuts down shared resources cleanly."""
|
||||
settings = Settings(
|
||||
|
||||
Reference in New Issue
Block a user