DreamLake

Host key rotation implementation

Published CLI 0.18.0 / Python 0.15.0. This hidden plan remains publicly accessible. Published-client hosted staging acceptance passed; production acceptance is separate.

Rotate one selected host binding with a durable local operation file. Save the replacement before installing its public key, verify a fresh login, conditionally switch the binding, remove the exact old authorization, and verify both old-key rejection and replacement login. Only then confirm cleanup. Target and jump-host bindings are rotated separately; neither operation retires a possibly shared vault entry automatically.

shell
dreamlake vault rotate-key \
  --binding-id "$BINDING_ID" \
  --operation-file /private/rotation/bos14.json \
  --new-entry geyang/bos14/login-next \
  --ssh '-p 22 alice@bos14.example' \
  --known-hosts /private/rotation/known_hosts \
  --jump-ssh 'alice@jump.example' \
  --jump-identity /private/rotation/jump-key

The paired Python method is Vault.rotate_host_key(binding_id=..., operation_file=..., new_entry=..., ssh=...). Its ssh mapping contains host, user, port, knownHostsFile, and an optional jump endpoint with its separate identityFile. Python calls never prompt unexpectedly. The normalized mapping and immutable phase records must allow either client to resume the same operation.

Dev Notes — 2026-09-13

Both candidate clients completed real target rotation through a jump host and direct jump rotation. Each operation confirmed cleanup, a freshly restored replacement key authenticated, the old key failed authentication, and unrelated authorization bytes/options/modes remained unchanged. An unrelated forced command that forged a denial message after successful authentication was rejected by both clients. Two disposable users and homes were independently confirmed absent after cleanup; six synthetic entries were retired and the owned fixture database removed. Sanitized evidence with exact source hashes.

The backend was a local native Mongo/HTTP fixture with synthetic enrollment metadata; SSH used a real authorized Linux server. This proves candidate client SSH behavior, not published-package, hosted KMS or live Nymph enrollment acceptance. The merged backend cleanup API records client attestation, not server-proven SSH authentication.

The CLI includes isolated OpenSSH configuration, immutable fsynced journals with crash recovery, explicit endpoint parsing, pinned API requests, the phase engine and a bundled Python remote helper. Remote Python 3 is required; an installed local Python SDK is not. Full CLI validation passed 965 tests plus TypeScript after integrating prefix migration. Quoted SSH port arguments are preserved alongside DreamLake prefixes. Parent-client authentication logs distinguish real denial from remote stderr or jump authentication; bounded private logs are removed after each probe.

Ambient SSH configuration is excluded because extra identities or connection reuse would invalidate rotation evidence. The initial endpoint grammar accepts explicit user/host and -l/-p; it rejects arbitrary config, command execution, forwarding, and alternate identities. Existing trusted known-hosts files are required. A jump failure, timeout, network failure, or host-key failure never proves that the old target key was revoked.

The clients are merged; CLI 0.18.0 is published to npm/native downloads and Python 0.15.0 is published to PyPI. Published-client hosted staging acceptance passed four target/jump rotations and committed-response-loss recovery, followed by independently verified cleanup. Runner and evidence. Password rotation remains a separate unfinished requirement of Vault #241. Local fault tests cover every durable phase, lost write/supersede/cleanup responses, changed bindings and inconclusive probes; pending recovery retains references and key material rather than removing unconfirmed access.

Recovery files use a unique private .rotation-<operationId> directory beside the operation file. Interrupted preparation may leave an unreferenced candidate directory; inspect the operation manifest before removing it. A publication error may have committed the manifest, so uncertain candidates are retained. Successful confirmed cleanup removes only matching owned private-key files and preserves the metadata journal. Encrypted private keys are currently rejected without prompting; jump transport uses its separately provided key.

Cross-client verification — 2026-09-13

A repeatable test alternated TypeScript and Python through all eight durable phases, including replacement, supersession receipt, verification timestamps and cleanup confirmation. Both implementations used their real metadata and transition validators and verified the exact accumulated state after every phase. This proves journal compatibility for the full phase chain; it does not prove full command recovery over live HTTP/SSH.

shell
node --import tsx scripts/test-rotation-cross-client.mjs \
  /path/to/python-environment/bin/python \
  /path/to/python-sdk-checkout