Skip to content

Encrypt Certificate.Password + CertificateData at rest#439

Merged
ppXD merged 1 commit into
mainfrom
feat/encrypt-certificate-secrets-at-rest
Jun 13, 2026
Merged

Encrypt Certificate.Password + CertificateData at rest#439
ppXD merged 1 commit into
mainfrom
feat/encrypt-certificate-secrets-at-rest

Conversation

@ppXD

@ppXD ppXD commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Encrypt a certificate's private-key material at rest — both Certificate.Password (the PFX password) AND Certificate.CertificateData (the PFX/PEM blob), previously cleartext (so the PFX password protection was moot) — in CertificateDataProvider, the single seam every certificate read/write funnels through (deploy-pipeline client-cert auth via EndpointContextBuilder, the cert-variable expander, the certificate service). Reuses the IVariableEncryptionService V2 envelope. Same proven pattern as DeploymentAccount.Credentials (#437) and ExternalFeed.Password (#438). Makes the long-stale Certificate.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 changeCertificateDto exposes only PasswordHasValue/HasPrivateKey.
  • Reads detach-then-decrypt (new IRepository.Detach) rather than QueryNoTracking. Detaching gives the same flush-safety as Encrypt DeploymentAccount.Credentials at rest #437/Encrypt ExternalFeed.Password at rest #438 (the in-place decrypt is never flushed back as plaintext by a later shared-scope SaveChanges) and frees the identity map, so the existing single-scope create-then-update/delete CRUD tests don't hit a duplicate-tracking conflict. (QueryNoTracking is 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 carry SQUID_ENCRYPTED_V2: at rest and not the cleartext secret; decrypt-on-read; read-both on a legacy plaintext row; same-scope read+decrypt+SaveChanges keeps both columns encrypted (flush regression); a metadata-only update preserves + re-encrypts the secrets.
  • Regression: all 34 existing IntegrationCertificateCrud integration 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 additive IRepository.Detach doesn't affect the QueryNoTracking slices.
  • Crypto-envelope contract (round-trip / read-both / tamper) is unit-covered by the shared IVariableEncryptionService tests; 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.

A certificate's PFX password AND the PFX/PEM blob (private-key material) were both
persisted in cleartext — so the PFX password protection was moot. Encrypt both at
rest in CertificateDataProvider, the single seam every certificate read/write
funnels through (deploy-pipeline client-cert auth via EndpointContextBuilder, the
cert-variable expander, the certificate service), reusing the
IVariableEncryptionService V2 envelope. Same pattern as DeploymentAccount (#437) /
ExternalFeed (#438). Read-both (unprefixed plaintext passthrough) = non-breaking,
no migration. No service change (CertificateDto is HasValue-only). Makes the
long-stale Certificate.cs 'encrypted at rest' comment finally true.

Reads use a load-tracked-then-Detach-then-decrypt approach (new IRepository.Detach)
rather than QueryNoTracking. Detaching gives the same flush-safety guarantee as
#437/#438 (the in-place decrypt is never flushed back as plaintext by a later
shared-scope SaveChanges) AND, unlike a second AsNoTracking instance, frees the
identity map — so the existing single-scope create-then-update/delete CRUD tests
do not hit a duplicate-tracking conflict. (QueryNoTracking is production-safe for
account/feed because each mediator request is a fresh scope; the certificate CRUD
tests exercise multiple ops in one scope, which detach handles cleanly.)

Tests: integration (real Postgres) covers both columns encrypted-at-rest,
decrypt-on-read, read-both on a legacy plaintext row, the same-scope flush
regression, and a metadata-only update preserving + re-encrypting the secrets. All
34 existing Certificate integration tests + 99 unit tests stay green.
@ppXD ppXD added this to the 1.9.1 milestone Jun 13, 2026
@ppXD
ppXD merged commit c5d6e38 into main Jun 13, 2026
15 checks passed
@ppXD
ppXD deleted the feat/encrypt-certificate-secrets-at-rest branch June 13, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant