Align Sinbad2 HTTPS deployment with orcid2sword reverse-proxy pattern.

This adds nginx dual-path routing, forwarded proxy headers, Uvicorn proxy-headers, production security settings, and deployment docs for https://sinbad2.ujaen.es/generadorexamenesllm/.
This commit is contained in:
Mireya Cueto Garrido
2026-06-03 10:12:05 +02:00
parent ca6d370585
commit 7dcc7dc0e1
13 changed files with 230 additions and 31 deletions
+11 -2
View File
@@ -1,6 +1,15 @@
# --- Aplicación ---
APP_NAME=GenExamenes IA
ENVIRONMENT=local
ENVIRONMENT=production
# URL pública HTTPS (Apache termina TLS; contenedores en HTTP interno)
PUBLIC_BASE_URL=https://sinbad2.ujaen.es/generadorexamenesllm
# Hosts aceptados por TrustedHostMiddleware (sin esquema ni puerto)
TRUSTED_HOSTS=sinbad2.ujaen.es,localhost,127.0.0.1
# HSTS (segundos; 1 año por defecto)
SECURITY_HSTS_SECONDS=31536000
# Clave legacy (reservada; las rutas /exam usan JWT de usuario).
API_KEY=change-me-in-production-min-16-chars
@@ -8,7 +17,7 @@ API_KEY=change-me-in-production-min-16-chars
# --- Base de datos (Docker: host "db") ---
DATABASE_URL=postgresql+psycopg://genexamenes:genexamenes@db:5432/genexamenes
# --- CORS (orígenes del frontend, separados por coma) ---
# --- CORS (orígenes HTTPS del frontend; separados por coma) ---
ALLOWED_ORIGINS=https://sinbad2.ujaen.es,http://sinbad2.ujaen.es,http://sinbad2.ujaen.es:8075
# --- Rate limiting y tamaño de petición ---
+6 -1
View File
@@ -25,4 +25,9 @@ USER app
EXPOSE 8074
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8074"]
CMD ["uvicorn", "app.main:app", \
"--host", "0.0.0.0", \
"--port", "8074", \
"--proxy-headers", \
"--forwarded-allow-ips", "*", \
"--no-server-header"]
+11
View File
@@ -7,6 +7,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "GenExamenes IA"
environment: str = "local"
public_base_url: str = "https://sinbad2.ujaen.es/generadorexamenesllm"
trusted_hosts: str = "sinbad2.ujaen.es,localhost,127.0.0.1"
security_hsts_seconds: int = Field(default=31_536_000, ge=0)
api_prefix: str = ""
api_key: str = Field(min_length=16)
database_url: str = "postgresql+psycopg://genexamenes:genexamenes@localhost:5432/genexamenes"
@@ -41,10 +44,18 @@ class Settings(BaseSettings):
extra="ignore",
)
@property
def is_production(self) -> bool:
return self.environment.lower() in {"production", "prod"}
@property
def cors_origins(self) -> list[str]:
return [origin.strip() for origin in self.allowed_origins.split(",") if origin.strip()]
@property
def trusted_hosts_list(self) -> list[str]:
return [host.strip() for host in self.trusted_hosts.split(",") if host.strip()]
@lru_cache
def get_settings() -> Settings:
+30
View File
@@ -0,0 +1,30 @@
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from app.core.config import Settings
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Cabeceras de seguridad; HSTS en producción o cuando el proxy indica HTTPS."""
def __init__(self, app: object, settings: Settings) -> None:
super().__init__(app)
self._settings = settings
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
response = await call_next(request)
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "SAMEORIGIN")
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
forwarded_proto = request.headers.get("x-forwarded-proto", request.url.scheme)
is_https = forwarded_proto == "https" or request.url.scheme == "https"
if is_https or self._settings.is_production:
hsts = f"max-age={self._settings.security_hsts_seconds}"
if self._settings.is_production:
hsts += "; includeSubDomains"
response.headers.setdefault("Strict-Transport-Security", hsts)
return response
+6 -1
View File
@@ -1,13 +1,15 @@
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from fastapi import Depends, FastAPI
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from app.api.routes import auth, exports, generation, health, history, images, materials, questions, templates
from app.core.config import get_settings
from app.core.errors import register_exception_handlers
from app.core.middleware import RateLimitMiddleware, RequestSizeLimitMiddleware
from app.core.security_headers import SecurityHeadersMiddleware
from app.db.init_db import init_db
@@ -25,6 +27,9 @@ def create_app() -> FastAPI:
# las peticiones OPTIONS (preflight) respondan antes que rate limit, etc.
app.add_middleware(RequestSizeLimitMiddleware, settings=settings)
app.add_middleware(RateLimitMiddleware, settings=settings)
app.add_middleware(SecurityHeadersMiddleware, settings=settings)
if settings.trusted_hosts_list:
app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.trusted_hosts_list)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,