DreamLake

Selected SSH import

dreamlake vault import --ssh reads a local SSH config and saves selected items through the existing vault entry API. It does not connect to hosts. The destination server must have authenticated vault routes and encryption configured; unavailable routes are reported as failures. No episode or other plaintext upload is used.

shell
# Discover item IDs and review metadata without reading private keys or calling vault.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --dry-run

# Open an empty checklist, then review selected destinations before uploading.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config

Omitting --config uses ~/.ssh/config. Use arrows to move, Space to toggle, Enter to review, and Escape or Ctrl-C to cancel. The final upload confirmation defaults to No. Empty selection or cancellation before upload writes nothing.

Profiles and private-key exports are separate rows. Selecting profile:dev keeps its key-file and jump-host references but does not upload those keys, another alias, or jump-host credentials. A key row is a candidate file reference, not a promise that the file is exportable. Only selected key files are read, after confirmation. Hardware-backed keys remain references.

shell
# Automation: explicit selections are the upload consent; no prompt opens.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --json \
  --select profile:dev

# Add the key only when exporting its private contents is intended.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --json \
  --select profile:dev --select key:dev:1

--select is repeatable. Non-TTY or --json uploads require at least one explicit selection. --dry-run can list all discovered metadata without a selection. The review goes to stderr; stdout contains one JSON result with no secret values. Neither a generic --yes nor quiet mode authorizes key export.

Destinations and conflicts

For prefix alice, profile:dev maps to alice/ssh/profiles/dev and key:dev:1 maps to alice/ssh/keys/dev-1. Key indexes are the one-based IdentityFile order within that concrete Host block. Review them again after editing the config. The prefix is required; the review shows the server, prefix, item type and exact vault names. Authentication is pinned for the upload/retry session.

Profiles are string-field maps with schema dreamlake-ssh-profile-v1, the alias, explicit hostname/user/port, optional jump reference, and a JSON string containing identity-file references. They contain no executable SSH directives. Omitted user/port remain unspecified; global defaults are not guessed. Keys use the ordinary vault string type, preserving UTF-8 bytes, CRLF and trailing newlines. No local key file is written and no suggested filename triggers file export.

An equal active destination is reported as verified, without another write. A differing destination is a conflict until its revision is explicitly supplied:

shell
dreamlake vault show -n alice/ssh/profiles/dev
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --json \
  --select profile:dev --if-match profile:dev=3

--if-match is repeatable and applies only to its selected item. Revisions are never automatically advanced on retry. A missing revision means create-only. Source files and unselected vault entries are not deleted or modified.

Partial results and retries

Each selected item reports pending, success, failure, or unknown. Cancel during upload to stop subsequent entries; an in-flight request is allowed to settle so its outcome can be reported. Completed writes remain saved. There is no batch rollback. Non-successful final results exit with status 1.

The interactive retry keeps the same selection and source values. Automation can use --retry 1 (maximum 3). Successful entries are not rewritten. Failed requests keep their original revision guard. Uncertain writes are only read back: an equal destination is verified, while an absent, different or unreadable destination remains unknown. A transport failure or malformed write acknowledgement does not prove that nothing was saved.

The backend currently has no entry-write operation receipt. Import cannot prove which request produced an equal value, or safely replay an unresolved write. Retry state is held in the current process, not a durable journal. Preserve the redacted result and resolve unknown outcomes before starting a new invocation; a new invocation is a new explicit request. No cross-process idempotency or backend source-manifest support is claimed. A rejected source file needs a new review after it is corrected.

Pass OTP import and code retrieval

Choose one source: --ssh or --pass-otp. Redacted dry-run previews an explicitly named store. Selected TOTP upload and owner-only code retrieval are available in CLI 0.13.0 and Python 0.10.0, with the deployed DreamLake API. For self-hosted servers, use a backend with TOTP support. HOTP remains preview-only. Ordinary --pass is reserved and rejected.

