10 KiB
Sicherheitstest-Bericht: Jumphost Gateway
Datum: 19.08.2026 | Version des Testobjekts: Implementierung v0.2 | Tester: Claude (im Auftrag von Midas)
1. Scope und Methodik
1.1 Was tatsächlich getestet wurde
Diese Sitzung lief in einer isolierten Cloud-Sandbox ohne Zugriff auf ein
reales Netzwerk, echte SSH-/RDP-Zielsysteme oder eine laufende guacd-
Instanz. Der Test ist daher ein Anwendungssicherheitstest (SAST + gezielter
DAST) gegen den Quellcode und die lokal laufende Anwendung, kein
vollständiger Infrastruktur-/Netzwerk-Penetrationstest. Konkret durchgeführt:
- Statische Analyse (SAST):
banditgegen den gesamten Python-Code (app/,scripts/),pip-auditgegen alle Produktiv- und Dev-Abhängigkeiten. - Dynamischer Sicherheitstest (DAST, White-/Grey-Box): 17 gezielte
Angriffstests (
tests/test_pentest_security.py) gegen die echte FastAPI-Anwendung inkl. SQLite-Datenbank, ausgeführt über den ASGI-Transport (kein echtes TCP/TLS, aber derselbe Anwendungscode wie in Produktion) sowie ergänzend manuelle Verifikation percurlgegen einen laufendenuvicorn-Prozess. - Manuelle Code-Review-Punkte: Session-/Cookie-Handling, RBAC- Durchsetzung, Key-Handling, Audit-Log-Integrität — im Rahmen der unter 2) genannten Testfälle verifiziert, nicht nur gelesen.
1.2 Was NICHT getestet wurde (out of scope in dieser Umgebung)
| Bereich | Warum nicht möglich |
|---|---|
| Netzwerk-/Infrastruktur-Pentest (Portscans, TLS-Konfiguration des realen nginx/nftables-Setups, Firewall-Umgehung) | Kein reales Zielsystem/Netzwerk in dieser Sandbox verfügbar |
RDP/guacd-Pfad end-to-end (echtes Windows-Ziel, echtes guacd+FreeRDP) |
Keine RDP-Zielumgebung verfügbar; Protokoll-Handshake wurde nur gegen die Spezifikation implementiert, nicht live gegenverifiziert |
| SSH-Proxy end-to-end gegen ein echtes Linux-Ziel | Kein SSH-Zielsystem verfügbar; SSH-Verbindungslogik ist durch asyncssh (etabliert, nicht selbst geschrieben) abgedeckt, die serverseitige Anbindung (Key-Laden, Host-Key-Pinning) wurde isoliert/unit-artig, nicht end-to-end getestet |
| Ansible-Rollen-Ausführung gegen ein reales Debian/Ubuntu-System | Kein Root-Zielsystem verfügbar; nur --syntax-check + YAML/Jinja-Parsing + Variablen-/Handler-Konsistenzprüfung möglich |
| OpenSCAP/CIS-Benchmark-Tool-Lauf | Kein Zielsystem, auf dem oscap laufen könnte |
| Social Engineering, physische Sicherheit | Nicht anwendbar auf ein Code-Review |
| Lasttest / Denial-of-Service-Robustheit | Nicht Teil dieses Auftrags; separat zu betrachten |
Konsequenz: Die hier bestätigte Sicherheit bezieht sich auf die Anwendungslogik selbst. Vor Produktivbetrieb sind ein echter Netzwerk-Pentest gegen die deployte Instanz und ein Live-Test des RDP-Pfads weiterhin erforderlich (siehe Abschnitt 4).
2. Ergebnisse: Statische Analyse
2.1 bandit (Python-Sicherheitslinter)
| Vorher | Nachher |
|---|---|
| 1× Medium (SQL-String-Konstruktion, B608) | 0 |
6× Low (breite except-Blöcke B110, subprocess-Nutzung B404/B603) |
0 (5× behoben durch Logging statt pass, 2× als geprüft/gerechtfertigt mit # nosec + Begründungskommentar markiert) |
Alle Findings wurden behoben oder mit expliziter, im Code dokumentierter
Begründung als bewusste Entscheidung markiert (kein stilles Wegklicken).
Details: app/auth/routes.py (SQL-Konstruktion durch zwei feste
parametrisierte Statements ersetzt), app/security/av_scan.py,
app/rdp_proxy/guacd_client.py, app/rdp_proxy/ws_tunnel.py,
app/ssh_proxy/terminal_ws.py.
2.2 pip-audit (Abhängigkeits-Schwachstellen)
16 bekannte Schwachstellen in 2 Paketen gefunden (Stand vor Bereinigung):
| Paket | Version (vorher) | Version (nachher) | Bekannte CVEs/Advisories |
|---|---|---|---|
cryptography |
43.0.3 | 50.0.0 | 6 (u.a. PYSEC-2026-3553/3554, GHSA-537c-gmf6-5ccf) |
starlette |
0.46.2 | 1.6.0 | 10 (u.a. PYSEC-2026-1941/1942/2280/2281) |
fastapi |
0.115.x | 0.141.1 | (transitiv mit starlette aktualisiert) |
pytest, pytest-asyncio (Dev-only) |
8.3.5 / 0.24.0 | 9.1.1 / 1.4.0 | PYSEC-2026-1845 |
Nach dem Upgrade: 0 bekannte Schwachstellen in einem vollständigen
pip-audit-Lauf über die gesamte aufgelöste Umgebung. requirements.txt
und requirements-dev.txt sind jetzt exakt (lockfile-artig) auf die
geprüften Versionen gepinnt.
⚠️ Wichtiger Fund durch die Regressionstests: Das Upgrade von Starlette
0.46 → 1.6 hat die Aufrufkonvention von Jinja2Templates.TemplateResponse()
geändert (alte Signatur TemplateResponse(name, {"request": request}) wurde
entfernt, neue Signatur ist TemplateResponse(request, name, context)). Ohne
die Pentest-Testsuite (test_security_headers_present_on_every_response u.a.,
die alle Seiten inkl. / aufrufen) wäre dieser funktionale Regressionsbug
erst im Betrieb aufgefallen: GET /, /dashboard, /terminal/{id},
/rdp/{id} hätten alle mit HTTP 500 geantwortet. Behoben in app/main.py;
alle vier Routen sind erneut per Test und manuellem curl-Aufruf verifiziert
(Status 200, korrektes HTML).
3. Ergebnisse: Dynamischer Sicherheitstest (17 Testfälle)
Alle 17 Testfälle sind grün (siehe tests/test_pentest_security.py,
ausführbar mit pytest tests/test_pentest_security.py -v). Zusammenfassung
nach Kategorie:
| Kategorie | Geprüft | Ergebnis |
|---|---|---|
| Authentifizierung | Keine Username-Enumeration über Fehlermeldungen; Account-Lockout nach 5 Fehlversuchen (auch mit korrektem Passwort danach gesperrt); IP-basiertes Rate-Limiting greift bei >10 Versuchen/Minute; klassische SQLi-Payloads im Username-Feld führen weder zu 500 noch zu Auth-Bypass, Datenbank bleibt intakt | ✅ Bestanden |
| Session-/Cookie-Sicherheit | Cookie trägt HttpOnly, Secure, SameSite=Strict; ein mit falschem Secret gefälschtes Cookie wird abgelehnt; Passwortänderung invalidiert alte Sessions (session_version-Mechanismus); "Überall abmelden" invalidiert das Cookie sofort |
✅ Bestanden |
| RBAC / IDOR | Nicht-Admin kommt an keinen Admin-Endpunkt (weder lesend noch schreibend); Nutzer mit Rolle nur auf Hostgruppe A wird bei Host aus Hostgruppe B mit 403 blockiert — unabhängig davon, dass die Host-ID gültig/erratbar ist; eine Rolle für einen Aktionstyp (z.B. ssh_connect) gewährt nicht automatisch eine andere (z.B. file_transfer) auf demselben Host |
✅ Bestanden |
| Dateitransfer-Härtung | Uploads über dem konfigurierten Limit werden mit 413 abgelehnt; als "infected" markierte Uploads werden blockiert (400) und erzeugen keinen Eintrag in file_transfers (kein falsches Erfolgssignal) |
✅ Bestanden |
| HTTP-Security-Header | CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Strict-Transport-Security, Referrer-Policy auf jeder Antwort inkl. Fehlerantworten; keine Stacktraces/Dateipfade in Fehlermeldungen |
✅ Bestanden |
| Audit-Log-Vollständigkeit | Fehlgeschlagene und erfolgreiche Logins sowie TOTP-Enrollment erzeugen Audit-Einträge; Hash-Chain bleibt nach einem vollständigen Testlauf (viele parallele Nutzer, Fehlversuche, Admin-Aktionen) durchgängig intakt | ✅ Bestanden |
3.1 Bewertung nicht-automatisierter Beobachtungen (kein Fix nötig, dokumentiert)
- Pfadangabe bei SFTP-Dateitransfer (
remote_path-Parameter): erlaubt beliebige Pfade wie../../etc/passwd. Das ist kein Jumphost-seitiges Path-Traversal, da dieser Pfad ausschließlich an das Zielsystem per SFTP übergeben wird (keine lokale Dateisystem-Interaktion auf dem Jumphost selbst) — die Zugriffskontrolle liegt beim Zielsystem-Betriebssystem über denssh_username. Funktional gewollt (Nutzer soll das Zielsystem wie mit einem SFTP-Client durchsuchen können). Empfehlung für sehr sensible Hostgruppen: zusätzliche pfadbasierte Allow-Lists auf Anwendungsebene als optionale Erweiterung (Konzept Kap. 12 könnte hierzu ergänzt werden). hosts.addressist nur admin-editierbar, nicht durch normale Nutzer beeinflussbar — ein SSRF-artiger Angriffsvektor über beliebige Zieladressen ist daher kein nutzerseitig ausnutzbares Risiko, sondern liegt in der bestehenden Admin-Vertrauensgrenze (Admins könnten ohnehin beliebige RBAC-Rechte vergeben).
4. Offene Punkte vor Produktivbetrieb (Restrisiko)
Diese Punkte waren bereits im ursprünglichen README als offen vermerkt und bleiben es, da sie eine echte Zielumgebung erfordern, die in dieser Sandbox nicht existiert:
- Netzwerk-Pentest gegen die tatsächlich deployte Instanz (TLS-Konfiguration von nginx/direktem Uvicorn-TLS, Firewall-Regeln, tatsächliches Verhalten von fail2ban) — Black-Box-Test von außen.
- RDP-Pfad live gegen
guacd+ FreeRDP + Windows-Ziel testen — insbesondere Parameter-Namen/-Reihenfolge derconnect-Instruktion gegen die konkret eingesetzte guacd-Version verifizieren (sieheapp/rdp_proxy/guacd_client.py, Docstring-Hinweis). ansible-playbook --checkbzw. echter Rollout in einer Staging-Umgebung — insbesondere diepam_faillock-Einbindung und die/tmp-//dev/shm-Remount-Logik sind distributionsversionsabhängig und sollten vor der Produktivfreigabe an einem echten System bestätigt werden (sieheansible/roles/os_hardening/CIS_STIG_MAPPING.md, Abschnitt „Bekannte Einschränkungen").- OpenSCAP-Lauf gegen das gewählte CIS/STIG-Profil zur unabhängigen Bestätigung der Hardening-Rolle.
- Alle bereits zuvor im README genannten funktionalen Erweiterungen (LDAP/AD, WebAuthn, verteiltes Rate-Limiting, CSRF-Token als zusätzliche Schicht) sind weiterhin nicht umgesetzt.
5. Fazit
Die Anwendungslogik hat den durchgeführten Sicherheitstest ohne verbleibende offene Findings bestanden: keine Auth-/Session-Bypässe, keine RBAC-/IDOR- Lücken, keine SQL-Injection, vollständige Audit-Protokollierung, durchgängige Security-Header, saubere Abhängigkeiten (0 bekannte CVEs). Der Testlauf hat zusätzlich einen realen, durch das Dependency-Upgrade eingeführten Funktionsfehler gefunden und behoben — ein gutes Beispiel dafür, warum Sicherheitsupdates immer mit Regressionstests kombiniert werden sollten, nicht isoliert eingespielt werden dürfen.
Die verbleibenden offenen Punkte sind ausschließlich solche, die eine echte Zielinfrastruktur voraussetzen und in einer isolierten Code-Sandbox grundsätzlich nicht abschließend geprüft werden können.