Files
ssh-jumphost/app/main.py
2026-08-19 22:33:19 +02:00

100 lines
3.7 KiB
Python

"""
FastAPI-Einstiegspunkt. Bindet Security-Header, Router und Static/Template-
Auslieferung zusammen (siehe Konzept 6.6 Web-Anwendungs-Hardening).
"""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app.admin.routes import router as admin_router
from app.auth.deps import get_current_user
from app.auth.routes import router as auth_router
from app.catalog.routes import router as catalog_router
from app.db import close_db, init_db
from app.rdp_proxy.ws_tunnel import router as rdp_ws_router
from app.ssh_proxy.sftp import router as sftp_router
from app.ssh_proxy.terminal_ws import router as ssh_ws_router
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
await close_db()
app = FastAPI(title="Jumphost Gateway", lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None)
app.include_router(auth_router)
app.include_router(admin_router)
app.include_router(catalog_router)
app.include_router(ssh_ws_router)
app.include_router(sftp_router)
app.include_router(rdp_ws_router)
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.middleware("http")
async def security_headers_middleware(request: Request, call_next):
"""Setzt die in Konzept 6.6 geforderten Security-Header auf jede Antwort.
Laeuft unabhaengig davon, ob nginx vorgeschaltet ist (Defense-in-Depth --
nginx setzt in der Praxis dieselben Header zusaetzlich, siehe ansible/roles/nginx_proxy)."""
response = await call_next(request)
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Referrer-Policy"] = "no-referrer"
response.headers["Permissions-Policy"] = "clipboard-read=(self), clipboard-write=(self), fullscreen=(self)"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self'; "
"img-src 'self' data:; "
"connect-src 'self' ws: wss:; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
)
response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains"
return response
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
# Hinweis: seit Starlette >=1.x ist "TemplateResponse(request, name, ...)"
# die aktuelle Aufrufkonvention (die alte "TemplateResponse(name, {...})"
# wurde entfernt) -- beim Dependency-Upgrade im Rahmen der pip-audit-
# Bereinigung angepasst und per Pentest-Testsuite regressionsgetestet.
return templates.TemplateResponse(request, "login.html", {})
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request):
# Auth-Pruefung erfolgt clientseitig ueber GET /auth/me (401 -> Redirect zu /);
# serverseitig zusaetzlich abgesichert, sobald Templates dynamische Inhalte rendern.
return templates.TemplateResponse(request, "dashboard.html", {})
@app.get("/terminal/{host_id}", response_class=HTMLResponse)
async def terminal_page(request: Request, host_id: int):
return templates.TemplateResponse(request, "terminal.html", {"host_id": host_id})
@app.get("/rdp/{host_id}", response_class=HTMLResponse)
async def rdp_page(request: Request, host_id: int):
return templates.TemplateResponse(request, "rdp.html", {"host_id": host_id})
@app.get("/healthz")
async def healthz():
return {"status": "ok"}