shell
dreamlake vault import --pass-otp -p alice --dry-run \
  --store /absolute/path/to/password-store --gpg-home /absolute/path/to/gnupg

The dry-run result contains metadata and uploaded: false; decryption failures and invalid OTP records cause a nonzero exit. Source selection is mutually exclusive, and source-specific options cannot be mixed. Prefix placement works before vault, after vault, or after import; conflicting prefixes fail.

Existing vault ssh sync and vault pass sync --otp commands remain compatibility aliases. Prefer vault import --ssh and vault import --pass-otp in new scripts. Import is a selected, one-way operation: it does not delete source entries or perform tracked reconciliation. The word sync is reserved for that future capability rather than a promise made by the compatibility aliases.

Supported sources

Discovery parses text without running OpenSSH, commands, plugins or config conditions. Include is reported and not followed. Match, global defaults, wildcard inheritance and blocks containing wildcard/negated patterns are not resolved. Unsupported directives are omitted with line-number warnings, including ProxyCommand, LocalCommand and host-key-checking overrides. The result is an explicit-fields profile, not a fully evaluated OpenSSH configuration.

Concrete aliases must start with a letter/digit and contain only letters, digits, underscores or hyphens, up to 128 characters. Duplicate aliases (including case variants), dotted aliases and unsafe aliases are excluded with warnings; they are never silently renamed. Simplify the selected config explicitly if necessary. Quoted values, comments, CRLF and Keyword=value are supported. Ambiguous escaped syntax or unsupported connection-field values exclude the affected profile.

Only absolute and ~/ key paths can offer export rows. Relative, tokenized, wildcard, environment-expanded and none identity paths stay references. Private-key files must be owned by the current user with no group/other permission bits, and use a supported PEM/OpenSSH private-key envelope. Config files must not be group/other writable. Both must be regular files, at most 64 KiB, valid UTF-8 without NULs, with no symlink components or hard links. File identity and changes are checked while reading; serialized vault values must also fit 64 KiB.

Implementation tracking: SSH sync #241 and master #247. CLI 0.13.0 and Python 0.10.0 passed production import and TOTP checks; see the release evidence. After importing credentials, verify the intended host login separately.

shell
# Create only the selected login.gpg registration under alice/otp/login.
dreamlake vault import --pass-otp -p alice/otp --store /canonical/test-store --select login
# Explicitly reveal a code; never put it in logs or command arguments.
dreamlake vault otp -p alice/otp -n login
dreamlake vault otp -p alice/otp -n login --to-json
python
result = client.vault.import_entries(source="pass-otp", prefix="alice/otp",
                                    store="/canonical/test-store", select=["login"])
code = client.vault.otp("login", prefix="alice/otp")
code_json = client.vault.otp("login", prefix="alice/otp", to_json=True)

Selection is mandatory, even on a TTY; interactive upload additionally asks [y/N]. --json is noninteractive, and explicit --select consents to those paths only. Python never prompts. Pass paths omit .gpg and use canonical slash-separated names. GPG uses batch/no-tty/pinentry-error. Apply decrypts only selected files, rejects HOTP, and excludes adjacent password lines. It validates the entire batch and rechecks encrypted source bytes before upload. There is no persisted preview plan; apply validates a fresh selection each time.

TOTP import creates entries only. Conflicts stop the batch; OTP does not accept SSH --if-match or --retry options. Successful earlier writes are retained. An unknown result means a write may have committed; inspect show and securely compare with explicit get before deciding what to do. Never blindly repeat an unknown import. General recovery is a separate workstream.

otp --to-json returns only code and validUntil; without it, output is the code. The server clock determines the code, and entry expiry may shorten its validity. Scoped keys cannot generate codes. Explicit vault get instead reveals the registration JSON string, including its seed. The target login service enforces one-time acceptance. HOTP authority/counter safety is deferred; no HOTP counter is advanced by these commands. The legacy pass sync command continues to provide preview only.