Skip to content

keyfile config is ignored by InteractiveSSHClient SSH config IdentityFile entries used instead, causing unrelated keys to be tried #145

@gabrielhamalwa

Description

@gabrielhamalwa

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

  1. Have two keys in ~/.ssh/config under Host *, e.g. id_ed25519 (auth) and id_ed25519_signing (git signing key)
  2. Configure a DVC SSH remote with keyfile pointing to only id_ed25519:
    dvc remote modify --local myremote keyfile ~/.ssh/id_ed25519
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions