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:
2026-04-26 15:09:51 +02:00
parent a768a2d303
commit df841c21e4
6 changed files with 82 additions and 75 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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(