ADR 028: Interim Encryption of Datasource Credentials at Rest
Date: 2026-02-05
Status: Proposed
Decision Makers: Architecture Board
Context
ADR 027 establishes HashiCorp Vault (or OpenBao) as the target architecture for credential management in CIVITAS/CORE data pipelines. However, Vault integration requires significant implementation effort (deployment, operational procedures, ConfigAdapter extension, Kubernetes auth setup) that cannot be completed before the upcoming release.
Currently, we don't have a solution for storing external datasource credentials (database passwords, API keys, etc.) other than storing them in plaintext in the CIVITAS/CORE PostgreSQL database (not yet implemented). This is an unacceptable security posture, even for an interim period:
- Any database backup, dump, or breach exposes all external credentials.
- Compliance and audit requirements (particularly in the German municipal context) demand encryption of sensitive data at rest.
- Platform operators with database access can read credentials for systems they are not authorized to access.
A lightweight interim solution is needed that:
- Eliminates plaintext credential storage immediately.
- Requires minimal implementation effort and no new infrastructure components.
- Is fully compatible with the existing Spring Boot / JPA / PostgreSQL stack.
- Provides a clear migration path to Vault (ADR 027) in a subsequent release.
Checked Architecture Principles
- [partial] Model-centric data flow — Credentials remain co-located with datasource metadata in the CIVITAS database rather than being externalized to a dedicated secrets store. This is a conscious trade-off for implementation speed; ADR 027 addresses the target state.
- [full] Distributed architecture with unified user experience — No user-facing changes; encryption is transparent at the persistence layer.
- [full] Modular design — The encryption logic is encapsulated in a single JPA
AttributeConverter, cleanly separated from business logic. - [full] Integration capability through defined interfaces — No interface changes; the converter operates transparently below the JPA entity layer.
- [full] Open source as the default — Spring Security Crypto is part of the Spring Security OSS project.
- [full] Cloud-native architecture — Master key is injected via Kubernetes Secrets and environment variables, following 12-factor principles.
- [full] Prefer standard solutions over custom development — AES-256-GCM via
Encryptors.text()from Spring Security Crypto; no custom cryptographic code. - [full] Self-contained deployment — No external service dependency introduced; only a Kubernetes Secret for the master key.
- [full] Technological consistency to ensure maintainability — Spring Security is already part of the platform stack (Keycloak integration); the
cryptomodule is included transitively. - [full] Multi-tenancy — Encryption is applied uniformly; tenant-level key separation is not implemented in this interim solution but is also not required at the current stage.
- [partial] Security by design — Credentials are encrypted at rest (AES-256-GCM), which is a significant improvement over plaintext. However, this approach has inherent limitations compared to Vault: a single master key protects all credentials, there is no audit log of secret access, no dynamic rotation, and no fine-grained access control. These are accepted trade-offs for the interim period.
Decision
JPA AttributeConverter with Spring Security Crypto
All credential fields on datasource entities are annotated with a JPA @Convert annotation pointing to an EncryptedStringConverter. This converter uses Spring Security Crypto's Encryptors.delux() (AES-256-GCM with a random 16-byte IV per encryption operation, key derived via PKCS #5 PBKDF2) to transparently encrypt values before writing to the database and decrypt them upon reading. Encrypted results are returned as hex-encoded strings suitable for storage in standard VARCHAR/TEXT database columns.
Note: Encryptors.text() (AES-256-CBC) must not be used, as CBC mode does not provide authenticated encryption. Encryptors.delux() uses GCM, which guarantees both confidentiality and authenticity.
The converter is stateless from a JPA perspective. Existing application code that reads or writes credential fields requires no changes — encryption and decryption are fully transparent at the persistence layer.
Master Key Management
A symmetric master key and salt are required by Encryptors.delux(). These are provided as environment variables (CIVITAS_MASTER_KEY, CIVITAS_MASTER_SALT), sourced from a Kubernetes Secret. The salt can be generated using Spring Security's KeyGenerators.string().generateKey(), which produces a cryptographically secure random 8-byte hex-encoded string. The master key must be:
- Generated using a cryptographically secure random generator (minimum 256 bits, hex-encoded) during deployment.
- Stored exclusively in a Kubernetes Secret (
civitas-encryption-key), never in source code, configuration files, or the CIVITAS database. - Consistent across all pods that access encrypted credential fields (i.e., the portal backend and the ConfigAdapter must share the same key).
- Backed up securely and separately from database backups. Loss of the master key means loss of all encrypted credentials.
Scope of Encrypted Fields
All fields on datasource-related entities that contain authentication material must be annotated. This includes, but is not limited to: password, apiKey, clientSecret, sshPrivateKey, and any similar fields identified during implementation.
Explicit Interim Status
This solution is explicitly designated as an interim measure. It must be superseded by Vault integration (ADR 027) in a subsequent release. To ensure this, the following constraints apply:
- The
EncryptedStringConverterclass must carry a prominent@Deprecatedannotation and Javadoc comment referencing ADR 027. - A technical debt ticket must be created to track the migration to Vault.
Consequences
- Portal backend must be extended with the
EncryptedStringConverterand the@Convertannotations on all relevant entity fields. This is a low-effort change (estimated: < 1 day). - ConfigAdapter must have access to the same master key to decrypt credentials when assembling pipeline configurations. This is already the case if both services read from the same Kubernetes Secret.
- Database backups no longer expose plaintext credentials. However, they are only as secure as the master key — if both the backup and the master key are compromised, all credentials are recoverable.
- Key rotation of the master key requires re-encrypting all stored credentials (decrypt with old key, encrypt with new key). This is a manual operational procedure in the interim solution.
- Performance impact is negligible — AES-256-GCM encryption/decryption of short strings (passwords, API keys) is sub-millisecond.
Alternatives
- Vault immediately (ADR 027): The target architecture. Discarded for this release due to insufficient implementation time. ADR 027 remains the agreed-upon target state.
- PostgreSQL pgcrypto extension: Database-level encryption using
pgp_sym_encrypt/pgp_sym_decrypt. Discarded because it couples encryption logic to SQL queries, leaks key material into query strings/logs, and is harder to migrate away from than an application-layer converter. - Jasypt (Java Simplified Encryption): A popular library for property/field encryption in Spring. Discarded because Spring Security Crypto is already available in the dependency tree and provides equivalent functionality with stronger defaults (AES-256-GCM vs. Jasypt's older PBE defaults).
- No action (accept plaintext): Discarded. Plaintext credential storage is not acceptable.
See also
- ADR 027 — Credential Management for External Datasource Access in Data Pipelines (target state)
- External specification: Spring Security Crypto Module — Encryptors, KeyGenerators
- API reference: Encryptors Javadoc
- API reference: KeyGenerators Javadoc
- External specification: JPA AttributeConverter