SEC-1c-3 Plan-Review — DB Least-Privilege (3-Rollen-Modell)

Projekt: Steve-TradingBot · Phase: SEC-1c-3 · Author: claude-opus-4-7[1m]
Generated: 2026-05-14 06:29 UTC · master HEAD: d351a3a
Status: NO CODE Plan-Review only — Operator-GO erforderlich vor jeder DB-Änderung.
Empfehlung: 3-Rollen-Modell (app / migrator / legacy-superuser), Split in 1c-3a (Laravel, no-Bot-Touch) + 1c-3b (Bot+Worker, mit Restart-Window)

0 — Executive Summary

Aktuelle DB-Situation (Live verifiziert 2026-05-14 06:29 UTC): tradingbot_gui ist die einzige DB-Rolle und hat SUPERUSER + Create role + Create DB + Replication + Bypass RLS. Sie wird von drei Clients gleichzeitig genutzt:

Das ist genau die strukturelle Schwachstelle, die der INCIDENT 2026-05-12 ausnutzbar gemacht hat: jeder kompromittierte App-Prozess (Laravel, Bot, Worker) kann DROP TABLE/DROP DATABASE/ALTER SYSTEM ausführen.

Ziel SEC-1c-3: Privilege-Separation in drei Rollen:

Split-Empfehlung wegen Restart-Boundary: SEC-1c-3 in 2 Sub-Phasen:


1 — Live-State (Recon-Ergebnisse)

ItemWert
master HEADd351a3a
Bot in-container PID363 (python3 main.py --paper)
Worker in-container PID1 (python3 -m trading.command_worker)
BINANCE_TESTNETtrue
DB-Rollen (postgres \du)1 Rolle: tradingbot_gui mit Attributen Superuser, Create role, Create DB, Replication, Bypass RLS
DB tradingbot_gui — Tables24 (alle Owner tradingbot_gui)
DB — Sequenzen18 (alle Owner tradingbot_gui)
DB — Functions0
DB — Views0
DB — Indexes104
DB — Foreign Keys10
Schema-Owner publicpg_database_owner (Postgres-Default)
Default-Privileges via \ddpkeine gesetzt (= GRANTs müssen für alle bestehenden + zukünftigen Tables manuell)
Laravel gui/.env DB-VarsDB_CONNECTION=pgsql DB_HOST=gui-db DB_PORT=5432 DB_DATABASE=tradingbot_gui DB_USERNAME=tradingbot_gui
Bot DSNpostgres://tradingbot_gui:…@steve-tradingbot-gui-db:5432/tradingbot_gui
Worker DSNidentisch zu Bot — tradingbot_gui-User mit SUPERUSER
aktive Connections jetztnur 1 psql-Test-Verbindung von Claude

2 — Risk-Profil: vorher vs. nachher

SzenarioHeute (SUPERUSER)SEC-1c-3 Soll (Least-Privilege)
Laravel-Process kompromittiert (XSS-RCE, dependency-injection, etc.)Kann DROP TABLE/DROP DATABASE/ALTER SYSTEMINCIDENT-VektorNur SELECT/INSERT/UPDATE/DELETE; kein Schema-Touch; kein Permission-Escalation
Bot-Process kompromittiert (Market-data-RCE, etc.)Wie oben — gleiche SUPERUSER-MachtWie oben — gleiches App-User-Limit
Worker-Process kompromittiertWie obenWie oben
Test-Code (php artisan test) ohne Safe-RunnerKann live-DB wipen (INCIDENT 2026-05-12 Präzedenz)migrate:fresh failed mit Permission-Error — strukturelle Defense
Migration laufenOK (SUPERUSER kann alles)Läuft mit separater migrator-Rolle, getrennter Pfad
DBA-Notfall-Zugriffvia SUPERUSER + psqlvia legacy tradingbot_gui + psql (unverändert)

3 — 3-Rollen-Modell (detailliert)

