Skip to content

Support per-jump-host SSH private key configuration in config.yaml #167

@inureyes

Description

@inureyes

Problem / Background

Currently, the jump_host field in config.yaml (and in the Defaults, ClusterDefaults, and NodeConfig types) only accepts an OpenSSH ProxyJump-style string: [user@]hostname[:port]. The JumpHost struct in src/jump/parser/host.rs only contains user, host, and port fields — there is no ssh_key field.

As a result, the determine_auth_method function in src/jump/chain/auth.rs uses the same key_path (derived from the cluster/defaults ssh_key setting) for both jump hosts and destination nodes. Users cannot specify a different SSH private key for jump hosts vs destination nodes through config.yaml.

This is a significant limitation in environments where:

  • Jump/bastion hosts use a different key than internal destination nodes
  • Security policies require separate keys for gateway and target access
  • Multi-hop chains require different keys at each hop

The only current workaround is to load all required keys into an SSH agent, which is not always desirable or possible.

Proposed Solution

Support a structured object format for jump_host in config.yaml that includes an optional ssh_key field, while maintaining full backward compatibility with the existing string format.

Desired Config Syntax

New structured format:

clusters:
  internal:
    nodes:
      - host: internal1.private
      - host: internal2.private
    user: admin
    ssh_key: ~/.ssh/destination_key
    jump_host:
      host: bastion.example.com
      user: jumpuser
      port: 22
      ssh_key: ~/.ssh/jump_host_key

Existing string format (must continue to work):

clusters:
  internal:
    jump_host: jumpuser@bastion.example.com

Per-node override:

clusters:
  mixed:
    nodes:
      - host: node1.internal
        jump_host:
          host: bastion1.example.com
          ssh_key: ~/.ssh/bastion1_key
      - host: node2.internal
        jump_host: jumpuser@bastion2.example.com
    ssh_key: ~/.ssh/default_key

Implementation Plan

1. Add ssh_key field to JumpHost struct

File: src/jump/parser/host.rs

Add an ssh_key: Option<String> field to the JumpHost struct. The field should be None when parsed from the legacy string format and Some(path) when provided via the structured config format.

2. Create a JumpHostConfig enum for serde deserialization

File: src/config/types.rs

Introduce a new type that supports both string and structured formats using #[serde(untagged)]:

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum JumpHostConfig {
    /// Legacy string format: "[user@]hostname[:port]"
    Simple(String),
    /// Structured format with optional ssh_key
    Detailed {
        host: String,
        #[serde(default)]
        user: Option<String>,
        #[serde(default)]
        port: Option<u16>,
        #[serde(default)]
        ssh_key: Option<String>,
    },
}

3. Update config types to use JumpHostConfig

File: src/config/types.rs

Change the jump_host field type in Defaults, ClusterDefaults, and NodeConfig::Detailed from Option<String> to Option<JumpHostConfig>.

4. Update determine_auth_method to accept per-jump-host key path

File: src/jump/chain/auth.rs

Modify determine_auth_method to accept and prioritize a per-jump-host key path over the cluster-level key path. The priority should be:

  1. Jump host's own ssh_key (from structured config)
  2. Cluster/defaults ssh_key (existing behavior, used as fallback)
  3. SSH agent / default key discovery (existing behavior)

5. Update jump chain connection logic

File: src/jump/chain/tunnel.rs

Update connect_through_tunnel and related functions to pass the per-jump-host key information from JumpHost.ssh_key through to determine_auth_method.

6. Update config resolution and parsing

Ensure that when JumpHostConfig::Detailed is encountered, it produces a JumpHost with the ssh_key field populated. When JumpHostConfig::Simple is encountered, the existing string parsing produces a JumpHost with ssh_key: None.

7. Update documentation and examples

File: example-config.yaml

Add examples showing both the legacy string format and the new structured format with ssh_key.

Acceptance Criteria

  • JumpHost struct has an ssh_key: Option<String> field
  • Config YAML supports structured jump_host with host, user, port, ssh_key fields
  • Config YAML continues to support legacy jump_host: "user@host:port" string format
  • #[serde(untagged)] enum correctly deserializes both formats
  • determine_auth_method uses jump host's ssh_key when provided, falling back to cluster ssh_key
  • Per-node jump_host structured format works in NodeConfig::Detailed
  • Global defaults jump_host structured format works in Defaults
  • Cluster-level jump_host structured format works in ClusterDefaults
  • Environment variable expansion works in jump host ssh_key paths (e.g., $HOME/.ssh/key)
  • example-config.yaml includes examples of the new syntax
  • Existing tests continue to pass (backward compatibility)
  • New unit tests cover structured jump_host deserialization
  • New unit tests cover per-jump-host key path resolution in determine_auth_method

Technical Considerations

  • Serde untagged ordering: The Detailed variant must be listed before Simple in the enum to ensure serde tries the structured format first. If a YAML object is provided, it should match Detailed; if a plain string is provided, it should match Simple.
  • Path expansion: The ssh_key field in the jump host config should support the same ~ and $HOME expansion as the existing ssh_key fields in cluster defaults.
  • Multi-hop chains: If multi-hop jump chains are supported (comma-separated ProxyJump syntax), consider how per-hop keys would work. The structured format naturally supports this if each hop is specified separately.
  • Backward compatibility: All existing config files must continue to work without modification.
  • Display / to_connection_string: The JumpHost::to_connection_string() method should not include the ssh_key path for security reasons (avoid logging key paths).

Related Issues

Files to Modify

File Change
src/jump/parser/host.rs Add ssh_key field to JumpHost
src/config/types.rs Add JumpHostConfig enum, update Defaults, ClusterDefaults, NodeConfig
src/jump/chain/auth.rs Accept per-jump-host key path in determine_auth_method
src/jump/chain/tunnel.rs Pass per-jump-host key to auth functions
example-config.yaml Add structured jump_host examples
Config resolution code Convert JumpHostConfig to JumpHost with ssh_key populated

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions