Warum wir Security als System und nicht als Feature behandeln
In produktiven Umgebungen reicht es nicht, nur „Login“ zu bauen. Entscheidend ist eine mehrstufige Sicherheitsarchitektur, die Identität, Zugriff, Session-Schutz, Abuse-Prevention und Betrieb zusammendenkt. Genau darauf basiert unsere FastAPI-Implementierung.
Dieser Artikel zeigt transparent:
- wie wir Dienste und Admin-Bereich absichern,
- wie Passkeys in FastAPI integriert sind,
- wie Step-up-Authentifizierung praktisch umgesetzt wird,
- welche Betriebsmaßnahmen (Container, Secrets, Firewall, Monitoring) das Ganze tragen.
1) Grundprinzip: Defense in Depth
Unsere Sicherheit basiert auf mehreren Schichten:
- Identity Layer: OAuth + Passkeys (WebAuthn)
- Access Layer: Entitlements, Admin-Guard, optionale IP-Allowlist
- Session Layer: signierte Session-Cookies mit getrennten Secrets
- Abuse Layer: Login-Ratelimits, Lockouts, Step-up-Frischefenster
- Runtime Layer: Secrets via
*_FILE, least privilege Container, Healthchecks
So entsteht kein „Single Point of Failure“: Wenn eine Schutzmaßnahme umgangen wird, greifen weitere Kontrollen.
2) FastAPI-Bausteine für sichere Requests
2.1 Zentrale Dependencies
In FastAPI nutzen wir Dependencies, um Sicherheitsregeln zentral durchzusetzen. Dadurch wird Security nicht in jede Route kopiert, sondern über Guards standardisiert erzwungen.
from fastapi import Depends, HTTPException
def admin_guard(...):
# Basic-Auth + optionale IP/CIDR-Policy + Lockout + Step-up-Status
...
if not allowed:
raise HTTPException(status_code=403, detail="Forbidden")
@router.get("/admin/users")
async def admin_users_panel(..., admin_user: str = Depends(admin_guard)):
...
Vorteil: Jede geschützte Route hat konsistente Regeln und dieselbe Audit-/Fehlerlogik.
2.2 Security Headers zentral im Middleware-Layer
@app.middleware("http")
async def security_headers(request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
response.headers["Content-Security-Policy"] = "default-src 'self'; ..."
return response
Damit ist Browser-Schutz nicht optional pro Endpunkt, sondern global aktiv.
3) Passkeys mit FastAPI: Schritt-für-Schritt
Schritt 1: Challenge erzeugen und in Session speichern
Beim Start einer Registrierung oder Authentifizierung erzeugen wir serverseitig eine Challenge. Diese wird kurzlebig in der Session hinterlegt.
challenge = generate_registration_options(...)
request.session["passkey_challenge"] = challenge
return challenge_payload
Schritt 2: Browser/WebAuthn führt Ceremony durch
Im Frontend läuft navigator.credentials.create() (Register) bzw. navigator.credentials.get() (Login).
Schritt 3: Response serverseitig verifizieren
verification = verify_registration_response(
credential=response,
expected_challenge=session_challenge,
expected_origin=origin,
expected_rp_id=rp_id,
)
Nur wenn Challenge, Origin, RP-ID und Signatur passen, wird das Credential akzeptiert.
Schritt 4: Credential persistieren
Gespeichert werden nur notwendige technische Daten (credential_id, public key, sign_count, Metadaten). Private Schlüssel verlassen das Gerät des Users nie.
Schritt 5: Bei Login sign_count & last_used_at aktualisieren
Nach erfolgreicher Assertion aktualisieren wir den Zähler und Zeitstempel, um Replays und Missbrauchsmuster besser erkennen zu können.
4) Step-up-Authentifizierung für Admin-Aktionen
Nicht jede Session darf sofort kritische Aktionen ausführen. Deshalb nutzen wir Step-up:
- reguläre Anmeldung für normale Nutzung,
- zusätzliche Passkey-Bestätigung + Admin-Kontext für sensible Bereiche.
Technisch wird ein kurzer Step-up-Zeitraum (MAX_AGE_SECONDS) gesetzt. Ist er abgelaufen, muss der User erneut bestätigen.
if admin_stepup_required and stepup_age > max_age:
raise HTTPException(status_code=403, detail="admin step-up required")
Das reduziert Risiko durch gestohlene oder offene Sessions drastisch.
5) Secrets, Runtime und Infrastruktur-Härtung
5.1 Secrets niemals im Klartext deployen
Wir lesen Secrets primär über *_FILE aus /run/secrets:
def get_env(name, default=""):
direct = os.getenv(name)
if direct:
return direct
file_path = os.getenv(f"{name}_FILE")
if file_path:
return Path(file_path).read_text().strip()
return default
5.2 Least Privilege im Container
no-new-privileges:truecap_dropmit gezieltemcap_addnur wenn technisch nötigread_only+tmpfsfür stateless Services- App läuft als Nicht-Root-User
5.3 Firewall & Exposure
Nur notwendige Ports offen (typisch 22, 80, 443, ggf. 81 für NPM-Admin). Interne Services wie DB bleiben auf private Bindings beschränkt.
6) Monitoring, Recovery und Betriebsrealität
Sicherheit ist erst vollständig, wenn auch Recovery sauber ist:
- Healthcheck-Endpunkte (
/healthz) - Instanz-Heartbeat und Monitoring-Übersicht
- Audit-Logs für Admin-Aktionen
- Recovery-Skript für Step-up-Freigaben (wenn noch kein berechtigter User existiert)
Damit bleibt das System auch unter Incident-Bedingungen steuerbar.
7) Ergebnis: Sicherheit mit nachvollziehbarer Operabilität
Unsere FastAPI-Architektur kombiniert moderne Authentifizierung (Passkeys) mit produktiver Governance (Step-up, Guardrails, Härtung, Monitoring). Der entscheidende Punkt ist die Transparenz: Sicherheitsregeln sind nachvollziehbar implementiert, reproduzierbar testbar und im Betrieb belastbar.
Wenn ihr ein ähnliches Setup aufbaut, startet mit der Reihenfolge:
- zentrale Guards,
- Passkey-Flow,
- Step-up für kritische Pfade,
- Secrets/Runtime-Härtung,
- Monitoring + Recovery.
So entsteht ein System, das nicht nur „sicher aussieht“, sondern im Alltag wirklich standhält.