# JIT SSH-Portal mit Telegram-2FA (Just-In-Time Firewall-Freischaltung) > Muster (2026-08-03, `https://ssh.perfekt-ohne-en.de`): SSH-Port 22 bleibt standardmäßig für die Welt > GESCHLOSSEN und wird nur nach doppelter Authentifizierung (Passwort + Telegram-2FA) für die > angefragte IP temporär geöffnet (1h) → Auto-Close. Besser als permanent offenes SSH. ## Architektur ``` ssh.perfekt-ohne-en.de (nginx+SSL) → proxy_pass → 127.0.0.1:8091 (Flask) ├─ SQLite (users, sessions) ├─ Telegram-2FA (6-stelliger Code an Nutzer-Chat) └─ UFW: öffnet Port 22 für angefragte IP (1h) ``` **Ablauf (end-to-end):** 1. Login `mehlzoerwer` + **Erstpasswort** `St230918kM@vps` → gilt NUR 1×, dann **PW-Wechsel-Pflicht** 2. 2. Login mit neuem Passwort → liest `pw_changed`-Flag → leitet zur **2FA**-Seite 3. Backend sendet **6-stelligen Code per Telegram** an Nutzer-Chat (out-of-band, Kanal getrennt vom Web) 4. Nach korrektem Code: `ufw allow from to any port 22 proto tcp` (Kommentar "SSH JIT Portal") 5. **Auto-Close:** Hintergrund-Thread prüft alle 60s Sessions mit `expiry <= now` → `ufw delete`, Status `closed` ## Dateien & Infrastruktur - App: `/opt/sshportal/app.py` (Flask, Telegram-Token aus `/root/.hermes/.env`, einheitlich via `re.search(key+r"\s*=\s*(\S+)")`) - venv: `/opt/sshportal/venv` (flask, requests; Passwort-Hash via `werkzeug.security.generate_password_hash/check_password_hash` — kein eigener bcrypt-Aufruf nötig) - systemd: `/etc/systemd/system/sshportal.service` → `active` auf `127.0.0.1:8091` (nur localhost; nginx davor) - DB: `/opt/sshportal/portal.db`, Log: `/opt/sshportal/portal.log` - nginx vhost + Let's-Encrypt via Certbot (Standard `certbot --nginx -d ... --redirect`) ## Sicherheits-Parameter (der Wert des Musters) - **Erstpasswort-Einmalnutzung:** `pw_changed=0` → Login erzwingt PW-Wechsel. - **2FA out-of-band:** Telegram = getrennter Kanal vom Web-Portal. - **Code-TTL 120s**, max 3 Fehlversuche → Login gesperrt + Telegram-Alarm. - **Firewall-Freischaltung zeitlich begrenzt:** 1h, danach automatische UFW-Löschung. - **Nur die angefragte IP** wird geöffnet (keine Subnetze). - Flask-Inhalte: `render_template_string(..., content=Markup(...))` damit HTML-Strings als Form nicht escaped werden. ## ⚠️ KRITISCHER PITFALL: Test verändert den laufenden Zustand End-to-End-Test (Login mit Erstpasswort → PW-Wechsel auf Test-Passwort) **überschreibt das echte Passwort**. Der Nutzer kann danach nicht mehr mit dem Erstpasswort einloggen → fälschliche "falsches Passwort"-Meldung. **Regel (Betriebsregel #1):** Tests/Änderungen an laufenden Systemen MÜSSEN beendet oder zurückgesetzt werden — nie Test-Artefakte hinterlassen. Sonst Rückfrage statt Test. **Reset nach Test:** ```bash systemctl stop sshportal.service /opt/sshportal/venv/bin/python -c " import sqlite3; from werkzeug.security import generate_password_hash c=sqlite3.connect('/opt/sshportal/portal.db') c.execute('UPDATE users SET pw_hash=?, pw_changed=0 WHERE username=?', (generate_password_hash('St230918kM@vps'),'mehlzoerwer')) c.commit(); c.close()" systemctl start sshportal.service ``` ## Verifikation (im Bau bestätigt) | Test | Status | |------|--------| | Login Erstpasswort → Redirect change-pw | ✅ 302 | | PW-Wechsel → "Passwort geändert" (Log) | ✅ | | Login neues PW → Redirect 2fa | ✅ 302 | | 2FA-Code → Telegram-Chat | ✅ Log "2FA-Code gesendet" | | UFW allow für IP | ✅ "Rule added" | | Auto-Close (ufw delete nach 1h) | ✅ "Rule deleted" |