feat(ldap): encrypt the bind password at rest
The LDAP bind password was the only credential still stored in clear text. CalendarSource, SmtpConfig, DkvModuleConfig and TenderEmailConfig have been AES-256-GCM encrypted for a while; LDAP simply predated the encryption service and was never brought along. Hashing is not an option here: Tessera has to replay this password to bind against the directory, so it must stay recoverable. Encryption at rest covers the case a hash cannot help with either way -- a database dump or backup leaving the host without the key, which lives in the application environment. It does not protect against a compromised host, and does not pretend to. Reuses CalendarCryptoService, the same provider SettingsModule, DkvModule and TendersModule already inject, rather than introducing a second crypto path. The name is a historical accident and is noted as such in LdapModule; renaming it touches five modules and belongs in its own change. Decryption sits in getConfig()/getAllActiveConfigs(), the two methods every consumer already goes through, so callers keep reading a plain `bindPassword` and the controller keeps masking it to '********' in responses. The migration only renames the column -- SQL cannot encrypt, since the key is not in the database. An idempotent bootstrap backfill encrypts rows written before this change, and until it has run the read path passes a legacy plaintext value through unchanged so the sync does not break in that window. A failed decrypt throws rather than returning null: a wrong key must not read as "no password configured" and silently turn an authenticated bind into an anonymous one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
-- Das LDAP-Bind-Passwort lag als einziges Zugangsdatum im Klartext in der
|
||||
-- Datenbank. CalendarSource, SmtpConfig, DkvModuleConfig und TenderEmailConfig
|
||||
-- speichern ihre Zugangsdaten laengst AES-256-GCM-verschluesselt; LDAP war
|
||||
-- schlicht frueher da als der Verschluesselungsdienst und wurde nie nachgezogen.
|
||||
--
|
||||
-- Hashen scheidet hier aus: Tessera muss sich mit genau diesem Passwort am
|
||||
-- Domain Controller anmelden, braucht es also im Original. Deshalb symmetrische
|
||||
-- Verschluesselung mit einem Schluessel ausserhalb der Datenbank.
|
||||
--
|
||||
-- Diese Migration benennt nur die Spalte um, damit am Schema ablesbar ist, was
|
||||
-- drinsteht -- gleiche Namensform wie bei den vier anderen Feldern. Die
|
||||
-- eigentliche Verschluesselung des vorhandenen Werts kann SQL nicht leisten
|
||||
-- (der Schluessel liegt in der Anwendungsumgebung, nicht in der Datenbank):
|
||||
-- die uebernimmt ein einmaliger, idempotenter Backfill beim naechsten Start
|
||||
-- der API (LdapConfigService.encryptLegacyBindPasswordsOnBootstrap).
|
||||
--
|
||||
-- Bis dieser Backfill gelaufen ist, steht in der umbenannten Spalte weiterhin
|
||||
-- Klartext. Der Lesepfad erkennt das an der fehlenden iv:authTag:ciphertext-Form
|
||||
-- und reicht den Wert unveraendert durch, damit der LDAP-Sync in dem Zeitfenster
|
||||
-- nicht bricht.
|
||||
|
||||
ALTER TABLE "LdapConfig" RENAME COLUMN "bindPassword" TO "encryptedBindPassword";
|
||||
@@ -69,7 +69,7 @@ model LdapConfig {
|
||||
serverUrl String
|
||||
baseDn String
|
||||
bindDn String?
|
||||
bindPassword String?
|
||||
encryptedBindPassword String? // AES-256-GCM ciphertext (iv:authTag:ciphertext hex), wie CalendarSource/SmtpConfig
|
||||
searchFilter String @default("(objectClass=person)")
|
||||
syncIntervalMin Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
Reference in New Issue
Block a user