Skip to content

Encrypt DeploymentAccount.Credentials at rest

Placeholder ppxd requested to merge feat/encrypt-account-credentials-at-rest into main

Summary

  • Encrypt the DeploymentAccount.Credentials column 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 existing IVariableEncryptionService V2 envelope — mirroring how VariableDataProvider protects the Variable table.
  • 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 (IsValidEncryptedValue guard) 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; GetAccountByIdAsync is switched from tracking FindAsync to an AsNoTracking query for the same reason. MapToDto builds 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 the SQUID_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 GetAccountByIdAsync decrypt seam.

Merge request reports

Loading