Skip to content

feat: Add support for SSH agents in addition to raw keyfiles#901

Open
KazWolfe wants to merge 8 commits into
railwayapp:masterfrom
KazWolfe:ssh-agent-support
Open

feat: Add support for SSH agents in addition to raw keyfiles#901
KazWolfe wants to merge 8 commits into
railwayapp:masterfrom
KazWolfe:ssh-agent-support

Conversation

@KazWolfe
Copy link
Copy Markdown

@KazWolfe KazWolfe commented May 13, 2026

Simple-ish attempt to fix #870 since it started to become a headache for me. I'll preface this PR by warning everyone that I am not a Rust developer, so I'm pretty sure a lot of things in here are wrong or deeply suboptimal (though I did have some people who are much better at Rust than me look at it and tell me to do arcane things). I'd be more than willing to bring in any fixes.

Changes:

  • Read all SSH keys returned by ssh-add -L to query the agent, then read keys from ~/.ssh.
    • If a key is in both locations, prefer the key from the agent.
  • Add new methods LocalSshKey#key_name and #key_source to encapsulate some logic.
    • Key name is decided by comment, then file_stem, then hash.
    • Key source is the path or an SSH agent hint.
  • Replace hostname with comment for semantic clarity.
    • Comments are now everything after the second split.

High level demo:

https://asciinema.org/a/wj2g7MpQf2AC82Wz

