From 90cb3906bdcc2bd7fd3bcfc919efe67c9c9405b1 Mon Sep 17 00:00:00 2001 From: galuszkm <=> Date: Thu, 2 Jul 2026 21:18:54 +0200 Subject: [PATCH 1/2] fix(middleware): exempt health probes from trusted-host check - Add HealthExemptTrustedHostMiddleware to bypass validation only on /health and /ready. For ALB probes hitting the container by private IP - in prod we forbid wildcar on TRUSTED_HOSTS - Fix Docker build and broken login.css reference in the login template. --- .dockerignore | 1 + Dockerfile | 4 +- src/strands_compose_chat/app.py | 16 ++++++-- .../middleware/__init__.py | 4 +- .../middleware/trusted_host.py | 40 +++++++++++++++++++ src/strands_compose_chat/templates/login.html | 2 +- tests/conftest.py | 2 +- uv.lock | 2 +- 8 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 src/strands_compose_chat/middleware/trusted_host.py diff --git a/.dockerignore b/.dockerignore index 4ab932a..3e63f8a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,3 +28,4 @@ tests/ # Docs / misc *.md +!README.md diff --git a/Dockerfile b/Dockerfile index d429074..fdc8a11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,8 @@ WORKDIR /app COPY pyproject.toml . COPY uv.lock . +COPY LICENSE . +COPY README.md . COPY src/ src/ # Copy pre-built frontend assets (required for wheel artifacts). @@ -41,8 +43,6 @@ RUN groupadd --gid 1001 app && \ WORKDIR /app COPY --from=builder /app/.venv .venv -COPY entrypoint.sh . -RUN chmod +x /app/entrypoint.sh ENV PATH="/app/.venv/bin:$PATH" diff --git a/src/strands_compose_chat/app.py b/src/strands_compose_chat/app.py index f6ac9d9..dd80a88 100644 --- a/src/strands_compose_chat/app.py +++ b/src/strands_compose_chat/app.py @@ -8,7 +8,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware -from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.responses import JSONResponse from sqladmin import Admin from sqlalchemy import select @@ -39,7 +38,7 @@ from .frontend import mount_frontend from .logging import configure_logging from .media.routes import router as media_router -from .middleware import SecurityHeadersMiddleware +from .middleware import HealthExemptTrustedHostMiddleware, SecurityHeadersMiddleware from .sessions.routes import router as sessions_router logger: structlog.stdlib.BoundLogger = structlog.get_logger(__name__) @@ -94,7 +93,18 @@ def create_app(settings: Settings | None = None) -> FastAPI: same_site="lax", ) app.add_middleware(SecurityHeadersMiddleware) - app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.TRUSTED_HOSTS) + app.add_middleware( + HealthExemptTrustedHostMiddleware, + allowed_hosts=settings.TRUSTED_HOSTS, + exempt_paths=frozenset( + { + f"{prefix}/health", + f"{prefix}/ready", # routes mounted under the URL prefix + "/health", + "/ready", # fallback when ASGI root_path is used + } + ), + ) app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ALLOWED_ORIGINS, diff --git a/src/strands_compose_chat/middleware/__init__.py b/src/strands_compose_chat/middleware/__init__.py index 4374af2..a3bbe82 100644 --- a/src/strands_compose_chat/middleware/__init__.py +++ b/src/strands_compose_chat/middleware/__init__.py @@ -1,7 +1,9 @@ -"""Middleware: security headers.""" +"""Middleware: security headers and trusted-host exemptions.""" from .security_headers import SecurityHeadersMiddleware +from .trusted_host import HealthExemptTrustedHostMiddleware __all__ = [ + "HealthExemptTrustedHostMiddleware", "SecurityHeadersMiddleware", ] diff --git a/src/strands_compose_chat/middleware/trusted_host.py b/src/strands_compose_chat/middleware/trusted_host.py new file mode 100644 index 0000000..d071b7d --- /dev/null +++ b/src/strands_compose_chat/middleware/trusted_host.py @@ -0,0 +1,40 @@ +"""TrustedHostMiddleware variant that exempts LB probe paths from host validation.""" + +from starlette.middleware.trustedhost import TrustedHostMiddleware +from starlette.types import Receive, Scope, Send + + +class HealthExemptTrustedHostMiddleware(TrustedHostMiddleware): + """TrustedHostMiddleware that skips host validation for LB health/readiness probes. + + ALB target-group health checks reach the container via its private IP, so + the probe's Host header is the container IP — never a value in + TRUSTED_HOSTS. We bypass host validation only for the exact probe paths; + every other request is still strictly enforced. + + Args: + app: The ASGI application to wrap. + allowed_hosts: Sequence of hostnames allowed through host validation. + www_redirect: When True, redirect non-www to www (passed to parent). + exempt_paths: Frozenset of path strings that skip host validation. + Both the prefixed and un-prefixed probe paths should be included so + the exemption works regardless of whether the app uses ASGI + ``root_path`` or mounts routes directly under the prefix. + """ + + def __init__( + self, + app, # type: ignore[override] # starlette parent uses untyped ASGIApp alias + allowed_hosts: list[str] | None = None, + www_redirect: bool = True, + exempt_paths: frozenset[str] = frozenset(), + ) -> None: + super().__init__(app, allowed_hosts=allowed_hosts, www_redirect=www_redirect) + self.exempt_paths = exempt_paths + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """Pass probe paths through without host validation; enforce all others.""" + if scope["type"] == "http" and scope.get("path", "").rstrip("/") in self.exempt_paths: + await self.app(scope, receive, send) + return + await super().__call__(scope, receive, send) diff --git a/src/strands_compose_chat/templates/login.html b/src/strands_compose_chat/templates/login.html index 9022aa0..725e6ce 100644 --- a/src/strands_compose_chat/templates/login.html +++ b/src/strands_compose_chat/templates/login.html @@ -8,7 +8,7 @@