Encrypt SSH proxy password at rest
Summary
- The SSH proxy password lived as plaintext inside the machine endpoint JSON column. Encrypt it at rest via the shared
AtRestSecretProtector— at the two write seams (MachineRegistrationServiceregister +MachineService.MergeSshupdate) and decrypt at the two read seams (SshEndpointVariableContributordeploy path +SshHealthCheckStrategyconnection builder). - The read seams are synchronous (
IEndpointVariableContributor.ContributeVariables), and decrypt is pure CPU work, so add a synchronousDecrypttoIVariableEncryptionService(the already-sync core ofDecryptAsync, which now delegates to it) and a synchronousUnprotecttoIAtRestSecretProtector. This keeps the contributor interface sync — no async ripple across every transport's contributor.
Second P2 at-rest finisher (after AtRestSecretProtector extraction #445). Remaining: Tentacle private key (agent-side, separate mechanism).
Non-breaking
-
No new plaintext exposure: the proxy password ships inside the opaque
Machine.EndpointJSON returned by machine GET/list — previously as plaintext, now as the encrypted envelope (a net improvement). No consumer round-trips it (SquidWeb has zero ProxyPassword references; updates send changed fields only). -
Read-both: a legacy plaintext proxy password in an existing machine's endpoint passes through
Unprotectverbatim — no migration. - The protector is an optional ctor param on all four seams; DI injects the real one in production, and any test that constructs them without it degrades to the pre-feature plaintext path. Encrypt is idempotent, so a re-save (or the unchanged value carried through an update merge) never double-wraps.
Test plan
-
Unit ( SshProxyPasswordAtRestTests): contributor decrypts an envelope before contributing the SSH connection variable; legacy plaintext passes through; no-protector leaves the value untouched. -
Integration (real Postgres + real DI + real AES-256-GCM, IntegrationSshProxyPasswordAtRest): a proxy password encrypted via the real protector and stored in a machine's endpoint jsonb is decrypted by the DI-resolved contributor (proves the protector is wired into the read seam, raw column carries theSQUID_ENCRYPTED_V2:envelope not cleartext); legacy plaintext reads back verbatim. -
Full unit suite green (6048).