# Konzept: Sicherer Multi-Protokoll-Jumphost (SSH/RDP) mit Web-Gateway **Version:** 0.2 (Entwurf)  |  **Datum:** 19.08.2026  |  **Autor:** Konzepterstellung für Midas **Änderungen gegenüber v0.1:** Grundannahme „Jumphost läuft auf Linux" bestätigt; TLS-Zertifikatsmodi für den nginx-Betrieb ergänzt (internes PKI-Zertifikat auf dem Jumphost selbst **oder** vorgelagerter externer Reverse Proxy mit eigener PKI/TLS-Terminierung), siehe Kap. 6.3, 7.2 und 9. ## Inhaltsverzeichnis 1. Zielsetzung & Grundannahmen 2. Anforderungsübersicht 3. Architekturüberblick 4. Komponentenbeschreibung 5. Datenmodell (SQLite) 6. Security- und Hardening-Konzept 7. Ansible-Deployment-Konzept 8. Systemd-Services 9. Betrieb mit vs. ohne nginx 10. Rollout-Plan & Teststrategie 11. Technologie-Stack (Zusammenfassung) 12. Offene Punkte & Erweiterungsoptionen --- ## 1. Zielsetzung & Grundannahmen Ziel ist ein zentraler **Jumphost/Bastion-Gateway**, über den sämtlicher administrativer Zugriff auf Ziel-Systeme (Linux via SSH, Windows via RDP) erfolgt. Nutzer greifen ausschließlich **browserbasiert** auf eine Weboberfläche zu – es wird kein lokal installierter SSH- oder RDP-Client benötigt. Dadurch lassen sich Vollbildmodus, Copy & Paste, Session-Aufzeichnung und Auditierung zentral und clientunabhängig umsetzen. **Grundannahmen:** - Der **Jumphost-Server selbst läuft auf Linux** (Voraussetzung für systemd, empfohlen: Debian/Ubuntu LTS oder RHEL/Rocky). "Windows und Linux" bezieht sich auf die **Ziel-Systeme**, nicht auf das Betriebssystem des Jumphosts. ✅ *Bestätigt.* - Zielgröße: kleine bis mittlere Umgebung (bis ca. 200 Nutzer, bis ca. 1000 verwaltete Hosts). SQLite ist dafür bei korrekter Konfiguration (WAL-Modus, ein Schreibprozess) ausreichend performant. Bei deutlich höherer Last ist ein Umstieg auf PostgreSQL als Erweiterungsoption vorgesehen (siehe Kap. 12). - Es wird von einer **On-Premises- oder privaten Cloud-Umgebung** ausgegangen, in der der Jumphost die einzige erlaubte Route zu den Zielsystemen ist (Netzwerksegmentierung: Zielsysteme sind aus dem Nutzernetz nicht direkt erreichbar). --- ## 2. Anforderungsübersicht | Anforderung | Umsetzung im Konzept | |---|---| | Jumphost für Windows- und Linux-Ziele | RDP-Proxy für Windows-Ziele, SSH-Proxy für Linux-Ziele | | Userverwaltung mit TOTP | Lokale Userverwaltung, Pflicht-2FA via TOTP (RFC 6238) | | Hostgruppen | RBAC-Modell: Rollen × Hostgruppen × Rechte | | SSH inkl. Keyverwaltung | Zentrale, verschlüsselte Keyverwaltung, serverseitige Nutzung, keine Key-Auslieferung an Browser | | RDP | Browserbasierter RDP-Client über guacd/FreeRDP | | Dateitransfer | SFTP-Kanal (SSH-Ziele) und Laufwerksumleitung (RDP-Ziele) | | Vollbild | HTML5-Client im Browser-Fullscreen-Modus | | Copy & Paste | Clipboard-Synchronisation über Guacamole-Protokoll, pro Hostgruppe deaktivierbar | | Auditlog | Manipulationssicheres, verkettetes Audit-Log aller Aktionen | | SQLite & Python | Backend in Python (FastAPI/ASGI), SQLite als Datenhaltung | | Ansible-Playbook | Rollenbasiertes Playbook, Modus mit/ohne nginx | | systemd Service | Gehärtete Unit-Files für alle Dienste | | Höchste Security/Hardening | Durchgängiges Hardening auf Anwendungs-, Transport- und OS-Ebene (Kap. 6) | --- ## 3. Architekturüberblick ``` ┌─────────────────────────────────────────┐ │ Nutzer (Browser) │ │ HTML5-Terminal (xterm.js) + Guacamole- │ │ Client (guacamole-common-js), TOTP-Login│ └───────────────────┬───────────────────────┘ │ HTTPS/WSS (TLS 1.3) ┌────────────────────────────────┼────────────────────────────────┐ │ (optional) nginx – TLS-Terminierung, │ │ Security-Header, Rate-Limiting, WS-Proxy │ └────────────────────────────────┬────────────────────────────────┘ │ (localhost / unix socket) ┌────────────────────────────────▼────────────────────────────────┐ │ Jumphost-App (Python, FastAPI, ASGI) │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌──────────────────┐ │ │ │ Auth & │ │ RBAC / │ │ Session- │ │ Audit-/Logging- │ │ │ │ TOTP │ │ Hostgrp. │ │ Broker │ │ Engine │ │ │ └───────────┘ └───────────┘ └───────────┘ └──────────────────┘ │ │ ┌───────────────────┐ ┌───────────────────────────────────┐ │ │ │ SSH-Proxy-Modul │ │ RDP-Proxy-Anbindung (guacd) │ │ │ │ (asyncssh) │ │ via Guacamole-Protokoll │ │ │ └─────────┬─────────┘ └───────────────┬───────────────────┘ │ │ │ SQLite (WAL) – Users, Hosts, Keys (verschl.), │ │ │ Sessions, Audit-Log, Hostgruppen │ └────────────┼─────────────────────────────┬─────────────────────┘ │ SSH (Keys aus Vault) │ Guacamole-Protokoll ┌────────────▼───────────┐ ┌───────────▼──────────────┐ │ Linux-Zielsysteme │ │ guacd (Protokoll-Daemon) │ │ (SSH, SFTP) │ │ → RDP via FreeRDP │ └─────────────────────────┘ └───────────┬───────────────┘ │ RDP (verschl.) ┌───────────▼──────────────┐ │ Windows-Zielsysteme │ └───────────────────────────┘ ``` **Designentscheidung RDP-Proxy:** Für RDP wird empfohlen, **nicht** das komplette Protokoll neu zu implementieren, sondern den bewährten, quelloffenen **guacd**-Daemon (Apache-Guacamole-Projekt, Protokoll-Engine inkl. FreeRDP-Anbindung) als reine Protokoll-Übersetzungskomponente einzusetzen. Die eigentliche Anwendung – Auth, TOTP, RBAC, Hostgruppen, Audit, Oberfläche – bleibt vollständig eigenentwickelt in Python/SQLite; **die Java-basierte Guacamole-Client-Webanwendung selbst wird nicht verwendet**. guacd läuft als eigener, isolierter systemd-Dienst und kommuniziert nur lokal (Unix-Socket oder localhost) mit dem Python-Backend. Für SSH wird kein guacd benötigt – hier terminiert das Python-Backend die Verbindung selbst über eine asynchrone SSH-Bibliothek (`asyncssh`) und pusht Terminal-I/O per WebSocket an `xterm.js` im Browser. --- ## 4. Komponentenbeschreibung ### 4.1 Backend-Anwendung (Python) - **Framework:** FastAPI (ASGI) mit Uvicorn/Hypercorn als Server, `asyncio`-basiert für nebenläufige Sessions. - **API:** REST für Verwaltung (User, Hosts, Gruppen, Keys), WebSocket-Endpunkte für Live-Sessions (SSH-Terminal-Stream, Guacamole-Protokoll-Tunnel). - **ORM/DB-Zugriff:** SQLAlchemy Core oder direkt `sqlite3`/`aiosqlite` mit strikt parametrisierten Queries (kein String-Concat), Pydantic-Modelle zur Validierung aller Eingaben. - **Templating/Frontend:** Serverseitig ausgeliefertes, schlankes SPA (z. B. mit Jinja2 + Vanilla-JS/xterm.js/guacamole-common-js), keine unnötigen externen Abhängigkeiten, Assets lokal gehostet (kein CDN-Bezug, reduziert Angriffsfläche und funktioniert offline). ### 4.2 SSH-Proxy-Modul - Verbindungsaufbau zum Zielsystem **serverseitig** über `asyncssh`, unter Verwendung des zentral verwalteten Schlüssels (siehe 4.4). - Der Nutzer sieht im Browser nur ein Terminal (xterm.js über WebSocket) – der private Schlüssel verlässt den Server nie. - **Strict Host Key Checking**: bekannte Host-Keys werden pro Zielsystem in der DB gepinnt; unbekannte/abweichende Host-Keys blockieren die Verbindung und lösen einen Alert aus (kein automatisches TOFU-Accept in Produktion). - Terminal-I/O wird parallel in einen Session-Recorder gestreamt (siehe 6.5). - Sitzungsspezifische Idle-Timeouts und harte Maximaldauer konfigurierbar pro Hostgruppe. ### 4.3 RDP-Proxy-Anbindung - Python-Backend baut einen Guacamole-Protokoll-Tunnel zu `guacd` auf (lokal, kein Netzwerkzugriff von außen). - `guacd` verbindet sich seinerseits per RDP (TLS-verschlüsselt, NLA aktiviert) zum Windows-Zielsystem. - Browser rendert die Session über `guacamole-common-js` in einem ``-Element. - **Vollbild:** über die Browser-Fullscreen-API (`element.requestFullscreen()`), vom Backend nicht beeinflussbar, rein clientseitig aktivierbar per Button. - **Copy & Paste:** native Guacamole-Clipboard-Synchronisation (bidirektional Browser ↔ Zielsystem). Pro Hostgruppe/Verbindung serverseitig deaktivierbar (Compliance-Anforderung für hochsensible Systeme). - **Dateitransfer bei RDP:** über Guacamole-Laufwerksumleitung (virtuelles Laufwerk `Guacamole RDP FS`), serverseitig ein-/ausschaltbar, mit Upload-Größenlimit und optionalem AV-Scan-Hook (ClamAV) vor Freigabe. ### 4.4 Keyverwaltung (SSH) - Private Schlüssel werden **verschlüsselt in SQLite** abgelegt (AES-256-GCM), der Schlüssel-Verschlüsselungs-Schlüssel (KEK) liegt **nicht** in der Datenbank, sondern in einer separaten, restriktiv berechtigten Datei (`0600`, dedizierter Service-User) oder – empfohlen – im systemd-Credential-Store (`systemd-creds`) bzw. optional einer externen KMS/Vault-Anbindung. - Unterstützt werden sowohl klassische statische Schlüsselpaare (Ed25519, min. RSA-3072 falls Legacy nötig) als auch **kurzlebige, CA-signierte SSH-Zertifikate** (empfohlener Zielzustand): der Jumphost besitzt eine eigene SSH-CA, signiert bei Sessionstart ein Zertifikat mit z. B. 5 Minuten Gültigkeit, das Zielsystem vertraut nur der CA. Dadurch entfällt die Verteilung/Rotation statischer Public Keys auf allen Zielsystemen komplett. - Rotation: automatisierte Rotation statischer Schlüssel über Ansible-Playbook, Journaling jeder Rotation im Audit-Log. ### 4.5 Userverwaltung & Authentifizierung - Lokale User-Tabelle mit Argon2id-Passwort-Hashes (Fallback: bcrypt, falls Argon2-Bibliothek in der Zielumgebung nicht verfügbar). - **TOTP verpflichtend** für alle Accounts (RFC 6238, kompatibel mit gängigen Authenticator-Apps), Secret verschlüsselt gespeichert, QR-Code-Enrollment nur einmalig anzeigbar, Recovery-Codes (einmalig, gehasht gespeichert). - Vorbereitet für optionale spätere Erweiterung um WebAuthn/FIDO2 als Ersatz/Ergänzung zu TOTP sowie LDAP/AD-Bind als externe Quelle (siehe Kap. 12). - Account-Lockout nach N Fehlversuchen (progressive Verzögerung), erzwungener Passwortwechsel nach Erstanmeldung, Session-Timeout (idle + absolute). ### 4.6 Rollen, Hostgruppen & Rechte (RBAC) - **Hostgruppen** bündeln Zielsysteme (z. B. „Prod-Linux-DB", „Windows-Fileserver", „DMZ"). - **Rollen** definieren, was ein Nutzer/eine Gruppe innerhalb einer Hostgruppe darf: `ssh_connect`, `rdp_connect`, `file_transfer`, `clipboard`, `session_recording_view` (Playback fremder Sessions, z. B. für Auditoren), `admin_hostgroup`. - Zuordnung erfolgt über eine Verknüpfungstabelle User/Gruppe × Hostgruppe × Rolle (siehe Datenmodell), sodass granular z. B. „Team A darf SSH auf Hostgruppe X, aber kein Copy & Paste und keinen Filetransfer" abgebildet werden kann. - Optional vorbereitet: zeitlich befristete/Just-in-Time-Freigaben (Access-Requests mit Ablaufzeit) als Erweiterung. ### 4.7 Audit-Log - Erfasst: Login/Logout (inkl. Fehlversuche), TOTP-Ereignisse, Sessionstart/-ende je Zielsystem, Dateitransfers (Richtung, Dateiname, Größe, Hash), Rechteänderungen, Admin-Aktionen, Verbindungsabbrüche, Host-Key-Mismatches. - Manipulationssicherheit: jeder Log-Eintrag enthält den Hash des Vorgängereintrags (Hash-Chain, ähnlich einer einfachen Blockchain) – nachträgliches Verändern oder Löschen einzelner Einträge wird durch Kettenbruch erkennbar. Zusätzlich optionale Ausleitung an externen Syslog/SIEM in Echtzeit (Log verlässt damit den Jumphost, unabhängige Kopie). - Die Audit-Tabelle ist **append-only** auf Anwendungsebene (kein UPDATE/DELETE-Pfad in der API); zusätzlich empfiehlt sich ein DB-Trigger, der `UPDATE`/`DELETE` auf der Tabelle verweigert. --- ## 5. Datenmodell (SQLite) Vereinfachtes Kernschema (Auszug, ohne Indizes/Constraints der Übersicht halber gekürzt): ```sql PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, -- Argon2id totp_secret_enc BLOB NOT NULL, -- AES-256-GCM verschlüsselt totp_enrolled INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, is_admin INTEGER NOT NULL DEFAULT 0, failed_logins INTEGER NOT NULL DEFAULT 0, locked_until TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), password_changed_at TEXT ); CREATE TABLE recovery_codes ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, code_hash TEXT NOT NULL, used_at TEXT ); CREATE TABLE roles ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL -- z.B. ssh_connect, rdp_connect, file_transfer, clipboard, admin ); CREATE TABLE host_groups ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, description TEXT ); CREATE TABLE hosts ( id INTEGER PRIMARY KEY, host_group_id INTEGER NOT NULL REFERENCES host_groups(id), hostname TEXT NOT NULL, address TEXT NOT NULL, protocol TEXT NOT NULL CHECK (protocol IN ('ssh','rdp')), port INTEGER NOT NULL, os_type TEXT NOT NULL CHECK (os_type IN ('linux','windows')), ssh_host_key_fingerprint TEXT, -- Pinning für Strict Host Key Checking rdp_require_nla INTEGER NOT NULL DEFAULT 1, clipboard_enabled INTEGER NOT NULL DEFAULT 1, file_transfer_enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE ssh_keys ( id INTEGER PRIMARY KEY, label TEXT NOT NULL, owner_user_id INTEGER REFERENCES users(id), -- NULL = shared/service key private_key_enc BLOB NOT NULL, -- AES-256-GCM, KEK extern public_key TEXT NOT NULL, key_type TEXT NOT NULL, -- ed25519, rsa-3072, ca-cert... created_at TEXT NOT NULL DEFAULT (datetime('now')), rotated_at TEXT, expires_at TEXT ); CREATE TABLE host_ssh_key_map ( host_id INTEGER NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, ssh_key_id INTEGER NOT NULL REFERENCES ssh_keys(id), PRIMARY KEY (host_id, ssh_key_id) ); CREATE TABLE user_hostgroup_roles ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, host_group_id INTEGER NOT NULL REFERENCES host_groups(id) ON DELETE CASCADE, role_id INTEGER NOT NULL REFERENCES roles(id), granted_by INTEGER REFERENCES users(id), granted_at TEXT NOT NULL DEFAULT (datetime('now')), expires_at TEXT, -- optionale zeitlich befristete Freigabe PRIMARY KEY (user_id, host_group_id, role_id) ); CREATE TABLE sessions ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), host_id INTEGER NOT NULL REFERENCES hosts(id), protocol TEXT NOT NULL, started_at TEXT NOT NULL DEFAULT (datetime('now')), ended_at TEXT, client_ip TEXT NOT NULL, recording_path TEXT, -- Pfad/Referenz zur Session-Aufzeichnung end_reason TEXT -- logout, timeout, error, admin_kill ); CREATE TABLE file_transfers ( id INTEGER PRIMARY KEY, session_id INTEGER NOT NULL REFERENCES sessions(id), direction TEXT NOT NULL CHECK (direction IN ('upload','download')), filename TEXT NOT NULL, size_bytes INTEGER NOT NULL, sha256 TEXT NOT NULL, av_scan_result TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE audit_log ( id INTEGER PRIMARY KEY, ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), user_id INTEGER REFERENCES users(id), event_type TEXT NOT NULL, -- login, login_failed, session_start, ... details_json TEXT NOT NULL, prev_hash TEXT NOT NULL, entry_hash TEXT NOT NULL -- sha256(prev_hash || ts || event_type || details_json) ); -- Schutz gegen nachträgliche Manipulation auf DB-Ebene: CREATE TRIGGER no_audit_update BEFORE UPDATE ON audit_log BEGIN SELECT RAISE(ABORT, 'audit_log ist append-only'); END; CREATE TRIGGER no_audit_delete BEFORE DELETE ON audit_log BEGIN SELECT RAISE(ABORT, 'audit_log ist append-only'); END; ``` --- ## 6. Security- und Hardening-Konzept Dies ist der **wichtigste Teil** des Konzepts und durchzieht alle Ebenen: Anwendung, Transport, Betriebssystem, Prozessisolation. ### 6.1 Threat-Model (Kurzfassung) Wesentliche Angriffsvektoren, gegen die das Design hart absichert: kompromittierte Nutzer-Credentials (→ Pflicht-2FA, Lockout), Diebstahl von SSH-Schlüsseln (→ Keys nie im Client, Verschlüsselung at rest, CA-Zertifikate mit kurzer TTL), Man-in-the-Middle (→ TLS 1.3 only, Host-Key-Pinning), Rechteausweitung über die Web-App (→ strikte Eingabevalidierung, parametrisierte Queries, RBAC serverseitig durchgesetzt), Manipulation von Audit-Daten durch einen kompromittierten Admin-Account (→ Hash-Chain + externe Log-Ausleitung), Kompromittierung des Jumphost-Prozesses selbst (→ systemd-Sandboxing, minimale Rechte, Netzwerksegmentierung als zusätzliche Schicht). ### 6.2 Authentifizierung & Session-Sicherheit - Argon2id-Hashing (Parameter an aktuelle OWASP-Empfehlung angelehnt, z. B. m=19 MiB, t=2, p=1 als Startwert, umgebungsabhängig kalibrieren). - TOTP verpflichtend, kein Login ohne zweiten Faktor möglich; Secret-Verschlüsselung mit separatem Schlüssel (nicht identisch mit KEK der SSH-Keys, Prinzip der Schlüsseltrennung). - Rate-Limiting & progressive Lockout gegen Brute-Force (pro Account und pro Quell-IP), zusätzlich Fail2ban auf OS-Ebene als zweite Verteidigungslinie gegen Login-Endpoint-Scanning. - Session-Cookies: `HttpOnly`, `Secure`, `SameSite=Strict`, kurze Lebensdauer + Refresh, serverseitige Session-Invalidierung bei Passwortänderung. - Absolute und Idle-Timeouts pro Hostgruppe konfigurierbar; sensible Aktionen (z. B. Schlüsselverwaltung, Rechtevergabe) verlangen Re-Authentifizierung. ### 6.3 Transportverschlüsselung - **Ausschließlich TLS 1.3** (TLS 1.2 nur als Übergangs-Fallback, falls zwingend erforderlich), moderne Cipher-Suiten, HSTS mit langer `max-age` und `includeSubDomains`. - WebSocket-Verbindungen (Terminal-Stream, Guacamole-Tunnel) laufen ausschließlich über WSS. - Optional Mutual-TLS für administrative API-Zugriffe (z. B. Ansible-getriebene Konfigurationsänderungen von einer festen Management-Quelle aus). - Zertifikate über Ansible-Rolle verwaltet. Im nginx-Betrieb werden dabei **zwei TLS-Zertifikatsmodi** unterstützt (Details und Konfiguration siehe Kap. 7.2): 1. **`internal_pki`** – nginx auf dem Jumphost terminiert TLS selbst, mit einem Zertifikat aus einer internen/unternehmenseigenen PKI (kein direkter Internetbezug nötig). 2. **`external_reverse_proxy`** – ein bereits vorhandener, vorgelagerter externer Reverse Proxy/Load Balancer übernimmt die öffentliche TLS-Terminierung mit einer eigenen (unternehmensweiten oder öffentlichen) PKI; das lokale nginx auf dem Jumphost dient dann als zweite, interne Schicht. In beiden Fällen bleibt der Grundsatz „TLS 1.3 end-to-end bis zur Anwendung" erhalten – unverschlüsseltes internes HTTP wird nicht als Standard vorgesehen (siehe 7.2). ### 6.4 Schlüssel- und Geheimnisverwaltung - Trennung von Datenverschlüsselungsschlüsseln (DEK, in DB pro Datensatz) und Schlüsselverschlüsselungsschlüsseln (KEK, außerhalb der DB). - KEK-Ablage über `systemd-creds` (LoadCredentialEncrypted) oder TPM-gebundene Speicherung, wo verfügbar; alternativ Anbindung an HashiCorp Vault/OpenBao als Erweiterungsoption. - Keine Secrets in Ansible-Klartext-Variablen – ausschließlich über `ansible-vault` verschlüsselt, mit Vault-Passwort außerhalb des Repositories (z. B. über CI-Secret-Store oder separates Passwort-Management). - Konsequentes Prinzip: **kein Private Key verlässt jemals den Server-Prozess** – weder Richtung Browser noch Richtung Log. ### 6.5 Session-Aufzeichnung & Integrität - SSH-Sessions werden als Terminal-Cast (z. B. asciicast-kompatibles Format) aufgezeichnet, RDP-Sessions als Guacamole-native Aufzeichnung (serverseitig, ohne Zusatzsoftware auf dem Client). - Aufzeichnungen werden mit fortlaufendem Hash verkettet (analog Audit-Log), um nachträgliche Manipulation erkennbar zu machen. - Zugriff auf Aufzeichnungen selbst wieder RBAC-gesteuert (`session_recording_view`), jeder Playback-Zugriff wird auditiert (Audit-Log über das Audit-Log, „wer hat wessen Session angeschaut"). - Aufbewahrungsfrist konfigurierbar, automatische Löschung/Archivierung nach Ablauf (Datenschutz-/Compliance-Anforderungen beachten, insb. bei mitgeloggtem Tastatur-Input). ### 6.6 Web-Anwendungs-Hardening - CSRF-Schutz für alle zustandsändernden Endpunkte, striktes Content-Security-Policy-Header (kein `unsafe-inline`, keine externen Script-Quellen), `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`. - Sämtliche API-Eingaben über Pydantic-Schemas validiert (Typ, Länge, Whitelist erlaubter Zeichen), Datei-Uploads mit Größenlimit, Dateityp-Prüfung (Magic-Bytes, nicht nur Endung) und optionalem AV-Scan (ClamAV) vor Freigabe zum Download durch andere Nutzer. - Abhängigkeiten fixiert (Lockfile), regelmäßiger Scan mit `pip-audit`/`safety` in CI, minimaler Dependency-Footprint. - Keine Debug-/Stacktrace-Ausgabe in Produktionsfehlermeldungen; strukturierte Fehlerbehandlung, generische Fehlermeldungen nach außen. ### 6.7 Betriebssystem- und Prozess-Hardening - Dedizierter, unprivilegierter Service-User pro Dienst (App, guacd), kein Root-Betrieb. - systemd-Sandboxing (Details Kap. 8): `NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`, `ProtectKernelTunables`, `ProtectKernelModules`, `ProtectControlGroups`, `RestrictNamespaces`, `RestrictAddressFamilies`, `RestrictSUIDSGID`, `MemoryDenyWriteExecute`, `SystemCallFilter=@system-service`, `CapabilityBoundingSet=` minimal (nur was zwingend nötig, im Regelfall leer). - Firewall (nftables) im Default-Deny-Modus: eingehend nur 443/tcp (bzw. App-Port bei Betrieb ohne nginx) und ggf. 22/tcp für administrativen Zugriff auf den Jumphost selbst (idealerweise ebenfalls nur aus einem Management-Netz); ausgehend nur zu den definierten Zielsystem-Netzen. - Automatische Sicherheitsupdates (`unattended-upgrades`) für das Basissystem, geregelter Patch-Prozess für die Anwendung selbst. - `auditd` auf OS-Ebene zusätzlich zum Anwendungs-Audit-Log, insbesondere für Zugriffe auf die SQLite-Datei und Konfigurationsdateien. - SELinux (RHEL-Familie) bzw. AppArmor (Debian/Ubuntu)-Profil als zusätzliche Verteidigungslinie empfohlen (im Playbook als optionale Rolle vorgesehen). ### 6.8 Backup & Wiederherstellung - Verschlüsselte, regelmäßige Backups der SQLite-Datei (Achtung: konsistente Snapshots via SQLite-Online-Backup-API oder `VACUUM INTO`, nicht per einfachem Filecopy während des Betriebs). - KEK/Vault-Backup getrennt von DB-Backup aufbewahren (sonst hebelt ein gemeinsamer Backup-Diebstahl die Verschlüsselung aus). - Dokumentierter Wiederherstellungs-Testlauf als fester Bestandteil des Betriebskonzepts (mind. jährlich). ### 6.9 Monitoring & Alerting - Prometheus-Metrics-Endpoint (intern, nicht öffentlich) für aktive Sessions, fehlgeschlagene Logins, Fehlerraten. - Alerts bei: gehäuften Fehlversuchen, Host-Key-Mismatch, Zugriff außerhalb üblicher Zeiten/Netze, Bruch der Audit-Hash-Chain. - Externe Log-Ausleitung (Syslog/SIEM) als unabhängige zweite Quelle für forensische Auswertung, auch falls der Jumphost selbst kompromittiert würde. --- ## 7. Ansible-Deployment-Konzept ### 7.1 Rollenstruktur ``` ansible/ ├── site.yml ├── inventory/ │ ├── production.ini │ └── group_vars/ │ ├── all.yml # gemeinsame Variablen │ ├── jumphost_with_nginx.yml # enable_nginx_proxy: true │ └── jumphost_no_nginx.yml # enable_nginx_proxy: false ├── roles/ │ ├── os_hardening/ # CIS-nahe Basis-Härtung (Firewall, SSH-Daemon des Hosts, auditd, unattended-upgrades) │ ├── firewall_nftables/ │ ├── fail2ban/ │ ├── python_runtime/ # venv, gehärtete Paketinstallation │ ├── jumphost_app/ # Deployment der eigentlichen Anwendung + systemd unit │ ├── guacd/ # Installation guacd + FreeRDP, systemd unit, lokal gebunden │ ├── sqlite_init/ # Schema-Migration, Verzeichnis-/Rechtevergabe │ ├── tls_certificates/ # tls_mode: internal_pki | external_reverse_proxy | acme_public │ ├── nginx_proxy/ # NUR wenn enable_nginx_proxy: true │ └── backup/ # Cron/systemd-Timer für verschlüsselte Backups └── files/ / templates/ ``` ### 7.2 Steuerung mit/ohne nginx Die Variable `enable_nginx_proxy` (in `group_vars`) steuert per `when`-Bedingung, ob die Rolle `nginx_proxy` inkludiert wird: ```yaml # site.yml (Auszug) - hosts: jumphosts roles: - os_hardening - firewall_nftables - fail2ban - python_runtime - sqlite_init - guacd - jumphost_app - { role: nginx_proxy, when: enable_nginx_proxy | default(false) } - tls_certificates - backup ``` Bei `enable_nginx_proxy: false` terminiert die Python-App TLS **direkt** (Uvicorn/Hypercorn mit `--ssl-certfile`/`--ssl-keyfile`), die Firewall-Rolle öffnet dann direkt den App-Port (Standard 8443) statt 443 auf nginx. ### 7.2a TLS-Zertifikatsmodi im nginx-Betrieb Zusätzlich zu `enable_nginx_proxy` steuert die Variable `tls_mode` (nur relevant, wenn nginx aktiv ist), **woher** das TLS-Zertifikat kommt, mit dem nginx auf dem Jumphost selbst terminiert bzw. ob nginx überhaupt selbst terminiert: ```yaml # group_vars/jumphost_with_nginx.yml enable_nginx_proxy: true tls_mode: internal_pki # internal_pki | external_reverse_proxy | acme_public ``` **Modus `internal_pki` – nginx terminiert selbst, Zertifikat aus interner PKI:** - Für rein interne Umgebungen ohne öffentliche Erreichbarkeit gedacht. - Die Rolle `tls_certificates` bezieht das Zertifikat entweder (a) automatisiert per ACME-Protokoll von einer internen CA, die ACME spricht (z. B. `step-ca`, oder Microsoft ADCS mit ACME-Plugin), oder (b) aus einem vorab per `ansible-vault` verschlüsselten Cert/Key-Paar, das zentral von der bestehenden Unternehmens-PKI ausgestellt und ins Repository (verschlüsselt) eingespielt wird. - Damit Browser der Nutzer das Zertifikat ohne Warnung akzeptieren, muss das interne Root-/Intermediate-CA-Zertifikat auf den Client-Rechnern als vertrauenswürdig hinterlegt sein (i. d. R. bereits über GPO/MDM im Unternehmen vorhanden – kein Bestandteil dieses Playbooks, aber als Voraussetzung dokumentiert). - Erneuerung automatisiert über systemd-Timer + Ansible-Handler (Reload von nginx bei neuem Zertifikat, ohne Verbindungsabbruch bestehender Sessions dank `nginx -s reload`). **Modus `external_reverse_proxy` – TLS-Terminierung erfolgt vorgelagert:** - Für Umgebungen, in denen bereits ein zentraler Reverse Proxy/Load Balancer (z. B. F5, HAProxy, zentrales nginx/Traefik-Ingress, Cloud-LB) existiert, der die öffentliche TLS-Terminierung mit der unternehmensweiten oder einer öffentlichen PKI übernimmt. - Empfohlene Umsetzung (**Defense-in-Depth, "TLS all the way"**): Die Strecke zwischen externem Reverse Proxy und dem lokalen nginx auf dem Jumphost wird **erneut verschlüsselt** (Re-Encryption) – das lokale nginx terminiert dabei ein zweites, internes TLS mit einem Zertifikat aus der internen PKI (technisch identisch zum `internal_pki`-Zweig), optional mit Mutual-TLS, sodass nur der bekannte externe Proxy sich verbinden kann. - Bewusste Ausnahme, **nicht Standardeinstellung**: Klartext-HTTP zwischen externem Proxy und Jumphost ist nur zulässig, wenn diese Strecke nachweislich in einem eigenen, physisch/logisch getrennten und zugriffskontrollierten Segment liegt, und erfordert eine dokumentierte Risikoakzeptanz (`tls_mode: external_reverse_proxy` mit zusätzlichem Flag `internal_hop_plaintext_accepted: true`, das im Playbook explizit gesetzt werden muss – kein stiller Default). - In diesem Modus muss dem lokalen nginx die IP-Adresse/das Subnetz des externen Reverse Proxys als vertrauenswürdig bekannt gemacht werden, damit `X-Forwarded-For` und `X-Forwarded-Proto` korrekt und nicht durch Dritte fälschbar ausgewertet werden – sonst wäre das Feld `client_ip` im Audit-Log (Kap. 5) manipulierbar: ```nginx # roles/nginx_proxy/templates/jumphost.conf.j2 (Auszug, Modus external_reverse_proxy) set_real_ip_from {{ external_reverse_proxy_cidr }}; # z.B. 10.10.5.0/24 real_ip_header X-Forwarded-For; real_ip_recursive on; server { listen 443 ssl; # zweite, interne TLS-Schicht (Re-Encryption) ssl_certificate /etc/jumphost/tls/internal.crt; ssl_certificate_key /etc/jumphost/tls/internal.key; ssl_protocols TLSv1.3; location / { proxy_pass http://unix:/run/jumphost/app.sock; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Real-IP $remote_addr; } } ``` **Modus `acme_public`** (unverändert gegenüber v0.1): Let's-Encrypt/öffentliches ACME für Deployments mit direkter Internet-Erreichbarkeit und öffentlichem DNS-Namen. Alle drei Modi werden von **derselben** Rolle `tls_certificates` über eine `case`/`when`-Verzweigung auf `tls_mode` abgedeckt – es entsteht kein separater Wartungspfad. ### 7.3 Secrets im Playbook - Alle sensiblen Variablen (KEK-Ausgangsmaterial, initiales Admin-Passwort, ggf. Vault-Zugangsdaten) liegen ausschließlich in `ansible-vault`-verschlüsselten Dateien (`group_vars/*/vault.yml`). - Playbook-Läufe erfordern `--ask-vault-pass` oder eine extern bereitgestellte Vault-ID-Datei (nicht im Repository). - Idempotenz: alle Rollen so gestaltet, dass wiederholte Ausführung keine Nebenwirkungen hat (Handler statt Direktbefehle, `creates`/`state`-Parameter konsequent genutzt). --- ## 8. Systemd-Services ### 8.1 Haupt-Anwendung (`jumphost-app.service`) ```ini [Unit] Description=Jumphost Gateway Application After=network.target [Service] Type=simple User=jumphost Group=jumphost ExecStart=/opt/jumphost/venv/bin/uvicorn app.main:app \ --uds /run/jumphost/app.sock Restart=on-failure RestartSec=5 # --- Hardening --- NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true PrivateDevices=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictNamespaces=true RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 RestrictSUIDSGID=true MemoryDenyWriteExecute=true LockPersonality=true SystemCallFilter=@system-service SystemCallErrorNumber=EPERM CapabilityBoundingSet= ReadWritePaths=/var/lib/jumphost /run/jumphost UMask=0077 [Install] WantedBy=multi-user.target ``` ### 8.2 guacd-Dienst (`guacd.service`) Analoge Hardening-Direktiven, zusätzlich `RestrictAddressFamilies=AF_UNIX AF_INET` (nur lokale Kommunikation zur App und ausgehend RDP), eigener unprivilegierter User `guacd`, kein Zugriff auf das SQLite-Datenverzeichnis. ### 8.3 Backup-Timer (`jumphost-backup.timer` / `.service`) `OnCalendar=daily`, ruft ein Skript auf, das `sqlite3 ... "VACUUM INTO '/backup/....db'"` ausführt und anschließend verschlüsselt (z. B. `age` oder GPG) ablegt; ebenfalls mit reduzierten systemd-Capabilities. --- ## 9. Betrieb mit vs. ohne nginx | Kriterium | **Mit nginx** | **Ohne nginx (direkt)** | |---|---|---| | TLS-Terminierung | Ausgereift, breite Erfahrung, einfache Cipher-Konfiguration | Möglich (Uvicorn/Hypercorn), weniger battle-tested für Internet-exponierte Lasten | | WebSocket-Handling | Sehr robust, granulare Timeout-/Buffer-Konfiguration | Funktioniert, aber weniger Tuning-Optionen | | Security-Header/Rate-Limiting | Zentral und einfach über nginx-Module/`limit_req` | Muss in der Anwendung selbst implementiert werden | | WAF-Anbindung (z. B. ModSecurity) | Möglich | Nicht ohne Zusatzkomponente | | Angriffsfläche | Zusätzliche Komponente = zusätzlicher Patch-Bedarf | Minimalere Angriffsfläche, weniger Komponenten | | Eignung | **Empfohlen für internet-exponierten/produktiven Betrieb** | Sinnvoll für rein internen Betrieb in bereits stark segmentiertem Netz oder minimalistische Testumgebungen | **Empfehlung:** Produktivbetrieb, insbesondere bei Erreichbarkeit außerhalb eines bereits stark abgesicherten internen Netzes, **mit nginx** als vorgelagertem Reverse Proxy. Der Modus „ohne nginx" bleibt als bewusst unterstützte, schlankere Option für stark isolierte interne Deployments erhalten – beide Pfade werden vom selben Playbook über eine einzige Variable gesteuert, wodurch kein Wartungsaufwand für zwei getrennte Codebasen entsteht. **TLS-Zertifikatsmodi innerhalb des nginx-Betriebs** (Details Kap. 7.2a): Sowohl der Fall „nginx terminiert selbst mit einem Zertifikat aus der internen PKI" (`internal_pki`) als auch der Fall „ein vorgelagerter externer Reverse Proxy übernimmt die TLS-Terminierung mit eigener PKI, das lokale nginx dient als zweite interne Schicht" (`external_reverse_proxy`) werden unterstützt und über die Variable `tls_mode` gesteuert – ohne dass dafür zwei getrennte Playbook-Varianten gepflegt werden müssen. --- ## 10. Rollout-Plan & Teststrategie 1. **Proof of Concept**: Kernfunktionen (Login+TOTP, ein SSH-Ziel, ein RDP-Ziel, Audit-Log) in isolierter Testumgebung. 2. **Security-Review/Penetrationstest**: vor jedem produktiven Rollout verpflichtend, insbesondere Prüfung von Session-Isolation, Key-Handling, RBAC-Bypass-Versuchen. 3. **Pilotbetrieb**: begrenzter Nutzerkreis, begrenzte Hostgruppen, engmaschiges Audit-Log-Review. 4. **Rollout**: schrittweise Ausweitung, Altlösungen (z. B. direkte SSH/RDP-Zugänge) parallel abschalten. 5. **Kontinuierlich**: automatisierte Tests (Unit-Tests für RBAC-Logik und Audit-Hash-Chain, Integrationstests für SSH-/RDP-Sessionaufbau, Dependency-Scans in CI, jährlicher Penetrationstest). --- ## 11. Technologie-Stack (Zusammenfassung) | Bereich | Technologie | |---|---| | Backend-Framework | Python 3.12+, FastAPI, Uvicorn/Hypercorn (ASGI) | | Datenbank | SQLite (WAL-Modus), `aiosqlite`/SQLAlchemy Core | | SSH | `asyncssh` | | RDP | guacd (Apache-Guacamole-Protokoll-Daemon) + FreeRDP, `guacamole-common-js` im Frontend | | Terminal-Frontend | `xterm.js` | | Auth | Argon2id (`argon2-cffi`), TOTP (`pyotp`) | | Verschlüsselung | AES-256-GCM für Secrets at rest, TLS 1.3 in transit | | Reverse Proxy (optional) | nginx | | Deployment | Ansible (Rollen, Vault) | | Prozess-Management | systemd (gehärtete Units) | | Monitoring | Prometheus-Exporter (optional), Syslog/SIEM-Anbindung | | CI/Dependency-Sicherheit | `pip-audit`, Lockfile-basierte Builds | --- ## 12. Offene Punkte & Erweiterungsoptionen - **LDAP/Active-Directory-Anbindung** als alternative/zusätzliche Identitätsquelle statt rein lokaler User. - **WebAuthn/FIDO2** als Ersatz oder Ergänzung zu TOTP für höhere Phishing-Resistenz. - **Skalierung**: bei Bedarf Migrationspfad von SQLite auf PostgreSQL (Schema ist bewusst so gehalten, dass eine Migration ohne grundlegenden Redesign möglich ist). - **Just-in-Time-Zugriff**: zeitlich befristete, genehmigungspflichtige Zugriffsanfragen statt dauerhafter Rechtevergabe. - **Hochverfügbarkeit**: Aktiv/Passiv-Setup mit synchronisierter DB und geteiltem Session-Storage bei Bedarf an Ausfallsicherheit. - **Mobile-/Tablet-Optimierung** der Weboberfläche. - **Externe Vault-Integration** (HashiCorp Vault/OpenBao) als Ersatz für die dateibasierte KEK-Verwaltung in größeren Umgebungen. --- ### Nächste Schritte 1. Zielgröße und Netzsegmentierung final festlegen (Kap. 1) sowie gewünschte Linux-Distribution für den Jumphost bestimmen. 2. Entscheidung, welcher TLS-Zertifikatsmodus (Kap. 7.2a: `internal_pki`, `external_reverse_proxy` oder `acme_public`) initial umgesetzt wird – ggf. auch mehrere, je nach Zielumgebung (z. B. intern vs. DMZ). 3. Entscheidung zu kurzlebigen SSH-CA-Zertifikaten vs. ausschließlich statischen Schlüsseln treffen (beeinflusst Ansible-Rolle und Zielsystem-Vorbereitung). 4. Freigabe dieses Konzepts als Grundlage für die Implementierungsphase (Backend-Projektskelett, DB-Migrationen, Ansible-Rollen).