Encrypt Certificate.Password + CertificateData at rest
Summary
- Encrypt a certificate's private-key material at rest — both
Certificate.Password(the PFX password) ANDCertificate.CertificateData(the PFX/PEM blob), previously cleartext (so the PFX password protection was moot) — inCertificateDataProvider, the single seam every certificate read/write funnels through (deploy-pipeline client-cert auth viaEndpointContextBuilder, the cert-variable expander, the certificate service). Reuses theIVariableEncryptionServiceV2 envelope. Same proven pattern asDeploymentAccount.Credentials(#437) andExternalFeed.Password(#438). Makes the long-staleCertificate.cs"encrypted at rest" comment finally true. -
Non-breaking / zero migration: read-both via the
SQUID_ENCRYPTED_V2:prefix (unprefixed plaintext returned verbatim → pre-existing rows still load, upgrade lazily). Idempotent encrypt-on-write. No service-layer change —CertificateDtoexposes onlyPasswordHasValue/HasPrivateKey. -
Reads detach-then-decrypt (new
IRepository.Detach) rather thanQueryNoTracking. Detaching gives the same flush-safety as #437/#438 (the in-place decrypt is never flushed back as plaintext by a later shared-scopeSaveChanges) and frees the identity map, so the existing single-scope create-then-update/delete CRUD tests don't hit a duplicate-tracking conflict. (QueryNoTrackingis production-safe for account/feed since each mediator request is a fresh scope; the certificate CRUD tests run multiple ops in one scope, which detach handles cleanly.) - Reuses the existing
Security:VariableEncryption:MasterKey— no new key, no hardcoded default (P5 key-lifecycle hardening tracked separately).
Test plan
-
Integration (real Postgres, IntegrationCertificateSecretsAtRest, 4): both columns carrySQUID_ENCRYPTED_V2:at rest and not the cleartext secret; decrypt-on-read; read-both on a legacy plaintext row; same-scope read+decrypt+SaveChangeskeeps both columns encrypted (flush regression); a metadata-only update preserves + re-encrypts the secrets. -
Regression: all 34 existing IntegrationCertificateCrudintegration tests + 99 Certificate unit tests stay green (the detach approach keeps the single-scope CRUD flows working). At-rest integration across Account + Feed + Certificate (12) green — the additiveIRepository.Detachdoesn't affect theQueryNoTrackingslices. -
Crypto-envelope contract (round-trip / read-both / tamper) is unit-covered by the shared IVariableEncryptionServicetests; this persistence-layer change is covered at the integration tier (Rule 9). -
CI: existing K8s Pipeline E2E (client-certificate auth) exercises certificate consumption via the same decrypting provider seam.