Ziel: Nach SEC-1b-6 (Identity-Layer abgesichert) jetzt Transport-Sicherheit, Privilege-Separation, Operational-Security und Browser-Hardening abschließen, BEVOR MH-5b/c/d Mutation-Actions ausgeliefert werden. Wichtigste Operator-Pin: „DB-Least-Privilege ist kein optional nice-to-have mehr — nach dem DB-Wipe-Incident ist es ein struktureller Sicherheitsblocker für Production-Härtung.“
| Domain | Item | Current | Risk |
|---|---|---|---|
| Transport | nginx server_name | 81.169.213.37 (IP-only) | HIGH — blockt SEC-1d Passkey; HSTS sinnlos auf IP |
| Transport | TLS cert | self-signed /root/steves-tradingbot/ssl/dashboard.crt | HIGH — Browser-Trust-Warnung; kein Let's Encrypt installiert |
| Transport | APP_URL | http://127.0.0.1:8090 plain HTTP | HIGH — Filament generiert HTTP-Links; Secure-Cookies wirkungslos |
| Transport | nginx proxy-target | proxy_pass http://127.0.0.1:8050 (Bot-Dashboard, 502 currently) | MEDIUM — GUI auf :8090 ist nicht hinter nginx; nur lokal erreichbar |
| Transport | Bot-Dashboard exposure | 0.0.0.0:8050 (compose port) | MEDIUM — öffentlich erreichbar ohne Auth-Layer |
| Session | SESSION_ENCRYPT | false | HIGH — Session-Payload Klartext in DB |
| Session | SESSION_SECURE_COOKIE | nicht gesetzt | HIGH — Cookie ohne Secure-Flag |
| Session | SESSION_HTTP_ONLY | nicht gesetzt (default true) | MEDIUM — default ist sicher; explizit pinnen |
| Session | SESSION_SAME_SITE | nicht gesetzt (Laravel-default: lax) | MEDIUM — strict empfohlen |
| Session | TRUSTED_PROXIES / Middleware | nicht konfiguriert (bootstrap/app.php ohne trustProxies()) | HIGH — Laravel erkennt HTTPS-via-nginx falsch → Secure-Cookie verliert Secure-Flag |
| Session | SESSION_LIFETIME | 120 (Minuten) | OK — Operator-Empfehlung 15–30 min für Admin-Panel |
| Privilege | DB-Runtime-User | tradingbot_gui = SUPERUSER + CREATE ROLE + CREATE DB + REPLICATION + BYPASS RLS | CRITICAL — Worst-Case; kompromittierter GUI-Prozess kann komplette DB löschen (genau der INCIDENT-Vektor) |
| Privilege | Migration-User getrennt | nein — gleicher User | HIGH — INCIDENT 2026-05-12 Vektor weiterhin offen |
| Ops | Admin-User | admin@example.local | MEDIUM — Default-User, technisch+organisatorisch Risiko |
| Browser | CSP-Header | kein Content-Security-Policy gesetzt | MEDIUM — XSS-/Clickjacking-Schutz fehlt |
| Browser | X-Frame-Options | nicht gesetzt | MEDIUM |
| Browser | X-Content-Type-Options | nur für /shares/ gesetzt (nosniff) | MEDIUM — nicht für Admin-Panel-Route |
| Network | GUI-Container port | 127.0.0.1:8090 → container:8000 | OK — bereits localhost-bound |
| Network | GUI-DB port | kein Host-Mapping (intern gui-db:5432) | OK |
| Network | Docker-Netze | steve-tradingbot_clawbot-net + bridge · GUI+DB im gleichen Netz | OK — aber Segmentierung prüfen |
| Network | Bot-Dashboard 8050 | 0.0.0.0:8050 | MEDIUM — offen; sollte hinter nginx-auth oder localhost-only |
| Visibility | audit_events-Table | vorhanden (RECON-MH-Pattern) | OK — Hook-Punkte vorhanden, aber keine Alerts |
| Visibility | Telegram-Notifier | vorhanden (Notifier-1) | OK — nutzbar für Audit-Alerts |
| Hygiene | fail2ban | aktiv (sshd-jail) | OK — SEC-1b-0 closure |
| Hygiene | SSHPasswordAuthentication | SEC-1b-0: prohibit-password für root; gen sshd ungeprüft | MEDIUM — global no + AllowGroups prüfen |
| Boundaries | Bot PID (clawbot) | 290 unverändert (python3 main.py --paper) | OK |
| Boundaries | Worker PID (clawbot-worker) | 1 unverändert | OK |
| Boundaries | BINANCE_TESTNET | true | OK |
| Boundaries | cmd 13 / managed_proposals | cancelled / 0 rows | OK |
Internet
|
v
nginx :443 (Let's Encrypt cert) ----+
| |
+---> Admin GUI Filament @127.0.0.1:8090
|
+---> /shares/ static (unchanged)
|
nginx :80 ---> force-redirect :443 (außer /.well-known/acme-challenge)
trading.kw-baustoffe.de oder steve.kw-baustoffe.de) — Empfehlung: erlaubt später SEC-1d Passkey ohne Detour.certbot + --nginx-Plugin ODER --standalone ODER --webroot:
--webroot /var/www/letsencrypt + dedizierter /.well-known/acme-challenge-Block in nginx port 80. Kein nginx-Downtime nötig.certbot renew --quiet alle 12h.ssl_protocols TLSv1.2 TLSv1.3ssl_ciphers HIGH:!aNULL:!MD5 (bestehend)ssl_prefer_server_ciphers onssl_session_timeout 1d; ssl_session_cache shared:SSL:50madd_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; (HSTS, kein preload zunächst)location /admin-Block → proxy_pass http://127.0.0.1:8090 (GUI Filament)proxy_set_header X-Forwarded-Proto $scheme; + X-Forwarded-For + HostAPP_URL=https://<FQDN>.bak sichern; bei Cutover-Fehler < 60s zurückspielen.max-age. Empfehlung: max-age zunächst niedrig (z. B. 86400 = 1 Tag), nach 1 Woche auf 1 Jahr hochziehen.--staging verwenden, dann produktiv.config/filament.php asset_url ggf. setzen, oder APP_URL mit Laravel URL::forceScheme('https').SESSION_ENCRYPT=true
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=strict
SESSION_DOMAIN=<FQDN> # exakt die Domain ohne Schema
TRUSTED_PROXIES=* # in unserem nginx-localhost-Pfad sicher
In gui/bootstrap/app.php im ->withMiddleware()-Block:
$middleware->trustProxies(at: '*', headers: \Illuminate\Http\Request::HEADER_X_FORWARDED_FOR
| \Illuminate\Http\Request::HEADER_X_FORWARDED_HOST
| \Illuminate\Http\Request::HEADER_X_FORWARDED_PORT
| \Illuminate\Http\Request::HEADER_X_FORWARDED_PROTO);
Begründung: nginx läuft lokal (localhost-only Proxy), at: '*' ist akzeptabel; bei externalisiertem Reverse-Proxy müssten wir die CIDR-Liste pinnen.
SESSION_LIFETIME=30 global (alle User-Rollen).SESSION_LIFETIME=120 bleibt; Admin-Panel-spezifisch 30 via Filament-Middleware-Override (komplexer, aber Viewer-friendlier)./admin/... (z. B. Telegram-Notifier-Links) brechen — prüfen, ob aktuell solche Links existieren. lax als Fallback dokumentiert.Aktueller Zustand (live verifiziert): tradingbot_gui ist Superuser, Create role, Create DB, Replication, Bypass RLS. Das ist exakt der Vektor, der den INCIDENT 2026-05-12 möglich gemacht hat.
| Rolle | Zweck | Privs erlaubt | Privs verboten |
|---|---|---|---|
tradingbot_gui_app |
Runtime (Laravel im GUI-Container) | CONNECT auf DB; USAGE auf public-Schema; SELECT/INSERT/UPDATE/DELETE auf alle Tables; USAGE/SELECT auf alle Sequences; EXECUTE auf Functions; TEMPORARY auf DB |
kein CREATE, kein DROP, kein ALTER; kein SUPERUSER/CREATE ROLE/CREATE DB/REPLICATION |
tradingbot_gui_migrator |
Schema-Migrationen (php artisan migrate) |
CONNECT; USAGE/CREATE auf public; CREATE/ALTER/DROP TABLE/INDEX/SEQUENCE/FOREIGN KEY/CONSTRAINT; SELECT/INSERT/UPDATE/DELETE auf alle (für data-migrations) |
kein SUPERUSER/CREATE DB/CREATE ROLE/REPLICATION |
tradingbot_gui (legacy) |
Owner / DBA-Notfall (von außen SSH+psql) | SUPERUSER bleibt | aber: nie in GUI-Container .env |
psql (kein Migration, keine Schema-Änderung).GRANTs explizit setzen + ALTER DEFAULT PRIVILEGES für zukünftige Tables.tradingbot_gui (SUPERUSER); parallel psql-Connect-Tests mit tradingbot_gui_app:
CREATE TABLE x() muss fehlschlagenDROP TABLE users muss fehlschlagenALTER SYSTEM muss fehlschlagen.env in GUI-Container wechseln DB_USERNAME=tradingbot_gui_app → php artisan config:clear → GUI-Container restart (oder DB::reconnect() via Artisan, je nach Decision).scripts/migrate.sh ODER manueller Befehl DB_USERNAME=tradingbot_gui_migrator php artisan migrate ODER eigener Migration-Container.ALTER USER tradingbot_gui WITH NOSUPERUSER NOCREATEROLE NOCREATEDB — nur noch für Notfall-Zugriff.| Cutover-Method | Pro | Contra |
|---|---|---|
(a) GUI-Container restart nach .env-Edit | Sauber, eindeutiger Cut | ~10s Downtime für GUI; bestehende Sessions invalidiert (kombiniert sich mit SEC-1c-2 Re-Login — gut!) |
(b) DB::reconnect() via Artisan ohne Restart | Keine Downtime | Brittle: Filament-internes Connection-Pool bleibt evtl. auf altem User; PHP-FPM-Worker müssen alle reconnecten |
Empfehlung: (a) — klar, atomic, kombiniert mit SEC-1c-2-Cutover sinnvoll.
tradingbot_gui_app gegen alle Eloquent-Models (manuell oder via Health-Endpoint).ALTER DEFAULT PRIVILEGES für tradingbot_gui_app + tradingbot_gui_migrator setzen, sonst muss bei jeder neuen Tabelle nachgegrantet werden.steve@kw-baustoffe.de oder talk@kw-baustoffe.de)UserRole::Adminapp_authentication_secret: NULL (forced-enrollment on first login — SEC-1b-6 Default)users.is_active + Filament-Login-Pre-Check — komplexer (extra Migration + Test-Suite-Update)UPDATE users SET email='disabled-admin@local.invalid', password='', role='viewer', app_authentication_secret=NULL WHERE id=23 — minimal-invasiv, kein Schema-TouchEmpfehlung: (b) — passt zu Scope-Lock „klein/atomar“. Spalte is_active kann in BACKLOG SEC-1c-FU-1 nachgezogen werden.
| Variante | Pro | Contra |
|---|---|---|
| (i) 2 Admins parallel während Test-Phase | Lockout-Safety: alter Admin als Backup | Längerer high-risk-Zustand |
| (ii) Hard-Cutover | Schnell, klar | Wenn neuer Admin MFA-Setup verfehlt → Lockout, Break-Glass-Pfad nötig |
Empfehlung: (i) — 2 Admins parallel; alten Admin erst deaktivieren nachdem neuer Admin vollständig (Login + MFA + Recovery-Codes gesichert) verifiziert wurde.
Header in nginx-Server-Block (HTTPS) — add_header mit always-Flag:
add_header Strict-Transport-Security "max-age=86400; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Content-Security-Policy:
default-src 'self';
img-src 'self' data:;
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline';
font-src 'self' data:;
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Operator-Warnung: Filament nutzt Inline-Styles + Livewire-Inline-Scripts; strict-CSP würde die Admin-UI brechen. unsafe-inline bleibt zunächst, später nonce-basiert in BACKLOG SEC-1c-FU-2.
Content-Security-Policy-Report-Only) zuerst nutzen für 1 Woche; dann auf enforce schalten.31536000 + preload hochziehen.| Event | Quelle | Notification-Severity |
|---|---|---|
| MFA Setup completed | Filament SetUpAppAuthenticationAction-Hook | info |
| MFA disabled / regenerated | Filament Disable+Regenerate-Actions | warning |
| MFA Break-Glass (DB-direkt-UPDATE) | DB-Trigger ODER manueller Audit-Log-Eintrag | critical |
| Failed login attempt | Filament Login-throttle | info (Aggregat alle 10) |
| 3 fehlgeschlagene MFA-Codes | Filament MFA-Step-Validator | warning |
| Successful login from new IP | session create + last_login_ip-cache | warning |
| Successful login from new ASN/country | + GeoIP-Lookup (SEC-2 / BACKLOG) | warning |
| Role escalation (UPDATE users.role) | Eloquent observer ODER DB-Trigger | critical |
| Command requested by viewer (denied) | Filament canCreate/canEdit-checks | warning |
audit_events-Tabelle nutzen (RECON-MH-Pattern, schon im Einsatz).User-Model für role-changes + MFA-changes.SESSION_LIFETIME=30 (Variante A in §3) → Idle-Logout nach 30 min.SANCTUM_STATEFUL_DOMAINS — prüfen; aktuell vermutlich nicht relevant (kein API-Token-Setup), aber dokumentieren.EditProfile „Sign out from all other browsers“-Aktion aktivieren (Built-in Feature) — BACKLOG.# /etc/nginx/conf.d/limits.conf
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/m;
limit_conn_zone $binary_remote_addr zone=admin_conn:10m;
# in HTTPS server-block:
location /admin/login {
limit_req zone=admin_login burst=5 nodelay;
limit_conn admin_conn 3;
proxy_pass http://127.0.0.1:8090;
...
}
Defense-in-Depth zu Laravel-Login-Throttle (MAX_LOGIN_ATTEMPTS=3); blockt L7-Floods bevor sie Laravel erreichen.
PermitRootLogin prohibit-password aktiv (key-only für root)PasswordAuthentication no global (nicht nur für root)ChallengeResponseAuthentication noUsePAM yes (default)AllowGroups ssh-users — explicit-Whitelist auf eine Unix-Group; nur Member dürfen SSHMaxAuthTries 3LoginGraceTime 30sshd -t testen, dann reload.steve-tradingbot_clawbot-net: enthält GUI + GUI-DBclawbot-docker_clawbot-net (separates Netz)0.0.0.0:8050 public — Prüfen ob hinter nginx-Auth oder localhost-only stellengui-db:5432) — OKdocker network inspect steve-tradingbot_clawbot-net → sind GUI + DB die einzigen Member?127.0.0.1:8050:8050 in compose; nginx-proxy mit Basic-Auth davor.iptables -L + ss -tlnp auf Host — finden aller 0.0.0.0-Bindings; dokumentieren.| Secret | Risiko bei Rotation | Schritte |
|---|---|---|
APP_KEY |
HOCH — rotiert encryptet alle encrypted-cast-Spalten (MFA-Secret, Recovery-Codes, ggf. weitere) |
Eigene Sub-Phase mit APP_PREVIOUS_KEYS-Compat-Layer; pg_dump vorher; alle Admins reset MFA |
DB-Passwort DB_PASSWORD | Mittel — GUI-Container muss reconnecten | 2-Step: neuer User mit neuem PW (siehe SEC-1c-3); altes PW dann ALTER USER ... PASSWORD |
| Telegram-Bot-Token | Niedrig | Neuen Token erzeugen, alten widerrufen; Notifier testen |
| OpenAI-Key (falls genutzt) | Niedrig | Im OpenAI-Dashboard rotieren, neuen Key in .env setzen |
| SSH-Authorized-Keys | Mittel — Lockout-Gefahr | Neuen Key generieren, hinzufügen, alten nach Test-Login entfernen |
| Recovery-Codes (alle bestehenden) | Niedrig | Filament-Action „Regenerate Recovery Codes“; alte werden invalidiert |
Eigene Sub-Phase SEC-1c-7 als letztes Item. APP_KEY-Rotation ist riskant; sollte separat geplant werden mit eigener pg_dump-Vorab-Sicherung + Test-Run im Staging.
| # | Block | Items | Restart-Pflicht | Risiko | Aufwand |
|---|---|---|---|---|---|
| 1 | HTTPS + Sessions + Phase 1 Headers + B nginx-RL | SEC-1c-1 + SEC-1c-2 + SEC-1c-5 Phase 1 + B | nginx-reload + GUI-Container-Restart | HIGH (TLS-Cutover, Cookie-Cutover atomar) | 2–3h |
| 2 | DB-Least-Privilege (2-User-Modell) | SEC-1c-3 | GUI-Container-Restart | HIGH (Connection-Cutover) | 2–3h |
| 3 | Admin-Rotation | SEC-1c-4 | kein Restart | MEDIUM (Lockout-Gefahr) | 30min |
| 4 | Audit-Alerts + A Session-Lifetime | SEC-1c-6 + A | config-clear, kein Restart | LOW | 3–4h |
| 5 | Phase 2 moderate CSP | SEC-1c-5 Phase 2 | nginx-reload | MEDIUM (UI-Breakage-Risk) | 1–2h |
| 6 | D Docker-Netzwerksegmentierung | D + Bot-Dashboard localhost-bind | compose-up für Bot-Dashboard | LOW | 1–2h |
| 7 | C SSH-Härtung | C | sshd-reload | LOW (lokaler Konsolen-Fallback bleibt) | 30min |
| 8 | E Secret-Rotation (SEC-1c-7) | E | variabel | HIGH (APP_KEY) | 3–4h |
Bot/Worker bleiben in allen 8 Blöcken unberührt. BINANCE_TESTNET=true bleibt. Bot-PID 290 + Worker-PID 1 unverändert pinnen.
Gesamt-Aufwand SEC-1c: 14–22h über 8 atomic commits, jeder mit eigenem pg_dump-Backup + Safe-Runner-Tests + Live-State-Verify.
trading.kw-baustoffe.de) — Empfehlung.env-Edit — EmpfehlungDB::reconnect() ohne Restarttalk@kw-baustoffe.de oder steve@kw-baustoffe.de)bash gui/scripts/run_tests_safe.sh.BINANCE_TESTNET=false, irgendein Mainnet-Order-API-Touch..bak).APP_PREVIOUS_KEYS-Compat oder ohne pg_dump.Empfehlung: Start mit Block 1 (HTTPS + Sessions + Phase 1 Headers + nginx-RL) sobald Operator-Decisions 1, 5, 7 beantwortet sind.
Parallel kann Block 2 (DB-Least-Privilege) in Test-Phase laufen (Rollen + GRANTs anlegen, Connect-Tests mit tradingbot_gui_app) ohne Cutover — Cutover dann nach Block 1.
Block 3 (Admin-Rotation) direkt nach Block 1, weil 1) HTTPS aktiv + Cookies sicher = neues Enrollment läuft über sicheren Channel, 2) Operator hat dann frischen Admin im neuen System.
Block 8 (Secret-Rotation, vor allem APP_KEY) ist letztes Item — nicht riskieren bevor alles andere stabil läuft.
PLAN-REVIEW Plan komplett. Warte auf:
GO SEC-1c-1 + 2 + 5 Phase 1 + B Block 1 (oder anderer Start-Block), und/oderKein Code geschrieben. Kein git / docker / test-Touch durchgeführt — pure analysis + scope-pin only.