Encrypt DeploymentAccount.Credentials at rest
Summary
- Encrypt the
DeploymentAccount.Credentialscolumn at rest — the system's most sensitive secret store (every account type's token / password / cloud secret / SSH private key), previously persisted as plaintext JSON. - Do it at the single seam every account read and write funnels through,
DeploymentAccountDataProvider(deploy pipeline, OpenClaw paths, and account service all load via it), reusing the existingIVariableEncryptionServiceV2 envelope — mirroring howVariableDataProviderprotects theVariabletable. -
Non-breaking / zero migration: read-both via the
SQUID_ENCRYPTED_V2:prefix — a value lacking it is returned verbatim, so pre-existing plaintext rows keep working and upgrade lazily on next save. No schema change, no backfill. Encrypt-on-write is idempotent (IsValidEncryptedValueguard) so a re-save never double-wraps. - Reads use the repository's AsNoTracking query so decrypting the in-memory entity can never be flushed back as plaintext;
GetAccountByIdAsyncis switched from trackingFindAsyncto an AsNoTracking query for the same reason.MapToDtobuilds the response summary from the plaintext credentials the service already holds (the entity carries ciphertext after a write).
Operational prerequisite: reuses the already-configured Security:VariableEncryption:MasterKey — no new key source, no hardcoded default. With an unset MasterKey the envelope is cosmetic (key derived from empty input); real protection requires an operator-set key + SQUID_MASTER_KEY_ENFORCEMENT=strict. Key-lifecycle hardening (forcing a non-empty key in strict mode, platform-rooted derivation) is tracked separately under the SOTA-audit P5.
Test plan
-
Unit ( DeploymentAccountCredentialsEncryptionTests, 14): every account type's serialized credentials round-trips through the V2 envelope with the secret intact + never verbatim; legacy plaintext reads back verbatim (read-both); already-encrypted detection (no double-wrap); tampered ciphertext rejected by the GCM tag. -
Integration (real Postgres, IntegrationAccountCredentialsAtRest, 3): a written account's RAW DB column carries theSQUID_ENCRYPTED_V2:prefix and never the cleartext secret; reads back decrypted through the provider; a hand-inserted plaintext row reads back verbatim (read-both); update re-encrypts. -
Regression: 165 account-related unit tests + 19 integration tests green (incl. DeploymentAccountServiceTests,EndpointContextBuilderTests,PrepareTargetsPhaseTests, OpenClaw) — the MapToDto refactor + provider change are non-breaking. -
CI: the existing K8s Pipeline E2E exercises account-credential consumption end-to-end through the same GetAccountByIdAsyncdecrypt seam.