Encrypt ExternalFeed.Password at rest
Summary
- Encrypt the
ExternalFeed.Passwordcolumn (package-feed credential, previously cleartext) at rest inExternalFeedDataProvider— the single seam every feed read/write funnels through (deploy-pipeline package fetch, Helm/K8s registry auth, the feed service), reusing theIVariableEncryptionServiceV2 envelope. Same proven pattern asDeploymentAccount.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-scopeSaveChanges— the flush-back hazard caught & fixed in #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 change —ExternalFeedDtoexposes onlyPasswordHasValue(a non-empty ciphertext still reportstrue), 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 carriesSQUID_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 unrelatedSaveChangeskeeps the column encrypted (the #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 IVariableEncryptionServicetests from #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.