Files
ssh-jumphost/Pentest_Report.md
2026-08-19 22:33:19 +02:00

165 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
1. **Statische Analyse (SAST):** `bandit` gegen den gesamten Python-Code
(`app/`, `scripts/`), `pip-audit` gegen alle Produktiv- und
Dev-Abhängigkeiten.
2. **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 per `curl` gegen einen
laufenden `uvicorn`-Prozess.
3. **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
den `ssh_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.address` ist 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:
1. **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.
2. **RDP-Pfad live gegen `guacd` + FreeRDP + Windows-Ziel testen**
insbesondere Parameter-Namen/-Reihenfolge der `connect`-Instruktion gegen
die konkret eingesetzte guacd-Version verifizieren (siehe
`app/rdp_proxy/guacd_client.py`, Docstring-Hinweis).
3. **`ansible-playbook --check` bzw. echter Rollout in einer
Staging-Umgebung** — insbesondere die `pam_faillock`-Einbindung und die
`/tmp`-/`/dev/shm`-Remount-Logik sind distributionsversionsabhängig und
sollten vor der Produktivfreigabe an einem echten System bestätigt werden
(siehe `ansible/roles/os_hardening/CIS_STIG_MAPPING.md`, Abschnitt
„Bekannte Einschränkungen").
4. **OpenSCAP-Lauf** gegen das gewählte CIS/STIG-Profil zur unabhängigen
Bestätigung der Hardening-Rolle.
5. 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.