@KazWolfe KazWolfe changed the title feat: Add support for SSH agents as well as raw file scans feat: Add support for SSH agents in addition to raw keyfiles May 21, 2026
Comment thread src/commands/ssh/keys.rs
Comment on lines +259 to +260
if !comment.is_empty() {
println!(" Comment: {}", comment);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semantics, I would prefer if we didn't change this- such that we can keep things consistent. I would prefer that we add a separate method so that we don't overwrite existing behavior.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix the lint aside, however the semantics of Hostname are wrong (imo) here - SSH key comments are significantly more broad and cover things like card serial numbers, generating user/host (as probably implied), etc. I can revert if preferred, but the intent is to follow how SSH itself wants to work.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for SSH-agent–resident keys to railway ssh so users without on-disk *.pub files (forwarded agents, 1Password, hardware tokens, etc.) can register and use keys. find_local_ssh_keys is reworked to merge keys from ssh-add -L and ~/.ssh/*.pub, deduplicated by fingerprint with agent keys winning. LocalSshKey becomes a unified file-or-agent representation with new key_name() / key_source() helpers, and the CLI display/registration paths are updated to use them. The previous "Hostname" field in listings is renamed to "Comment" and now captures everything after the second whitespace split.

Changes:

  • New fetch_keys_from_ssh_agent() and refactored find_local_ssh_keys() that merges agent + file keys.
  • LocalSshKey.path becomes Option<PathBuf>, gains key_comment, and exposes key_name() / key_source() helpers, with all callers updated.
  • Renamed displayed "Hostname" → "Comment" and switched to a full parts[2..].join(" ") capture in keys.rs.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/controllers/ssh_keys.rs Core changes: LocalSshKey made agent-capable, agent fetch added, find_local_ssh_keys merges sources by fingerprint.
src/commands/ssh/native.rs Uses key_name() / key_source() in prompts, registration, and "Using SSH key" message; no longer assumes a file path.
src/commands/ssh/keys.rs List/add flows updated for agent keys: comment instead of hostname, key_source() for Source line, path matching guarded by Option.
Comments suppressed due to low confidence (3)

src/controllers/ssh_keys.rs:61

  • fetch_keys_from_ssh_agent propagates errors with ? to find_local_ssh_keys, which means any environment without ssh-add installed, without SSH_AUTH_SOCK set, with the agent unreachable, or simply with no identities loaded will cause find_local_ssh_keys() to fail entirely. Previously this function would succeed and return the on-disk keys. This regresses every caller — ensure_ssh_key, list_keys, add_key, and preflight_db_stats_ssh — for users who don't run an SSH agent (a very common case on Linux desktops and CI). The agent fetch should degrade gracefully: when ssh-add is missing, exits non-zero (e.g. exit code 1 = "agent has no identities", exit code 2 = "cannot connect to agent"), or the command can't be spawned, treat it as "no agent keys" and continue to scan ~/.ssh/, rather than bailing.
    for key in fetch_keys_from_ssh_agent()? {
        seen.entry(key.fingerprint.clone()).or_insert(key);
    }

    for key in find_ssh_key_files()? {
        seen.entry(key.fingerprint.clone()).or_insert(key);
    }

src/controllers/ssh_keys.rs:133

  • parts[0] and parts[2..] are indexed without verifying the line has at least one (or two) whitespace-separated fields. read_ssh_key performs an explicit parts.len() < 2 check before doing the same indexing — this function should do the same. While ssh-add -L output is normally well-formed, a stray blank/whitespace-only line (e.g. trailing whitespace after the final newline that survives filter(|s| !s.is_empty()) because it contains only spaces) or any future agent quirk would panic the process. Also note parts[2..] panics when parts.len() < 2, not when it equals 2.
        .map(|s| {
            let parts: Vec<_> = s.split_whitespace().collect();
            let fingerprint = compute_fingerprint_from_pubkey(s)?;

            Ok(LocalSshKey {
                path: None,
                public_key: s.trim().to_string(),
                fingerprint,
                key_type: parts[0].to_string(),
                key_comment: parts[2..].join(" ").into(),
            })

src/controllers/ssh_keys.rs:30

  • path.file_stem().unwrap() will panic if the path terminates in .. or otherwise has no file stem. Although unlikely in practice for ~/.ssh/*.pub, this is a hard panic in a user-facing code path; consider falling back to the fingerprint (as the final arm already does) when file_stem() is None.
            (_, Some(path)) => path.file_stem().unwrap().to_string_lossy(),

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/controllers/ssh_keys.rs
Comment thread src/commands/ssh/keys.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

src/controllers/ssh_keys.rs:151

  • Same Some("")-vs-None issue as in fetch_keys_from_ssh_agent: for a key file that contains just <type> <base64> (no trailing comment), parts.get(2..) returns Some(&[]) and key_comment becomes Some(""). key_name() will then resolve to the empty string instead of falling back to the file stem. Filter empty joined comments down to None.
    let key_comment = parts.get(2..).map(|p| p.join(" "));

Comment thread src/controllers/ssh_keys.rs Outdated
Comment thread src/controllers/ssh_keys.rs Outdated
Comment thread src/controllers/ssh_keys.rs Outdated
Comment thread src/controllers/ssh_keys.rs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread src/controllers/ssh_keys.rs
Comment thread src/commands/ssh/keys.rs Outdated
Comment thread src/commands/volume/ssh_key.rs Outdated
for public_key in find_local_ssh_keys()? {
if let Some(private_key_path) = private_key_path_for_public_key(&public_key.path) {
for public_key in find_ssh_key_files()? {
if public_key.path.is_none() {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added in after I started working on this, so for now I just patched things such that this only works with SSH keyfiles. It'll take a bit more work in general to get agent support working properly, and it'd really be better to refactor all of the key discovery mechanism(s) now that we have russh accessible to us.

(As an aside, this is a very fragile way of doing key discovery, but eh, it works.)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment on lines +122 to +141
String::from_utf8_lossy(&output.stdout)
.split("\n")
.filter(|s| !s.is_empty())
.map(|s| {
let parts: Vec<_> = s.split_whitespace().collect();
let fingerprint = compute_fingerprint_from_pubkey(s)?;
let key_comment = parts
.get(2..)
.map(|p| p.join(" "))
.filter(|s| !s.is_empty());

Ok(LocalSshKey {
path: None,
public_key: s.trim().to_string(),
fingerprint,
key_type: parts[0].to_string(),
key_comment,
})
})
.collect()
Copy link
Copy Markdown
Author

@KazWolfe KazWolfe May 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pre-existing - any such explosion on file-based keys would do same. This is another good candidate for "rewrite the entire thing using russh." (which only became a thing after I opened this PR!)

Comment thread src/controllers/ssh_keys.rs
Comment thread src/commands/volume/ssh_key.rs Outdated
Comment thread src/commands/volume/ssh_key.rs Outdated
- replace collect with pattern matching
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/minor Author minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

railway ssh ignores ssh-agent, fails when keys aren't on disk

4 participants