diff --git a/README.md b/README.md index 64a62e9..910d3f3 100755 --- a/README.md +++ b/README.md @@ -45,6 +45,14 @@ werden beim ersten Login transparent auf Argon2id umgestellt. - DSGVO-Retention: automatische Bereinigung von Tickets/Audit-Log gemaess Mandanten-Fristen; Login-Versuche nach 7 Tagen, abgelaufene Sessions sofort. +## Dokumentation + +- `docs/ARCHITEKTUR.md` — technische Architektur (Module, Datenmodell, Request-Fluss, RBAC-Matrix, ITIL-Abbildung) +- `docs/BETRIEB.md` — Betriebshandbuch (ENV, Deployment/Update, Backup, Troubleshooting) +- `docs/adr/` — Architecture Decision Records (ADR-001 bis ADR-008) +- Prozess-Doku nach AES-Regelwerk (Projektbeschreibung, Phasen-/Forensik-Reports, Tasks): + `mscadm/AES`, Branch `dev`, Verzeichnis `projects/itsm/` + ## Betrieb # Entwicklung diff --git a/docs/ARCHITEKTUR.md b/docs/ARCHITEKTUR.md new file mode 100755 index 0000000..c509d01 --- /dev/null +++ b/docs/ARCHITEKTUR.md @@ -0,0 +1,104 @@ +# ITSM — Technische Architektur + +Stand: 2026-07-15 (Rust-Rewrite, Version 1.0). Verbindliche Entscheidungen: siehe `docs/adr/`. + +## 1. Überblick + +Mandantenfähige ITSM-Plattform (Tickets, Service-Katalog, Wissensdatenbank, CMDB) +als einzelnes Rust-Binary mit PostgreSQL. AES ist ein buchbarer Service im +Katalog (HTTP-Integration, kein Code-Import). Repo-Bearbeitung aus Tickets +läuft über die Gitea-kompatible Forge-Contents-API. + + Browser ──HTTP──► itsm (axum, Port 8090) ──► PostgreSQL 16 + │ + ├──► AES-Dashboard (Erreichbarkeits-Check, Katalog) + └──► Forge-API (Repo-Edit aus Change-Tickets) + +## 2. Modulstruktur (src/) + +| Modul | Verantwortung | +|---|---| +| `main.rs` | App-Aufbau, Routing, Retention-Hintergrundtask | +| `config.rs` | ENV-Konfiguration + Startvalidierung | +| `web.rs` | AppState, Middleware (Session, CSRF, Client-IP, Security-Header), Fehlertyp, RBAC-Helfer, PageCtx | +| `db.rs` | Datenzugriff (deadpool/tokio-postgres), alle Queries parametrisiert und tenant-gescoped | +| `security.rs` | Passwort-Hashing/-Verifikation (Argon2id + Werkzeug pbkdf2/scrypt), Token, Policy, SSRF-Guard | +| `itil.rs` | ITIL-Logik: Rollen, Statusmodell, Prioritätsmatrix, Change-Gate (reine Funktionen, unit-getestet) | +| `auth.rs` | Login (Rate-Limit, Rehash-Migration), Logout, Ersteinrichtung | +| `tickets.rs` | Listen/KPIs/Sparklines, Ticket-API, Statuswechsel, Change-Freigabe, Problem/CI-Links, Repo-Edit, Kanban | +| `services.rs` | Service-Katalog, paralleler Erreichbarkeits-Check (30 s Cache) | +| `kb.rs` | Wissensdatenbank inkl. Freigabe-Workflow | +| `cmdb.rs` | Configuration Items + Beziehungen | +| `admin.rs` | Mandanten-Einstellungen, Audit-Ansicht, Benutzerverwaltung | +| `dashboard.rs` | CSI-Kennzahlen (SLA-Erfüllung, MTTR, Verteilungen) | +| `forge.rs` | Forge-Contents-Client (get/update, Sha-Locking) | + +Frontend: Askama-Templates (`templates/`), ein statisches CSS + ein JS +(`static/`), kein Framework, kein Inline-JS (CSP). + +## 3. Request-Fluss + +1. `ctx_middleware` (web.rs): Session-Cookie → SHA-256 → DB-Lookup (`sessions` + JOIN `users`/`tenants`) → `AuthUser` in Request-Extensions. Client-IP nur + aus X-Forwarded-For, wenn `ITSM_TRUSTED_PROXY_COUNT > 0`. +2. CSRF: jede zustandsändernde Methode braucht Token aus Session (Formular- + Feld `_csrf` oder Header `X-CSRF-Token`); Ausnahmen: `/login`, `/setup/new`. +3. Handler: RBAC-Check (`need_auth`/`need_operative`/`need_admin`/ + `need_change_approver`) → Fachlogik → Audit-Log-Eintrag. +4. Antwort: Security-Header (CSP, nosniff, X-Frame-Options DENY, Referrer- + Policy, HSTS bei `ITSM_HTTPS=1`). + +## 4. Datenmodell (PostgreSQL, schema.sql) + +Alle fachlichen Tabellen tragen `tenant_id`; jede Query filtert darauf. + +- `tenants` — Mandanten inkl. SLA-Defaults + DSGVO-Retention-Fristen +- `users` — global eindeutige E-Mail (Login löst Mandant auf), `role` + (admin/change_manager/agent/user), `auth_source` ('local', 'ldap' reserviert) +- `tickets` — inkl. ITIL-Feldern: `impact`/`urgency` (Priorität via Matrix), + `change_typ`/`approval_status`/`approved_by/-_at` (Change Enablement), + `problem_id`/`known_error` (Problem Mgmt), `first_response_at`/`resolved_at`/ + `closed_at` (SLA-Zeitstempel) +- `ticket_timeline` — Worklog (Status, Kommentare, Repo-Edits: dokumentationspflichtig) +- `ticket_ci_links` — Ticket↔CI (SACM) +- `services`, `kb_articles` (mit `status` Entwurf/Freigegeben), + `configuration_items` + `ci_relationships` +- `audit_log` — alle sicherheitsrelevanten Aktionen (ISO 27001 A.12.4 / DSGVO Art. 30) +- `login_attempts` — Rate-Limit-Basis (7 Tage aufbewahrt) +- `sessions` — serverseitige Sessions, nur SHA-256-Hash des Cookie-Tokens + +Migrationen: idempotenter Block am Ende von `schema.sql`, wird bei jedem +App-Start per `include_str!` ausgeführt (ADR-005). + +## 5. RBAC-Matrix + +| Aktion | admin | change_manager | agent | user | +|---|---|---|---|---| +| Eigene Tickets (Incident/Service Request) anlegen/sehen | ✓ | ✓ | ✓ | ✓ | +| Alle Tickets sehen/bearbeiten, Status wechseln | ✓ | ✓ | ✓ | – | +| Probleme/Änderungen/Releases/Kanban/CMDB | ✓ | ✓ | ✓ | – | +| KB lesen | alle | alle | alle | nur Freigegeben | +| KB anlegen/bearbeiten | ✓ | ✓ | ✓ | – | +| KB freigeben, Change-Freigabe (CAB) | ✓ | ✓ | – | – | +| Repo-Edit aus Ticket (nur umsetzbarer Change) | ✓ | ✓ | – | – | +| Services anlegen, Administration, Audit-Log | ✓ | – | – | – | + +## 6. ITIL-Abbildung (v3-Prozesse / v4-Practices) + +- Statusmodell: Offen → In Bearbeitung → Warten → Gelöst → Geschlossen; + Reopen Gelöst→Offen; Geschlossen final. Übergänge serverseitig erzwungen. +- Priorität = Matrix(Impact × Urgency), 3×3 nach v3 SO 4.2.5.4. +- Change Enablement: Standard (vorautorisiert) / Normal (CAB) / Emergency + (sofort, nachträgliche ECAB-Freigabe). Umsetzung + Repo-Edit erst wenn + `itil::change_may_be_implemented`. +- Problem Management: Incident↔Problem-Verknüpfung, Known-Error-Flag. +- SLA: Fristen aus Mandanten-Defaults, Zeitstempel bei Statuswechseln, + Überfälligkeits- und Restzeit-Berechnung serverseitig. +- Continual Improvement: /dashboard (SLA-Erfüllung 30 T, MTTR, Verteilungen). + +## 7. Sicherheitsarchitektur + +Siehe ADR-002/003/007/008 und README-Abschnitt "Sicherheit". Kurzfassung: +serverseitige widerrufbare Sessions, CSRF überall, DB-gestütztes Login-Rate- +Limit (E-Mail+IP), Argon2id mit transparenter Migration von Werkzeug-pbkdf2 +UND -scrypt (ADR-003!), SSRF-Guard, CSP ohne Inline-JS, Audit-Log. diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md new file mode 100755 index 0000000..9c1be9d --- /dev/null +++ b/docs/BETRIEB.md @@ -0,0 +1,63 @@ +# ITSM — Betriebshandbuch + +Stand: 2026-07-15. Produktivinstanz: SRV1361746 (`/docker/itsm/`), Port 8090. + +## 1. ENV-Variablen + +| Variable | Pflicht | Bedeutung | +|---|---|---| +| `DATABASE_URL` | ja | `postgresql://itsm:...@postgres:5432/itsm` | +| `ITSM_BIND` | – | Bind-Adresse (Default `0.0.0.0:8090`) | +| `ITSM_HTTPS` | – | `1` = Secure-Cookies + HSTS (hinter TLS-Terminierung setzen) | +| `ITSM_TRUSTED_PROXY_COUNT` | – | Anzahl vertrauenswürdiger Reverse-Proxies; nur dann zählt X-Forwarded-For | +| `ITSM_SESSION_HOURS` | – | Session-Lebensdauer (Default 8) | +| `ITSM_LOGIN_MAX_ATTEMPTS` / `ITSM_LOGIN_WINDOW_MINUTES` | – | Rate-Limit (Default 5 / 15) | +| `ITSM_PASSWORD_MIN_LENGTH` | – | Default 12 | +| `AES_DASHBOARD_URL` | – | AES-Service im Katalog | +| `FORGE_BASE_URL` / `FORGE_SERVICE_TOKEN` | – | Repo-Edit aus Change-Tickets; ohne = Feature deaktiviert | +| `RUST_LOG` | – | z. B. `info` | + +Hinweis: `ITSM_SECRET_KEY` (Flask-Ära) wird nicht mehr genutzt und kann aus +der Server-`.env` entfernt werden. + +## 2. Deployment / Update (SRV1361746) + + cd /docker/itsm + rm -rf src && git clone --depth 1 https://git1.mrmoe.de/mscadm/ITSM.git src + docker compose build itsm # Multi-Stage-Build, ~3 min + docker compose up -d # Postgres läuft weiter, nur App wird getauscht + curl -s localhost:8090/health # {"db":"ok","status":"ok"} + +Es wird KEIN Code mehr beim Container-Start gezogen (früherer bootstrap.sh- +Mechanismus ist abgeschafft) — ein Neustart startet exakt das gebaute Image. +Schema-Migrationen laufen idempotent beim App-Start. + +Rollback: `docker-compose.yml.flask-backup` liegt als letzter Flask-Stand in +`/docker/itsm/`; für das Rust-Image genügt `git clone` eines älteren Tags/ +Commits nach `src/` + erneutes `build`. + +## 3. Backup & Retention + +- Täglicher pg_dump: `deploy/backup.sh` via Cron `0 3 * * *` nach + `/docker/itsm/backups/` (14 Tage Rotation). Live-Daten: Bind-Mount + `/docker/itsm/pgdata` (kein anonymes Docker-Volume). +- DSGVO-Retention: Hintergrundtask löscht gelöste/geschlossene Tickets und + Audit-Einträge nach Mandanten-Fristen; Sessions/Login-Versuche werden + mitbereinigt. Manuell: Admin → Einstellungen → "Jetzt manuell bereinigen". + +## 4. Troubleshooting + +| Symptom | Ursache / Abhilfe | +|---|---| +| Login schlägt für Bestandskonto fehl | Hash-Prefix prüfen: `docker exec itsm_postgres psql -U itsm -d itsm -c "SELECT email,left(password_hash,16) FROM users;"` — `pbkdf2:`/`scrypt:` = Werkzeug-Altbestand (wird unterstützt, ab Commit f732392 auch scrypt), `$argon2id$` = migriert. | +| "Zu viele Fehlversuche" (429) | Rate-Limit-Sperre; wartet 15 min oder `DELETE FROM login_attempts;` | +| 500er | `docker logs itsm_app --tail 50` (tracing-Log) | +| Health rot | DB-Verbindung prüfen (`docker ps`, `pg_isready`) | +| Repo-Edit-Button fehlt | FORGE_BASE_URL nicht gesetzt, Rolle < change_manager, oder Ticket ist kein freigegebener Change (gewollt, ADR-004) | +| Alle Sessions ungültig nach Deploy | erwartungsgemäß NICHT der Fall (Sessions liegen in der DB); falls doch: sessions-Tabelle prüfen | + +## 5. Monitoring-Punkte + +- `GET /health` (Docker-HEALTHCHECK eingebaut, curl-basiert) +- Audit-Log (Admin-UI) für login_failed/login_rate_limited-Häufungen +- Backup-Log `/docker/itsm/backup.log` diff --git a/docs/adr/ADR-001-rust-axum-rewrite.md b/docs/adr/ADR-001-rust-axum-rewrite.md new file mode 100755 index 0000000..5e43157 --- /dev/null +++ b/docs/adr/ADR-001-rust-axum-rewrite.md @@ -0,0 +1,23 @@ +# ADR-001: Rust/axum statt Python/Flask (In-Place-Rewrite) + +**Datum:** 2026-07-15 · **Status:** Akzeptiert · **Entscheider:** Moe (Vorgabe), umgesetzt via Claude + +## Kontext +Die Flask-Version (single-file, ungepinnte Dependencies) hatte 8 Sicherheits- +Review-Befunde; Nutzer-Vorgabe: alle Schwachstellen beseitigen, technisch +aktuellster Stand, ITIL v3/v4, Umstellung auf Rust. Es existierte ein +Parallelbau-Plan (AES projects/itsm-rust) mit späterem Cutover. + +## Entscheidung +Kompletter In-Place-Rewrite in Rust mit axum 0.7 + tokio-postgres/deadpool + +askama — gleicher Stack wie forge-web (einheitliche Technologierichtung). +Der Parallelbau-Plan wurde auf explizite Nutzer-Anweisung übersprungen. + +## Konsequenzen ++ Ein Binary, kompilierte Templates, keine Laufzeit-Dependency-Drift mehr. ++ Stack-Konsistenz mit Forge (Wartbarkeit, Wissenstransfer). +− Kein paralleles Rollback-System; kompensiert durch: identisches Schema + (nur additiv migriert), Werkzeug-Hash-Kompatibilität (ADR-003), E2E-Tests + vor Push, Compose-Backup als Rollback-Punkt. +− Lehre aus Produktion (Forensik phase-010 B4): Kompatibilitätsannahmen + gegen ECHTE Produktionsdaten testen, nicht nur gegen eigene Vektoren. diff --git a/docs/adr/ADR-002-serverseitige-sessions.md b/docs/adr/ADR-002-serverseitige-sessions.md new file mode 100755 index 0000000..87e1692 --- /dev/null +++ b/docs/adr/ADR-002-serverseitige-sessions.md @@ -0,0 +1,20 @@ +# ADR-002: Serverseitige Sessions in PostgreSQL + +**Datum:** 2026-07-15 · **Status:** Akzeptiert + +## Kontext +Flask nutzte Client-Side-Sessions (signierte Cookies) mit optionalem Secret; +ohne gesetztes Secret wurde pro Prozessstart ein Zufallswert erzeugt +(Sessions bei Neustart weg, mehrere Worker inkompatibel). Sessions waren +nicht widerrufbar. + +## Entscheidung +Sessions liegen in der DB (`sessions`-Tabelle). Cookie enthält nur ein +256-Bit-Zufallstoken; die DB speichert dessen SHA-256. CSRF-Token wird pro +Session serverseitig gehalten. + +## Konsequenzen ++ Widerrufbar (Logout, Kontosperrung löscht Sessions), neustart- und + multi-instanz-fest, kein Secret-Management (ITSM_SECRET_KEY entfällt). ++ DB-Leak kompromittiert keine laufenden Sessions (nur Hashes). +− Ein DB-Roundtrip pro Request (Middleware); bei aktueller Last irrelevant. diff --git a/docs/adr/ADR-003-passwort-hashing-migration.md b/docs/adr/ADR-003-passwort-hashing-migration.md new file mode 100755 index 0000000..7018d81 --- /dev/null +++ b/docs/adr/ADR-003-passwort-hashing-migration.md @@ -0,0 +1,23 @@ +# ADR-003: Argon2id + Werkzeug-Kompatibilitätsschicht (pbkdf2 UND scrypt) + +**Datum:** 2026-07-15 · **Status:** Akzeptiert (ergänzt um scrypt nach Prod-Befund B4) + +## Kontext +Bestandskonten wurden von Werkzeug (Flask) gehasht. Zwangs-Passwort-Reset +für den Produktiv-Mandanten war inakzeptabel. Werkzeug erzeugt je nach +Version pbkdf2:sha256- ODER scrypt-Hashes (>= 3.0 Default: scrypt) — die +Flask-Ära installierte ungepinnt, in Produktion lagen scrypt-Hashes. + +## Entscheidung +Neue Hashes: Argon2id (RustCrypto-Defaults). verify_password() erkennt +Werkzeug-Formate (`pbkdf2:sha256:*` und `scrypt:n:r:p$salt$hex`), verifiziert +in Konstantzeit und rehasht beim ersten erfolgreichen Login transparent auf +Argon2id (schleichende Migration, kein Reset). + +## Konsequenzen ++ Bestandslogins funktionieren unverändert; Bestand härtet sich selbst nach. +− Werkzeug-Kompatibilitätscode bleibt, bis alle Konten migriert sind + (prüfbar: `SELECT count(*) FROM users WHERE password_hash NOT LIKE '$argon2%'`). +− Lehre: Der erste Wurf unterstützte nur pbkdf2 und fiel in Produktion auf + (Login-Ausfall + Rate-Limit-Sperre). Hash-Kompatibilität ist künftig gegen + reale Prod-Hash-Prefixe zu verifizieren (Forensik phase-010, Befund B4). diff --git a/docs/adr/ADR-004-itil-rollen-change-gate.md b/docs/adr/ADR-004-itil-rollen-change-gate.md new file mode 100755 index 0000000..8c4fd68 --- /dev/null +++ b/docs/adr/ADR-004-itil-rollen-change-gate.md @@ -0,0 +1,23 @@ +# ADR-004: ITIL-Rollenmodell + Change-Gate für Repo-Edits + +**Datum:** 2026-07-15 · **Status:** Akzeptiert · **Vorgabe:** "Rollen gemäß ITIL" + +## Kontext +Vorher nur admin/agent; Repo-Edit aus Tickets stand JEDEM eingeloggten +Nutzer offen (mit gemeinsamem Forge-Admin-Token) — Review-Befund. + +## Entscheidung +Vier Rollen: admin (Service Owner), change_manager (Change Enablement/CAB), +agent (Service Desk), user (Requester/Self-Service: eigene Tickets, nur +freigegebene KB). Repo-Edits gelten als Change-Implementierung: erlaubt nur +für admin/change_manager UND nur aus einem umsetzbaren Change-Ticket +(Standard = vorautorisiert; Normal = nach CAB-Genehmigung; Emergency = +sofort mit nachträglicher ECAB-Freigabe; Abgelehnt = nie). Jede Repo- +Änderung bleibt worklog-pflichtig (phase-008-Garantie unverändert). + +## Konsequenzen ++ Least-Privilege; auditierbarer Change-Pfad für Produktions-Repos. +− Agents müssen für Repo-Änderungen einen Change anlegen und freigeben + lassen — gewollte Prozesshürde. +− Feingranulare Forge-Repo-Rechte (statt Admin-Token) bleiben offen, bis + Forge Repo-Scopes kennt (Backlog). diff --git a/docs/adr/ADR-005-idempotente-migrationen.md b/docs/adr/ADR-005-idempotente-migrationen.md new file mode 100755 index 0000000..a41c8a2 --- /dev/null +++ b/docs/adr/ADR-005-idempotente-migrationen.md @@ -0,0 +1,19 @@ +# ADR-005: Idempotente Schema-Migrationen statt Migrationstool + +**Datum:** 2026-07-15 · **Status:** Akzeptiert + +## Kontext +Bestehende Prod-DB (Flask-Ära) musste ohne Migrationsschritt weiterverwendet +werden; das Projekt hat genau eine schema.sql-Historie und wenige Releases. + +## Entscheidung +`schema.sql` bleibt die einzige Quelle: CREATE TABLE IF NOT EXISTS + +ADD COLUMN IF NOT EXISTS + einmalige, selbstneutralisierende UPDATEs. +Wird beim App-Start per `include_str!` komplett ausgeführt (batch_execute). + +## Konsequenzen ++ Kein Migrationstool, keine Versionstabelle, Deploy = Start. ++ Kompilierzeit-Einbettung: Binary und Schema sind untrennbar konsistent. +− Destruktive Änderungen (DROP/RENAME) brauchen künftig explizite Sorgfalt + oder den Wechsel auf ein echtes Migrationstool — akzeptiert für die + aktuelle Projektgröße. Additiv-only ist Konvention. diff --git a/docs/adr/ADR-006-multistage-build.md b/docs/adr/ADR-006-multistage-build.md new file mode 100755 index 0000000..f8bc25e --- /dev/null +++ b/docs/adr/ADR-006-multistage-build.md @@ -0,0 +1,20 @@ +# ADR-006: Multi-Stage-Docker-Build statt Clone-on-Start + +**Datum:** 2026-07-15 · **Status:** Akzeptiert + +## Kontext +Der frühere bootstrap.sh klonte bei JEDEM Container-Start main und +installierte ungepinnte pip-Pakete: unreproduzierbar, ungetesteter Stand +konnte durch bloßen Neustart live gehen; forge_client.py wurde dabei sogar +vergessen (Feature in Prod defekt). + +## Entscheidung +Multi-Stage-Dockerfile (rust:1-slim-bookworm → debian:bookworm-slim, +USER nobody, HEALTHCHECK). deploy/docker-compose.yml baut per `build:` aus +dem geklonten Quellstand; Neustart startet exakt das gebaute Image. + +## Konsequenzen ++ Reproduzierbare, testbare Deployments; Rollback = altes Image/älterer Commit. +− Update erfordert bewussten Build (~3 min auf SRV1361746) — gewollt. +− postgres:16-alpine wird beibehalten (pgdata-Kompatibilität); Major-Upgrade + wäre ein eigener Change mit pg_upgrade/dump+restore. diff --git a/docs/adr/ADR-007-csp-ohne-inline-js.md b/docs/adr/ADR-007-csp-ohne-inline-js.md new file mode 100755 index 0000000..7312a9e --- /dev/null +++ b/docs/adr/ADR-007-csp-ohne-inline-js.md @@ -0,0 +1,20 @@ +# ADR-007: CSP ohne Inline-JavaScript + +**Datum:** 2026-07-15 · **Status:** Akzeptiert + +## Kontext +Die Flask-Version renderte onclick-Handler und