Labels: bug
dvc-ssh version: 4.3.0
asyncssh version: 2.23.0
Description
When keyfile is configured for an SSH remote, DVC correctly passes it to asyncssh.connect() as client_keys. However, when the key requires a passphrase (triggering the interactive auth path), InteractiveSSHClient.public_key_auth_requested() ignores options.client_keys and instead reads IdentityFile entries directly from the SSH config file:
# dvc_ssh/client.py — public_key_auth_requested()
config = options.config
client_keys = cast("Sequence[FilePath]", config.get("IdentityFile", ()))
This means any IdentityFile entries inherited from a Host * block in ~/.ssh/config are tried as well, including keys that are unrelated to the configured remote (e.g. a git commit signing key). This produces unexpected passphrase prompts for keys the user never intended to use with DVC.
Steps to reproduce
- Have two keys in
~/.ssh/config under Host *, e.g. id_ed25519 (auth) and id_ed25519_signing (git signing key)
- Configure a DVC SSH remote with
keyfile pointing to only id_ed25519:
dvc remote modify --local myremote keyfile ~/.ssh/id_ed25519
- Run
dvc pull
Expected: only ~/.ssh/id_ed25519 is tried; no prompt for id_ed25519_signing
Actual: both keys are tried; user is prompted for the passphrase of id_ed25519_signing
Root cause
public_key_auth_requested() sources its key list from options.config.get("IdentityFile") (the parsed SSH config) rather than from options.client_keys (the explicit client_keys argument passed to asyncssh.connect()). The explicitly configured keyfile is therefore only respected for the initial non-interactive auth attempt, not for the interactive passphrase-prompting fallback.
Suggested fix
In public_key_auth_requested(), prefer options.client_keys when it is set, falling back to SSH config only when no explicit keys are configured:
# Prefer explicitly configured client_keys over SSH config discovery
client_keys = list(options.client_keys) if options.client_keys else None
if not client_keys:
client_keys = cast("Sequence[FilePath]", config.get("IdentityFile", ()))
if not client_keys:
client_keys = [
os.path.expanduser(os.path.join("~", ".ssh", path))
for path, cond in _DEFAULT_KEY_FILES
if cond
]
Workaround
Remove unintended keys from Host * in ~/.ssh/config so they are not picked up by asyncssh's config parsing.
Labels:
bugdvc-ssh version: 4.3.0
asyncssh version: 2.23.0
Description
When
keyfileis configured for an SSH remote, DVC correctly passes it toasyncssh.connect()asclient_keys. However, when the key requires a passphrase (triggering the interactive auth path),InteractiveSSHClient.public_key_auth_requested()ignoresoptions.client_keysand instead readsIdentityFileentries directly from the SSH config file:This means any
IdentityFileentries inherited from aHost *block in~/.ssh/configare tried as well, including keys that are unrelated to the configured remote (e.g. a git commit signing key). This produces unexpected passphrase prompts for keys the user never intended to use with DVC.Steps to reproduce
~/.ssh/configunderHost *, e.g.id_ed25519(auth) andid_ed25519_signing(git signing key)keyfilepointing to onlyid_ed25519:dvc remote modify --local myremote keyfile ~/.ssh/id_ed25519dvc pullExpected: only
~/.ssh/id_ed25519is tried; no prompt forid_ed25519_signingActual: both keys are tried; user is prompted for the passphrase of
id_ed25519_signingRoot cause
public_key_auth_requested()sources its key list fromoptions.config.get("IdentityFile")(the parsed SSH config) rather than fromoptions.client_keys(the explicitclient_keysargument passed toasyncssh.connect()). The explicitly configuredkeyfileis therefore only respected for the initial non-interactive auth attempt, not for the interactive passphrase-prompting fallback.Suggested fix
In
public_key_auth_requested(), preferoptions.client_keyswhen it is set, falling back to SSH config only when no explicit keys are configured:Workaround
Remove unintended keys from
Host *in~/.ssh/configso they are not picked up by asyncssh's config parsing.