Skip to content

Encrypt ExternalFeed.Password at rest#438

Merged
ppXD merged 1 commit into
mainfrom
feat/encrypt-external-feed-password-at-rest
Jun 13, 2026
Merged

Encrypt ExternalFeed.Password at rest#438
ppXD merged 1 commit into
mainfrom
feat/encrypt-external-feed-password-at-rest

Conversation

@ppXD

@ppXD ppXD commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Encrypt the ExternalFeed.Password column (package-feed credential, previously cleartext) at rest in ExternalFeedDataProvider — the single seam every feed read/write funnels through (deploy-pipeline package fetch, Helm/K8s registry auth, the feed service), reusing the IVariableEncryptionService V2 envelope. Same proven pattern as DeploymentAccount.Credentials (#437).
  • Decrypt-on-read uses repository.QueryNoTracking (detached entities) so the in-place decrypt can never be flushed back as plaintext by a later shared-scope SaveChanges — the flush-back hazard caught & fixed in Encrypt DeploymentAccount.Credentials at rest #437, applied here from the start.
  • Non-breaking / zero migration: read-both via the SQUID_ENCRYPTED_V2: prefix (unprefixed values returned verbatim → pre-existing plaintext rows still load, upgrade lazily). Encrypt-on-write is idempotent. No service-layer changeExternalFeedDto exposes only PasswordHasValue (a non-empty ciphertext still reports true), so the response stays correct with the entity holding ciphertext.
  • Reuses the existing Security:VariableEncryption:MasterKey — no new key source, no hardcoded default (P5 key-lifecycle hardening tracked separately).

Test plan

  • Integration (real Postgres, IntegrationExternalFeedPasswordAtRest, 4): raw column carries SQUID_ENCRYPTED_V2: and not the cleartext secret; decrypt-on-read; read-both on a hand-inserted plaintext row; update re-encrypts; same-scope read+decrypt then unrelated SaveChanges keeps the column encrypted (the Encrypt DeploymentAccount.Credentials at rest #437 flush-back regression, applied as a guard here).
  • Regression: 411 feed-related unit + 9 integration tests green (no breakage to package fetch / registry auth / feed service).
  • Crypto-envelope contract (round-trip / read-both / tamper) is unit-covered by the shared IVariableEncryptionService tests from Encrypt DeploymentAccount.Credentials at rest #437; this persistence-layer change is correctly covered at the integration tier (Rule 9).
  • CI: existing K8s Pipeline E2E (feed secrets) exercises feed consumption via the same decrypting provider seam.

The package-feed credential was persisted in cleartext. Encrypt it at rest in
ExternalFeedDataProvider — the single seam every feed read/write funnels through
(deploy-pipeline package fetch, Helm/K8s registry auth, the feed service all load
via GetFeedByIdAsync / GetExternalFeedsByIdsAsync / paging) — reusing the
IVariableEncryptionService V2 envelope, same pattern as DeploymentAccount (#437).

- Encrypt-on-write (Add/Update, idempotent IsValidEncryptedValue guard).
- Decrypt-on-read via repository.QueryNoTracking (detached entities) so the in-place
  decrypt is never flushed back as plaintext by a later shared-scope SaveChanges —
  the exact hazard caught + fixed in #437, applied here from the start.
- Read-both: an unprefixed value is returned verbatim, so pre-existing plaintext
  rows still load and upgrade lazily. No schema change, no migration.

No service-layer change: ExternalFeedDto exposes only PasswordHasValue (a non-empty
ciphertext still reports true), so the response stays correct with the entity
holding ciphertext.

Reuses the existing Security:VariableEncryption:MasterKey (no new key, no hardcoded
default). Integration tests (real Postgres) cover encrypted-at-rest column,
decrypt-on-read, read-both on a legacy plaintext row, update re-encrypt, and the
same-scope flush regression.
@ppXD ppXD added this to the 1.9.1 milestone Jun 13, 2026
@ppXD
ppXD merged commit 83a96b7 into main Jun 13, 2026
15 checks passed
@ppXD
ppXD deleted the feat/encrypt-external-feed-password-at-rest branch June 13, 2026 15:46
ppXD added a commit that referenced this pull request Jun 13, 2026
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.
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