Files
tessera-ctl/.planning/todos/pending/2026-08-11-ldap-bindpasswort-unverschluesselt.md
T
schalli 6fb0276d1a
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 45s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
docs: add two backlog items found during Phase 16 testing
Neither is a Phase 16 defect; both surfaced while testing it and would
otherwise have been lost with the session.

1. Module activation has no licence check — a tenant admin can activate any
   catalogue module for their own tenant. The grants matrix is NOT the hole: it
   only distributes what is already active. Carries open product questions
   (who issues licences, what expiry does), so it is written up as a draft, not
   a decision.

2. The LDAP bind password is stored in clear text although an AES-256-GCM
   service already exists and is used for calendar, DKV and tender inbox
   credentials. Hashing is not an option here — the password must be replayable
   to bind against the directory — so encryption at rest is the fix. No open
   questions, just work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:29:08 +02:00

2.7 KiB

created, title, area, severity, trigger, files
created title area severity trigger files
2026-08-11 LDAP-Bind-Passwort liegt im Klartext in der DB, obwohl der Verschluesselungsdienst schon existiert ldap major vor dem Verkauf an externe Kunden; unabhaengig davon jederzeit machbar, da klein und ohne Produktfragen
apps/api/prisma/schema.prisma:72
apps/api/src/ldap/ldap-config.service.ts:38
apps/api/src/calendar/crypto.service.ts

Problem

LdapConfig.bindPassword wird so gespeichert, wie es aus dem Formular kommt — ldap-config.service.ts:38 und :76 schreiben den Wert unveraendert in die Datenbank. Wer Lesezugriff auf die Datenbank hat, hat damit die Zugangsdaten des AD-Dienstkontos.

Hashen ist hier KEIN gangbarer Weg: Tessera muss sich mit diesem Passwort am Domain Controller anmelden, braucht es also im Original. Der richtige Weg ist symmetrische Verschluesselung mit einem Schluessel ausserhalb der Datenbank.

Genau das macht das Projekt an drei anderen Stellen bereits, mit AES-256-GCM:

Feld Modell
encryptedPassword CalendarSource (schema.prisma:333)
encryptedInboxCreds DkvModuleConfig (:264)
encryptedInboxCreds TenderEmailConfig (:289)
encryptedPassword SmtpConfig-naher Bereich (:237)

Der Dienst dafuer existiert: calendar/crypto.service.ts, AES-256-GCM, Schluessel aus der Umgebungsvariable CALENDAR_ENCRYPTION_KEY. Die LDAP-Konfiguration nutzt ihn schlicht nicht — LDAP war frueher dran als der Verschluesselungsdienst und wurde nie nachgezogen.

Was bereits richtig ist: die API gibt das Passwort nie heraus, ldap.controller.ts maskiert es an allen vier Stellen zu ********.

Aufgefallen am 2026-08-11 waehrend der Phase-16-Tests, als die LDAP-Konfiguration fuer eine read-only-Pruefung aus der Datenbank gelesen wurde.

Solution

  1. Den bestehenden Verschluesselungsdienst aus calendar/ heraushebeln, damit er nicht laenger nach einem Modul benannt ist, das ihn zufaellig zuerst brauchte — samt Schluesselvariable. Alternativ zunaechst unveraendert wiederverwenden und die Umbenennung als eigenen Schritt fuehren; das ist die kleinere Aenderung.
  2. LdapConfigService.create/update verschluesselt schreiben, alle Lesestellen entschluesseln. Lesestellen sind ldap.controller.ts (Zeilen 136, 177, 210, 244, 277, 313) und der Sync selbst.
  3. Migration, die den vorhandenen Klartext-Eintrag einmalig verschluesselt. Muster vorhanden — dieselbe Form wie die bestehenden Backfill-Migrationen.
  4. Feld umbenennen (bindPassword → encryptedBindPassword), damit am Schema ablesbar ist, was drinsteht, wie bei den anderen vier Feldern.

Keine offenen Produktfragen — reine Umsetzung, entlang eines im Projekt bereits etablierten Musters.