connect fix 4
This commit is contained in:
112
README.md
112
README.md
@ -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
|
||||
|
||||
Reference in New Issue
Block a user