RolleZweckPrivs erlaubtPrivs verbotenLogin
tradingbot_gui_app Runtime (Laravel + Bot + Worker) CONNECT auf tradingbot_gui
USAGE auf public-Schema
SELECT/INSERT/UPDATE/DELETE auf ALL TABLES in public
USAGE/SELECT auf ALL SEQUENCES in public
EXECUTE auf Functions (zukunftssicher; aktuell 0 Funcs)
TEMPORARY auf DB
kein CREATE auf Schema (kann keine neuen Tabellen anlegen)
kein ALTER/DROP auf Tabellen
kein TRUNCATE (zu zerstörerisch)
kein SUPERUSER/CREATEROLE/CREATEDB/REPLICATION/BYPASSRLS
ja, mit Passwort
tradingbot_gui_migrator Schema-Migrationen (php artisan migrate) CONNECT
USAGE/CREATE auf public-Schema
SELECT/INSERT/UPDATE/DELETE/TRUNCATE/REFERENCES/TRIGGER auf ALL TABLES
ALL on ALL SEQUENCES
TEMPORARY
kein SUPERUSER/CREATEROLE/CREATEDB/REPLICATION
keine Berechtigung andere DBs zu ändern
ja, mit eigenem Passwort (NICHT im Runtime-Container .env)
tradingbot_gui (legacy) DBA-Notfall — nur via SSH+psql vom Host SUPERUSER bleibt (unverändert) ja, Passwort bleibt

GRANT-Sequenz (Plan only)

-- AUSFÜHRUNG via `tradingbot_gui` (SUPERUSER) am Host:
-- docker exec -i steve-tradingbot-gui-db psql -U tradingbot_gui -d tradingbot_gui << SQL

-- Variant: Passwoerter werden NIEMALS in diesem SQL-Snippet stehen. Generation:
--    PW_APP=$(openssl rand -base64 24)
--    PW_MIG=$(openssl rand -base64 24)
-- via heredoc + WITH PASSWORD '$PW_APP' — Heredoc-Body wird NICHT geechoed.

-- 1. Rollen anlegen
CREATE ROLE tradingbot_gui_app WITH LOGIN PASSWORD <PW_APP> NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;
CREATE ROLE tradingbot_gui_migrator WITH LOGIN PASSWORD <PW_MIG> NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION;

-- 2. Grants fuer App-User (ZUERST, sicher fuer existing tables)
GRANT CONNECT ON DATABASE tradingbot_gui TO tradingbot_gui_app;
GRANT TEMPORARY ON DATABASE tradingbot_gui TO tradingbot_gui_app;
GRANT USAGE ON SCHEMA public TO tradingbot_gui_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO tradingbot_gui_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO tradingbot_gui_app;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO tradingbot_gui_app;

-- 3. Default-Privileges fuer ZUKUENFTIGE Tabellen (= nach Migrationen automatisch)
ALTER DEFAULT PRIVILEGES FOR ROLE tradingbot_gui_migrator IN SCHEMA public
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO tradingbot_gui_app;
ALTER DEFAULT PRIVILEGES FOR ROLE tradingbot_gui_migrator IN SCHEMA public
    GRANT USAGE, SELECT ON SEQUENCES TO tradingbot_gui_app;
ALTER DEFAULT PRIVILEGES FOR ROLE tradingbot_gui_migrator IN SCHEMA public
    GRANT EXECUTE ON FUNCTIONS TO tradingbot_gui_app;

-- 4. Grants fuer Migrator-User
GRANT CONNECT ON DATABASE tradingbot_gui TO tradingbot_gui_migrator;
GRANT TEMPORARY ON DATABASE tradingbot_gui TO tradingbot_gui_migrator;
GRANT USAGE, CREATE ON SCHEMA public TO tradingbot_gui_migrator;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO tradingbot_gui_migrator;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO tradingbot_gui_migrator;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO tradingbot_gui_migrator;

-- 5. Migrator MUSS Owner werden fuer existing tables damit er ALTER kann
--    (alternativ: GRANT REASSIGN OWNED BY tradingbot_gui TO tradingbot_gui_migrator)
--    Aber das macht den Migrator zum De-facto-Owner und nimmt SUPERUSER raus.
--    Pragmatischer: SUPERUSER bleibt Owner; Migrator kriegt ALL GRANTS.
--    Migration via Laravel testet sich vor Cutover (mit migrate --pretend).

-- 6. SUPERUSER NICHT vom Owner zurückziehen (sonst wird REASSIGN bei naechster
--    Migration nötig); SUPERUSER bleibt Owner aller 24 Tabellen + 18 Sequenzen.
--    Defense-in-Depth: SUPERUSER nicht mehr in Container.env

4 — Cutover-Strategy & Sub-Phase-Split

SEC-1c-3a — Laravel-Cutover (sicher, no-Bot-Touch)

