ADR-0003. AES-256-GCM encryption for provider credentials at rest
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-02-12 |
| Deciders | payment-team, security |
| Supersedes | — |
Context
- Payment provider credentials (VNPAY appId/secrets, per-merchant tokens) are highly sensitive — leaks enable unauthorized payments.
- Storing them plaintext in
Configurationtable is unacceptable. - We need a balance between security and operational simplicity (we're not at the scale where HSM/KMS is required).
Decision
All payment credentials are encrypted at rest using AES-256-GCM via CryptoUtility (from @nx/core). The encryption key is derived from APP_ENV_APPLICATION_SECRET.
Specifics:
- The
Configuration.credentialcolumn is hidden from standard CRUD reads (drizzle hidden field). PaymentConfigurationService.getPaymentCredentialaccesses the column directly via the drizzle connector and decrypts inline.- Same secret encrypts non-credential payment configs in
tValue(e.g., VNPAY'smasterMerchantCode,appId). - All pods (API + WORKER) MUST share
APP_ENV_APPLICATION_SECRET.
Consequences
| Pros | Cons |
|---|---|
| Database dump alone doesn't leak credentials | Single-secret model — leaked secret = total compromise |
| Authenticated encryption (GCM mode) prevents tampering | Secret rotation requires re-encrypting every credential row (manual) |
| In-process decrypt — sub-millisecond | No per-merchant KMS isolation |
| Standard primitive, well-audited | App secret must be stored in K8s secret with strict RBAC |
Rotation procedure
- Generate new secret.
- Read all
Configurationrows with credentials, decrypt with old secret. - Re-encrypt with new secret.
- Roll deployment with new env.
Rotation MUST be coordinated — no half-rolled state where pods have different secrets.
Alternatives Considered
| Option | Pros | Cons | Why rejected |
|---|---|---|---|
| HashiCorp Vault / AWS KMS | Per-credential keys, rotation built-in | Vendor lock-in, ops cost, latency | Premature for our scale |
| Asymmetric (RSA per merchant) | Per-merchant isolation | Complex; key explosion | Unnecessary for current threat model |
| Plaintext + DB ACL | Simple | Unacceptable — DB ops can read credentials | No |
References
core/src/utilities/crypto.utility.ts(CryptoUtility)services/payment-configuration.service.ts:43-176(decrypt sites)core/src/models/schemas/public/configuration/schema.ts(hiddencredentialcolumn)