connect fix 4

This commit is contained in:
2026-08-21 06:14:10 +02:00
parent bf2a02679d
commit 5e506c9921
17 changed files with 1266 additions and 162 deletions

112
README.md
View File

@ -328,13 +328,123 @@ protokollieren jetzt und geben den Grund als WebSocket-Close-Reason mit, den
Passphrase nachtragen (Adminbereich -> Zugangsdaten -> SSH-Key bearbeiten).
3. Bei RDP-Hosts pruefen, dass ein RDP-Benutzername gesetzt ist.
## Phase 11: fehlendes `bcrypt` im Virtualenv
Symptom: `Der private Schluessel konnte nicht gelesen werden: OpenSSH private
key encryption requires bcrypt with KDF support`.
Ursache war weder der Schluessel noch die (seit Phase 10 speicherbare)
Passphrase, sondern die **Serverumgebung**: OpenSSH verschluesselt private
Schluessel mit `bcrypt_pbkdf`. asyncssh und `cryptography` fuehren diese
Ableitung nur aus, wenn das Python-Modul `bcrypt` installiert ist -- es ist
bei beiden ein *optionales* Extra (`asyncssh[bcrypt]`) und stand nicht in
`requirements.txt`. Im ausgerollten Virtualenv fehlte es deshalb, und damit
war jeder mit `ssh-keygen` erzeugte passphrasegeschuetzte Schluessel
prinzipiell unlesbar -- unabhaengig davon, ob die Passphrase stimmte.
Behoben:
* `bcrypt` steht jetzt als **Pflichtabhaengigkeit** in `requirements.txt`
(gepinnt wie alle anderen; entspricht dem Extra `asyncssh[bcrypt]`).
* `import_private_key_material()` uebersetzt die englische Bibliotheksmeldung
in einen deutschen Klartext, der die Ursache benennt (Serverumgebung, nicht
Schluessel/Passphrase) und den Behebungsweg nennt. Diese Meldung erscheint
im Terminal, im Dateitransfer und als HTTP 400 beim Key-Upload.
* Der Anwendungsstart prueft die Abhaengigkeit einmalig und schreibt eine
ERROR-Zeile ins Log (und damit ins Live-Verbindungslog des Adminbereichs),
statt den Fehler bis zur ersten Benutzersitzung zu verschleppen.
### Nach dem Deployment zu tun
```bash
# Ansible: die Rolle python_runtime installiert requirements.txt neu
ansible-playbook -i inventory/production.ini site.yml
# oder von Hand auf dem Jumphost:
sudo -u jumphost /opt/jumphost/venv/bin/pip install -r /opt/jumphost/requirements.txt
sudo systemctl restart jumphost-app
```
Kontrolle: `journalctl -u jumphost-app | grep bcrypt` bleibt nach dem Neustart
leer. Erscheint die ERROR-Zeile, laeuft der Dienst noch gegen ein Virtualenv
ohne `bcrypt`.
## Phase 12: Benutzername gehoert zu den Zugangsdaten -- und wirksames Host-Key-Pinning
**Symptom:** `Host-Key konnte nicht ermittelt werden -- Ziel nicht erreichbar:
Permission denied for user l4u on host <ip>`.
Das Ziel war sehr wohl erreichbar: der Key-Exchange war laengst durch, nur die
anschliessende Anmeldung scheiterte. Die Ermittlung lief bisher ueber einen
vollstaendigen `asyncssh.connect()`-Versuch und las den Fingerprint aus dem
Callback `SSHClient.validate_host_public_key()`. **Diesen Callback ruft
asyncssh nur auf, wenn `known_hosts` nicht `None` ist** -- mit
`known_hosts=None` ist die Host-Key-Pruefung komplett abgeschaltet. Der
Fingerprint blieb deshalb leer, und der voellig erwartbare Auth-Fehler wurde
als "Ziel nicht erreichbar" gemeldet.
**Dieselbe Ursache hatte eine sicherheitsrelevante zweite Wirkung:** der
regulaere Verbindungspfad benutzte exakt dieselbe Kombination. Das
"Strict Host Key Checking" gegen den gepinnten Fingerprint lief also
faktisch nie -- der Server wurde ueberhaupt nicht geprueft.
Neu:
* Die Ermittlung benutzt `asyncssh.get_server_host_key()`: reiner
Key-Exchange, gar kein Anmeldeversuch. Ein Auth-Fehler kann sie damit nicht
mehr verhindern. Gespeichert wird jetzt neben dem Fingerprint auch der
**vollstaendige Host-Key** (`hosts.ssh_host_key`, Migration 0010).
* Vor jeder SSH-Sitzung wird der Host-Key des Ziels auf demselben Weg geholt
und gegen den hinterlegten geprueft -- **bevor** Schluesselmaterial
entschluesselt oder eine Anmeldung versucht wird. Weicht er ab, bricht die
Sitzung mit Klartextmeldung ab. Nach dem Sitzungsaufbau wird der Schluessel
der Sitzung noch einmal gegengeprueft (`connection.get_server_host_key()`),
damit auch ein Wechsel zwischen Pruefung und Sitzung auffaellt.
* Ist fuer einen Host **kein** Host-Key hinterlegt, wird die Verbindung
abgelehnt (`HostKeyNotPinnedError`) statt stillschweigend jedem Gegenueber
zu vertrauen. Kein Trust-on-First-Use.
* Fuer Hosts, deren Key vor Migration 0010 ermittelt wurde, wird der
vollstaendige Schluessel beim naechsten Verbindungsaufbau nachgetragen --
nachdem er gegen den gepinnten Fingerprint geprueft wurde.
**Benutzername gehoert zur Anmeldung, nicht zum Server (Migration 0010).**
`hosts.ssh_username` / `hosts.rdp_username` / `hosts.rdp_domain` waren am
falschen Objekt: der Anmeldename ist Teil der Zugangsdaten. Ab jetzt:
* **SSH:** `ssh_keys.username` -- ein Benutzername je Schluessel ("dieser Key
ist der Zugang von l4u"). Fuer ein zweites Konto wird ein zweiter
Schluesseleintrag angelegt.
* **RDP/Windows:** `rdp_credentials.username` und `.domain` -- zusammen mit
dem Passwort, das ohnehin schon dort liegt. Der Benutzername ist beim
Setzen der Zugangsdaten Pflicht.
* Die Felder verschwinden aus dem Host-Formular; das Hostdetail zeigt
stattdessen an, mit welchem Namen die Anmeldung erfolgen wuerde.
* Die Migration uebernimmt die alten Werte: bei RDP eindeutig, bei SSH nur
dann, wenn alle Hosts eines Schluessels denselben Benutzernamen hatten --
sonst bliebe es Raten. Solche Schluessel bleiben leer und greifen weiter auf
den Host-Wert zurueck, bis ein Admin den Namen am Schluessel setzt (die
Oberflaeche weist darauf hin).
* Die alten Spalten bleiben als Fallback lesbar (ein Spalten-Drop erzwaenge in
SQLite einen Tabellen-Rebuild, siehe Begruendung in `0006_tenants.sql`), die
API nimmt sie als `deprecated` weiterhin entgegen.
### Nach dem Deployment zu tun
1. Migration 0010 laeuft beim Start automatisch.
2. Fuer jeden SSH-Key pruefen, ob der Benutzername gesetzt ist
(Adminbereich -> Zugangsdaten -> Spalte "Benutzer").
3. Bei RDP-Hosts die Zugangsdaten einmal neu speichern (Benutzername +
Domaene + Passwort), falls die Uebernahme nichts gefunden hat.
4. Fuer SSH-Hosts ohne hinterlegten Host-Key einmal "Host-Key ermitteln"
ausfuehren -- ohne ihn wird die Verbindung jetzt abgelehnt.
## Tests
```bash
pytest -q
```
78 Tests (vorher 68; `tests/test_phase10.py` kam hinzu) decken ab: Argon2id/TOTP-Grundfunktionen, Audit-Hash-Chain
105 Tests (vorher 88; `tests/test_phase12.py` kam hinzu) decken ab: Argon2id/TOTP-Grundfunktionen, Audit-Hash-Chain
(inkl. Manipulationserkennung und Trigger-Durchsetzung), RBAC-Logik inkl.
Ablaufdaten, den vollstaendigen Login-Flow (Passwort -> TOTP-Enrollment ->
Session-Cookie -> geschuetzte Endpunkte) gegen die echte FastAPI-App, 17