#StepRisikoZeit
3a.0Pre-flight + git/PID/TESTNET/managed_proposals-Snapshot02 min
3a.1pg_dump-Backup (Pflicht durable rule)03 min
3a.2Generate 2 starke Random-PWs in /root/.secrets/sec-1c-3a-{app,migrator}.SECRET-READ-ONCE mode 60001 min
3a.3Rollen + GRANTs anlegen via Heredoc-SQL (PWs werden NICHT geechoed)LOW3 min
3a.4Verify-Tests: Connect mit tradingbot_gui_appSELECT erlaubt, CREATE TABLE x() muß fehlschlagen, DROP TABLE users muß fehlschlagen, ALTER SYSTEM muß fehlschlagenLOW5 min
3a.5Verify-Tests: Connect mit tradingbot_gui_migratorCREATE TABLE foo() + DROP TABLE foo + ALTER TABLE users ADD COLUMN ... alle erlaubt; CREATE ROLE muß fehlschlagenLOW5 min
3a.6Backup gui/.env01 min
3a.7gui/.env ändern: DB_USERNAME=tradingbot_gui_app + DB_PASSWORD=<app-PW>MEDIUM (Wert nur in .env, niemals im Output)2 min
3a.8Cache invalidation: config:clearLOW1 min
3a.9GUI-Container restart — ~10s Downtime; alle aktiven Filament-Sessions invalidiert (akzeptabel, Operator wartet)HIGH (Operator-Recovery beobachten)2 min
3a.10Smoke-Test: https://gui.*/admin/login → 200; Filament-Login (steve@gewerbespeicher-rechner.de) klappt; eine simple Read-Page (DecisionLogs) lädtHIGH (Operator)5 min
3a.11Migration-Script-Test: DB_USERNAME=tradingbot_gui_migrator php artisan migrate --pretend — bestaetigt Pfad funktioniert ohne wirklich migrierenLOW2 min
3a.12Defense-in-Depth: ALTER USER tradingbot_gui WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION? → DEFERRED zu 1c-3b/1c-3c (Operator-Pin nötig)OPTIONAL1 min
3a.13Closure-Pin + STOP-Report03 min

Σ SEC-1c-3a: ~35 min, davon ~10s GUI-Container-Restart.

SEC-1c-3b — Bot+Worker-Cutover (eigene Phase, Operator-GO erforderlich)

#StepRisikoZeit
3b.0Pre-flight + Operator-Notification: Bot/Worker-Restart-Window nötig02 min
3b.1Bot-Container .env: GUI_DB_USER=tradingbot_gui_app + GUI_DB_PASSWORD=<app-PW> + DSN entsprechendMEDIUM5 min
3b.2Worker-Container .env: gleiche ÄnderungenMEDIUM5 min
3b.3Bot-Container Restart — Bot PID 363 wird neu — Mainnet-Block & Bot-Verhalten verifizierenHIGH (Operator-Approve für Restart-Window)10 min
3b.4Worker-Container RestartHIGH5 min
3b.5Connection-Tests: Bot können Read+Write zu commands/managed_proposals usw.HIGH5 min
3b.6Worker-Heartbeat-VerifikationHIGH5 min
3b.7Closure-Pin03 min

Σ SEC-1c-3b: ~40 min, mit explizitem Bot/Worker-Restart-Window.

SEC-1c-3c (BACKLOG) — SUPERUSER-Lock-Down

Nach erfolgreichem 1c-3a + 1c-3b: ALTER USER tradingbot_gui WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION. SUPERUSER bleibt nur via psql-Direkt-Reconnect-mit-PW-Reset wieder aktivierbar. Mehr Defense, weniger DBA-Convenience — Operator-Decision.


5 — Test-Plan (vor Cutover)

Connect-Test (NACH GRANT, VOR Cutover)

# positive case (sollte alle gehen):
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "SELECT COUNT(*) FROM users;"
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "INSERT INTO audit_events (event, occurred_at) VALUES ('sec-1c-3a-test', NOW()); SELECT LASTVAL();"
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "DELETE FROM audit_events WHERE event='sec-1c-3a-test';"

# negative case (MUSS alle fehlschlagen mit permission denied):
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "CREATE TABLE leak_test (id int);"
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "DROP TABLE users;"
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "ALTER TABLE users ADD COLUMN leak text;"
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "TRUNCATE TABLE users;"
PGPASSWORD=<app-PW> psql -U tradingbot_gui_app -h gui-db -d tradingbot_gui -c "CREATE ROLE evil;"

# Migrator-Test (sollte alle gehen):
PGPASSWORD=<mig-PW> psql -U tradingbot_gui_migrator -h gui-db -d tradingbot_gui -c "CREATE TABLE _sec_1c_3_canary (id int); DROP TABLE _sec_1c_3_canary;"

