Skip to main content

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 crypto module 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 EncryptedStringConverter class must carry a prominent @Deprecated annotation 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 EncryptedStringConverter and the @Convert annotations 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