Laravel-Tests via Safe-Runner (NACH 1c-3a cutover)

bash gui/scripts/run_tests_safe.sh
# Erwartet: 856/856 ✅ wie zuvor.
# Safe-Runner nutzt SQLite in-memory — DB-Wechsel beeinflusst Tests nicht.

6 — Operator-Decision-Points (vor GO 1c-3a)

  1. Sub-Phase-Split: 1c-3a (Laravel-only, kein Bot/Worker-Touch) jetzt, 1c-3b später? Empfehlung: JA, weil Bot/Worker-Restart-Window bewusst getrennt.
  2. Migrator-Owner-Strategie: SUPERUSER bleibt Owner; Migrator hat ALL PRIVILEGES — OK? (Empfehlung: ja, vermeidet REASSIGN-Komplexität)
  3. SEC-1c-3c (SUPERUSER lock-down): jetzt mit-planen oder als BACKLOG für später? Empfehlung: BACKLOG bis 1c-3a/1c-3b live + 1 Woche stabil.
  4. PW-Speicherort: /root/.secrets/<phase>.SECRET-READ-ONCE mode 600 + Operator-Shred nach PW-Manager-Import. Standard-Pattern, OK?
  5. Migration-CLI-Vereinfachung: ein Wrapper-Script /projekte/Steve-TradingBot/gui/scripts/run_migrate_safe.sh der automatisch zwischen App-User (Runtime) und Migrator-User (Migrations) wechselt? Empfehlung: ja, BACKLOG nach 1c-3a.
  6. Filament-Tinker-Zugriff: nutzt php artisan tinker die App-User-Connection oder soll es eine separate Read-Only-Rolle dafuer geben? Empfehlung: bleibt App-User für jetzt; Read-Only-Rolle Optional BACKLOG.

7 — Stop-Regeln


8 — Rollback-Pfade

Failure-ZeitpunktRollback
nach 3a.3 (Rollen + GRANTs angelegt, App-Test failed)DROP ROLE tradingbot_gui_app; DROP ROLE tradingbot_gui_migrator; — sauber, kein .env-Touch
nach 3a.7 (.env geändert, Container noch nicht restartet)backup-.env zurück + config:clear
nach 3a.9 (Container restarted, GUI broken)backup-.env zurück + docker restart steve-tradingbot-gui
komplettes RollbackDROP ROLEs + .env-Restore + Container-Restart — < 5 min

9 — NO-GO-Bedingungen


10 — Boundaries (Plan-Review-End)

master HEADd351a3a unverändert
Bot in-container PID363 unverändert
Worker in-container PID1 unverändert
BINANCE_TESTNETtrue
managed_proposals0
DB-Rollen-Change0 (geplant in 1c-3a)
.env-Change0
Container-Restart0
Mainnet0
Push0
Secret-Werte im PDF / Output0 (durable Hygiene-Regel)

11 — Phasen-Einordnung & Empfehlung

PhaseStatus
SEC-1c-0✅ closed (ufw + nginx-Basic-Auth + netdata/cups + fail2ban-nginx)
SEC-1c-1✅ closed (HTTPS + multi-SAN-Cert + Session-Hardening + Permissions-Policy + CSP-Report-Only)
SEC-1c-2a✅ closed (HSTS 1d→1w + SESSION_LIFETIME=30)
SEC-1c-2b⏸ DEFERRED bis 2026-05-21 (CSP enforce + HSTS staged-bump weiter)
SEC-1c-3a🆕 dieses Plan-Review (Laravel-Cutover, kein Bot/Worker-Touch)
SEC-1c-3bgeplant nach SEC-1c-3a (Bot+Worker-Cutover, Restart-Window)
SEC-1c-3c (BACKLOG)SUPERUSER lock-down nach 1c-3a/1c-3b stabil
SEC-1c-4Admin-Rotation (Email schon zu steve@…, Cleanup pending)
SEC-1c-5Audit-Alerts
SEC-1c-6SSH-Finetuning
SEC-1dPasskey/WebAuthn
SEC-1eFinal Secret-Rotation (Tokens + APP_KEY + DB-PWs)

Empfehlung

GO SEC-1c-3a atomic Block (35 min, kein Bot/Worker-Touch). Danach STOP und Operator-Verifikation auf gui.gewerbespeicher-rechner.de bevor Operator-GO für SEC-1c-3b ausgesprochen wird.

Status

PLAN-REVIEW Plan fertig. Warte auf:

Kein Code geschrieben. Keine DB-Rollen-Änderung. Keine .env-Änderung. Pure analysis only.