Skip to content

Release v2.4.0#29

Merged
tis24dev merged 2 commits into
mainfrom
dev
Jul 16, 2026
Merged

Release v2.4.0#29
tis24dev merged 2 commits into
mainfrom
dev

Conversation

@tis24dev

@tis24dev tis24dev commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Automated release PR for v2.4.0.

Summary by Sourcery

Introduce SSH key-based authentication across configuration, migration, and SSH client layers, including path resolution and validation, while enforcing safety around database applies and expanding test coverage for the new auth behavior.

New Features:

  • Add SSH private key authentication support alongside password-based SSH for source and destination hosts
  • Support resolving relative ssh_key_path values against the configuration file directory for host configs

Enhancements:

  • Expose non-sensitive SSH auth method labels in configuration and client logging for better diagnostics
  • Ensure SSH dial and redial reuse the same parsed credential so reconnects cannot silently change authentication method

Tests:

  • Add comprehensive tests for SSH authentication selection, private key handling, and symlinked key paths
  • Add tests verifying key-auth redial parity and key-authenticated destination dialing
  • Add tests ensuring config validation for auth methods and guarding DB apply from key-authenticated sources

Summary by CodeRabbit

  • New Features

    • Added SSH private-key authentication, including optional passphrases.
    • Added support for resolving relative key paths from configuration locations.
    • Authentication methods are identified clearly without exposing credentials.
    • Key-based authentication remains available during SSH reconnections.
  • Bug Fixes

    • Added validation to prevent conflicting or incomplete authentication settings.
    • Database apply operations now stop early when the source uses key authentication and required credentials are unavailable.
    • Improved handling of invalid, missing, or protected key files without leaking secrets.

Greptile Summary

This PR adds SSH private-key authentication throughout configuration, migration, and SSH connection handling. The main changes are:

  • Supports plaintext and passphrase-protected SSH private keys.
  • Resolves relative key paths from the configuration directory.
  • Reuses parsed credentials for initial connections and redials.
  • Supports key authentication for destination hosts.
  • Rejects database applies when key-only source authentication cannot supply the database credential.
  • Adds coverage for auth validation, key loading, symlinks, dialing, and redials.

Confidence Score: 5/5

This looks safe to merge.

  • The updated auth checks reject missing and conflicting credentials.
  • Initial dials and redials use the same parsed credential.
  • Destination setup closes the source connection when destination authentication or dialing fails.
  • The database guard runs after scope resolution and before network or write operations.
  • No blocking issue remains in the changed code.

Important Files Changed

Filename Overview
internal/config/config.go Adds key-auth fields, relative-path resolution, auth validation, and key-aware destination detection.
internal/sshx/auth.go Adds password and private-key credential construction with encrypted-key support.
internal/sshx/client.go Stores the selected credential so redials preserve the initial authentication method.
internal/sshx/retry.go Builds retry connections from the shared authentication abstraction.
internal/sshx/pool.go Builds source and destination credentials and supports key-authenticated destinations.
internal/migrate/runner.go Stops database applies before dialing when a key-only source cannot provide the required database credential.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Config as Configuration
    participant Pool as SSH Pool
    participant Auth as Authentication
    participant Client as SSH Client
    Config->>Config: Resolve key paths and validate auth
    Pool->>Auth: Build password or private-key credential
    Auth-->>Pool: Parsed credential
    Pool->>Client: Initial dial
    Client->>Client: Store credential
    Client->>Client: Reuse credential on redial
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Config as Configuration
    participant Pool as SSH Pool
    participant Auth as Authentication
    participant Client as SSH Client
    Config->>Config: Resolve key paths and validate auth
    Pool->>Auth: Build password or private-key credential
    Auth-->>Pool: Parsed credential
    Pool->>Client: Initial dial
    Client->>Client: Store credential
    Client->>Client: Reuse credential on redial
Loading

Reviews (2): Last reviewed commit: "fix(sshx): reject ambiguous SSH auth con..." | Re-trigger Greptile

Introduce an Authentication value built once per host and reused for the
initial dial and every self-heal redial, so a reconnect never falls back to
a different method or re-reads the key. A host may now authenticate with a
private key (plaintext or passphrase-encrypted) via ssh_key_path/
ssh_key_passphrase in addition to a password.

- internal/sshx/auth.go: Authentication, PasswordAuth, PrivateKeyAuth, and
  newClientConfig (the single *ssh.ClientConfig builder). The key is read
  via filepath.EvalSymlinks + os.Root containment (openContained), so a
  symlinked key works and the read needs no #nosec. The signer is parsed
  once; encrypted-no-passphrase and wrong-passphrase map to literal errors
  that never echo key bytes or the passphrase.
- internal/sshx/{client,retry,pool}.go: stash the Authentication on the
  Client and rebuild the redial config from it; DialBoth/DialDest select
  the method with AuthFromHost.
- internal/config: HostConfig gains ssh_key_path/ssh_key_passphrase;
  validate enforces exactly one of password/key (passphrase requires a
  key); relative key paths resolve against the config-file directory; debug
  logs use a non-secret auth label only.
- internal/migrate: reject a database apply from a key-authenticated source
  fail-fast (the source MySQL credential derives from the source ssh_pass,
  which a key source cannot provide); file/mail and dry-run runs are
  unaffected.

Tests cover plaintext/encrypted keys, wrong/missing passphrase, no secret
leak in errors, symlinked keys, dial/redial parity against a
password-rejecting server (key deleted after the initial dial), a
key-authenticated destination, and the DB-apply guard wired into Run.
@sourcery-ai

sourcery-ai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

This release adds SSH private key authentication support alongside passwords, wires it into config loading and the SSH client/pool, and introduces guards and tests around key usage and database migration behavior, ensuring dial/redial parity and non-leaky error handling.

Sequence diagram for SSH client dial using Authentication

sequenceDiagram
    actor Operator
    participant Config as config.Load
    participant Pool as sshx.DialBoth
    participant Auth as sshx.AuthFromHost
    participant Dial as sshx.Dial
    participant NewCfg as sshx.newClientConfig
    participant SSH as dialAttempts

    Operator->>Config: Load(path)
    Config->>Config: resolveKeyPath for SSHKeyPath
    Config->>Config: HostConfig.validate("src"/"dest")

    Config-->>Pool: Config with HostConfig

    Pool->>Auth: AuthFromHost(cfg.Src)
    Auth-->>Pool: Authentication (password or private_key)

    Pool->>Dial: Dial(ctx, "source", addr, cfg.Src.SSHUser, Authentication,...)
    Dial->>NewCfg: newClientConfig(user, Authentication, hostKeyCB, timeout)
    NewCfg-->>Dial: *ssh.ClientConfig
    Dial->>SSH: dialAttempts(ctx, name, addr, timeout, ClientConfig)
    SSH-->>Dial: *ssh.Client
    Dial-->>Pool: *Client with auth stored

    Note over Pool,Client: Client.heal uses same Authentication via newClientConfig for redial
Loading

File-Level Changes

Change Details Files
Extend HostConfig and config loading/validation to support SSH private key auth, including relative key path resolution and destination-auth detection.
  • Add SSHKeyPath and SSHKeyPassphrase fields plus hasAuth/AuthMethod helpers on HostConfig
  • Resolve relative ssh_key_path values against the config file directory during Load
  • Update validation to enforce exactly one auth method (password or key) and consistent passphrase usage
  • Adjust DestConfigured/destIntended and debug logging to account for key-based authentication
internal/config/config.go
internal/config/auth_test.go
Introduce an Authentication abstraction for SSH, with password and private-key implementations, and use it throughout the SSH client and pool (including redials).
  • Add Authentication type with PasswordAuth/PrivateKeyAuth/AuthFromHost/newClientConfig helpers
  • Ensure private key files are read securely, support encrypted keys, and avoid secrets in error messages
  • Change Client to store Authentication instead of a raw password and build configs via newClientConfig
  • Update Dial/dialWithRetry and pool DialBoth/DialDest to construct Authentication from HostConfig
internal/sshx/auth.go
internal/sshx/client.go
internal/sshx/retry.go
internal/sshx/pool.go
Add guards and tests to prevent unsupported database apply flows from key-authenticated sources and to validate key-based dest connectivity and redial behavior.
  • Add validateSourceAuthForDBApply and invoke it early in Run to reject DB apply when source uses key auth
  • Add tests covering the guard behavior in Run and validateSourceAuthForDBApply
  • Add key-auth dial/redial tests including encrypted keys, DialDest with key auth, and unauthorized key rejection
internal/migrate/runner.go
internal/migrate/auth_guard_test.go
internal/sshx/auth_redial_test.go
Update existing SSH dial/keepalive tests and helpers to use the new Authentication API and expand auth-unit tests.
  • Replace password string arguments to Dial with PasswordAuth in all sshx tests
  • Add unit tests for Authentication methods, PrivateKeyAuth behavior (including symlinks and error hygiene), and AuthFromHost selection
  • Adjust sshtest helpers to construct clients with Authentication
internal/sshx/retry_dial_test.go
internal/sshx/client_dial_test.go
internal/sshx/client_redial_test.go
internal/sshx/client_run_test.go
internal/sshx/pr2_regression_test.go
internal/sshx/stream_bridge_test.go
internal/sshtest/sshtest.go
internal/sshx/auth_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

SSH configuration and connection handling now support private-key authentication alongside passwords. Keys are resolved and parsed once, reused during redials, and selected for source and destination connections. Database apply rejects key-authenticated sources before dialing.

SSH key authentication

Layer / File(s) Summary
Configuration authentication contract
internal/config/config.go, internal/config/auth_test.go
HostConfig supports key paths and passphrases, validates exactly one authentication method, resolves relative paths, and recognizes key-only destinations.
Authentication construction and parsing
internal/sshx/auth.go, internal/sshx/auth_test.go
Password and private-key credentials are represented by Authentication; private keys are parsed once, labeled without secrets, and selected from host configuration.
Dialing, redialing, and pool integration
internal/sshx/client.go, internal/sshx/retry.go, internal/sshx/pool.go, internal/sshtest/sshtest.go, internal/sshx/*_test.go
Dial and retry paths retain authentication objects, redials reuse cached key material, and source/destination pools derive configured authentication.
Database-apply authentication guard
internal/migrate/runner.go, internal/migrate/auth_guard_test.go
Migration runs reject database apply from key-authenticated sources before dialing or writing artifacts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Config
  participant AuthResolver
  participant SSHClient
  participant SSHServer
  Config->>AuthResolver: provide key or password settings
  AuthResolver-->>SSHClient: return Authentication
  SSHClient->>SSHServer: dial using selected authentication
  SSHServer-->>SSHClient: establish SSH connection
  SSHClient->>SSHServer: redial with cached authentication
  SSHServer-->>SSHClient: reestablish connection
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is a generic release label and does not describe the main change in the PR. Rename it to highlight the primary change, e.g. adding private-key SSH authentication and related config support.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.96296% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/sshx/pool.go 46.15% 4 Missing and 3 partials ⚠️
internal/sshx/auth.go 90.19% 3 Missing and 2 partials ⚠️
internal/sshtest/sshtest.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • Several tests assert exact error strings (e.g., in PrivateKeyAuth and validateSourceAuthForDBApply); consider matching via substring or sentinel error values to avoid brittleness if wording changes.
  • validateSourceAuthForDBApply currently checks cfg.Src.SSHKeyPath directly; using cfg.Src.AuthMethod() or a shared helper for “is key-auth” would keep DB-auth logic consistent with the rest of the config/auth selection code.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several tests assert exact error strings (e.g., in PrivateKeyAuth and validateSourceAuthForDBApply); consider matching via substring or sentinel error values to avoid brittleness if wording changes.
- validateSourceAuthForDBApply currently checks cfg.Src.SSHKeyPath directly; using cfg.Src.AuthMethod() or a shared helper for “is key-auth” would keep DB-auth logic consistent with the rest of the config/auth selection code.

## Individual Comments

### Comment 1
<location path="internal/config/auth_test.go" line_range="48-68" />
<code_context>
+	}
+}
+
+func TestLoad_RelativeKeyPathResolvedAgainstConfigDir(t *testing.T) {
+	dir := t.TempDir()
+	yaml := "src:\n" +
+		"  ip: 1.2.3.4\n" +
+		"  port: 22\n" +
+		"  ssh_user: u\n" +
+		"  ssh_key_path: keys/id_ed25519\n" +
+		"  timeout: 10s\n"
+	cfgPath := filepath.Join(dir, "host.yaml")
+	if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
+		t.Fatal(err)
+	}
+	cfg, err := Load(cfgPath)
+	if err != nil {
+		t.Fatalf("Load: %v", err)
+	}
+	want := filepath.Join(dir, "keys", "id_ed25519")
+	if cfg.Src.SSHKeyPath != want {
+		t.Errorf("SSHKeyPath = %q, want %q (resolved against the config dir)", cfg.Src.SSHKeyPath, want)
+	}
</code_context>
<issue_to_address>
**suggestion (testing):** Extend Load key-path tests to cover destination resolution and the empty-key case.

`Load` now resolves both `Src` and `Dest` via `resolveKeyPath`. Please also add: (1) a test for `Dest.SSHKeyPath` to verify it’s resolved against the config directory, and (2) a test that confirms an empty `ssh_key_path` remains empty. This will fully exercise `resolveKeyPath` and guard against regressions affecting the destination host or empty-key handling.

```suggestion
func TestLoad_RelativeKeyPathResolvedAgainstConfigDir(t *testing.T) {
	dir := t.TempDir()
	yaml := "src:\n" +
		"  ip: 1.2.3.4\n" +
		"  port: 22\n" +
		"  ssh_user: u\n" +
		"  ssh_key_path: keys/id_ed25519\n" +
		"  timeout: 10s\n"
	cfgPath := filepath.Join(dir, "host.yaml")
	if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := Load(cfgPath)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	want := filepath.Join(dir, "keys", "id_ed25519")
	if cfg.Src.SSHKeyPath != want {
		t.Errorf("SSHKeyPath = %q, want %q (resolved against the config dir)", cfg.Src.SSHKeyPath, want)
	}
}

func TestLoad_RelativeDestKeyPathResolvedAgainstConfigDir(t *testing.T) {
	dir := t.TempDir()
	yaml := "src:\n" +
		"  ip: 1.2.3.4\n" +
		"  port: 22\n" +
		"  ssh_user: srcuser\n" +
		"  ssh_key_path: keys/src_id_ed25519\n" +
		"  timeout: 10s\n" +
		"dest:\n" +
		"  ip: 5.6.7.8\n" +
		"  port: 22\n" +
		"  ssh_user: destuser\n" +
		"  ssh_key_path: keys/dest_id_ed25519\n" +
		"  timeout: 10s\n"
	cfgPath := filepath.Join(dir, "host.yaml")
	if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := Load(cfgPath)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	want := filepath.Join(dir, "keys", "dest_id_ed25519")
	if cfg.Dest.SSHKeyPath != want {
		t.Errorf("Dest.SSHKeyPath = %q, want %q (resolved against the config dir)", cfg.Dest.SSHKeyPath, want)
	}
}

func TestLoad_EmptySSHKeyPathRemainsEmpty(t *testing.T) {
	dir := t.TempDir()
	yaml := "src:\n" +
		"  ip: 1.2.3.4\n" +
		"  port: 22\n" +
		"  ssh_user: srcuser\n" +
		"  ssh_key_path: \"\"\n" +
		"  timeout: 10s\n" +
		"dest:\n" +
		"  ip: 5.6.7.8\n" +
		"  port: 22\n" +
		"  ssh_user: destuser\n" +
		"  ssh_key_path: \"\"\n" +
		"  timeout: 10s\n"
	cfgPath := filepath.Join(dir, "host_empty_key.yaml")
	if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := Load(cfgPath)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	if cfg.Src.SSHKeyPath != "" {
		t.Errorf("Src.SSHKeyPath = %q, want empty string", cfg.Src.SSHKeyPath)
	}
	if cfg.Dest.SSHKeyPath != "" {
		t.Errorf("Dest.SSHKeyPath = %q, want empty string", cfg.Dest.SSHKeyPath)
	}
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +48 to +68
func TestLoad_RelativeKeyPathResolvedAgainstConfigDir(t *testing.T) {
dir := t.TempDir()
yaml := "src:\n" +
" ip: 1.2.3.4\n" +
" port: 22\n" +
" ssh_user: u\n" +
" ssh_key_path: keys/id_ed25519\n" +
" timeout: 10s\n"
cfgPath := filepath.Join(dir, "host.yaml")
if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
t.Fatal(err)
}
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("Load: %v", err)
}
want := filepath.Join(dir, "keys", "id_ed25519")
if cfg.Src.SSHKeyPath != want {
t.Errorf("SSHKeyPath = %q, want %q (resolved against the config dir)", cfg.Src.SSHKeyPath, want)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Extend Load key-path tests to cover destination resolution and the empty-key case.

Load now resolves both Src and Dest via resolveKeyPath. Please also add: (1) a test for Dest.SSHKeyPath to verify it’s resolved against the config directory, and (2) a test that confirms an empty ssh_key_path remains empty. This will fully exercise resolveKeyPath and guard against regressions affecting the destination host or empty-key handling.

Suggested change
func TestLoad_RelativeKeyPathResolvedAgainstConfigDir(t *testing.T) {
dir := t.TempDir()
yaml := "src:\n" +
" ip: 1.2.3.4\n" +
" port: 22\n" +
" ssh_user: u\n" +
" ssh_key_path: keys/id_ed25519\n" +
" timeout: 10s\n"
cfgPath := filepath.Join(dir, "host.yaml")
if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
t.Fatal(err)
}
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("Load: %v", err)
}
want := filepath.Join(dir, "keys", "id_ed25519")
if cfg.Src.SSHKeyPath != want {
t.Errorf("SSHKeyPath = %q, want %q (resolved against the config dir)", cfg.Src.SSHKeyPath, want)
}
}
func TestLoad_RelativeKeyPathResolvedAgainstConfigDir(t *testing.T) {
dir := t.TempDir()
yaml := "src:\n" +
" ip: 1.2.3.4\n" +
" port: 22\n" +
" ssh_user: u\n" +
" ssh_key_path: keys/id_ed25519\n" +
" timeout: 10s\n"
cfgPath := filepath.Join(dir, "host.yaml")
if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
t.Fatal(err)
}
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("Load: %v", err)
}
want := filepath.Join(dir, "keys", "id_ed25519")
if cfg.Src.SSHKeyPath != want {
t.Errorf("SSHKeyPath = %q, want %q (resolved against the config dir)", cfg.Src.SSHKeyPath, want)
}
}
func TestLoad_RelativeDestKeyPathResolvedAgainstConfigDir(t *testing.T) {
dir := t.TempDir()
yaml := "src:\n" +
" ip: 1.2.3.4\n" +
" port: 22\n" +
" ssh_user: srcuser\n" +
" ssh_key_path: keys/src_id_ed25519\n" +
" timeout: 10s\n" +
"dest:\n" +
" ip: 5.6.7.8\n" +
" port: 22\n" +
" ssh_user: destuser\n" +
" ssh_key_path: keys/dest_id_ed25519\n" +
" timeout: 10s\n"
cfgPath := filepath.Join(dir, "host.yaml")
if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
t.Fatal(err)
}
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("Load: %v", err)
}
want := filepath.Join(dir, "keys", "dest_id_ed25519")
if cfg.Dest.SSHKeyPath != want {
t.Errorf("Dest.SSHKeyPath = %q, want %q (resolved against the config dir)", cfg.Dest.SSHKeyPath, want)
}
}
func TestLoad_EmptySSHKeyPathRemainsEmpty(t *testing.T) {
dir := t.TempDir()
yaml := "src:\n" +
" ip: 1.2.3.4\n" +
" port: 22\n" +
" ssh_user: srcuser\n" +
" ssh_key_path: \"\"\n" +
" timeout: 10s\n" +
"dest:\n" +
" ip: 5.6.7.8\n" +
" port: 22\n" +
" ssh_user: destuser\n" +
" ssh_key_path: \"\"\n" +
" timeout: 10s\n"
cfgPath := filepath.Join(dir, "host_empty_key.yaml")
if err := os.WriteFile(cfgPath, []byte(yaml), 0600); err != nil {
t.Fatal(err)
}
cfg, err := Load(cfgPath)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Src.SSHKeyPath != "" {
t.Errorf("Src.SSHKeyPath = %q, want empty string", cfg.Src.SSHKeyPath)
}
if cfg.Dest.SSHKeyPath != "" {
t.Errorf("Dest.SSHKeyPath = %q, want empty string", cfg.Dest.SSHKeyPath)
}
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/sshx/auth.go`:
- Around line 96-108: Update AuthFromHost to validate authentication fields
before selecting a method: reject configurations that set both SSHKeyPath and
SSHPass, and reject configurations that provide SSHKeyPassphrase without
SSHKeyPath or alongside password authentication. Preserve the existing
PrivateKeyAuth, PasswordAuth, and missing-method behavior for valid
configurations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb787df7-0278-4c82-b478-e948d54821d3

📥 Commits

Reviewing files that changed from the base of the PR and between 285dad9 and 951af4d.

📒 Files selected for processing (17)
  • internal/config/auth_test.go
  • internal/config/config.go
  • internal/migrate/auth_guard_test.go
  • internal/migrate/runner.go
  • internal/sshtest/sshtest.go
  • internal/sshx/auth.go
  • internal/sshx/auth_redial_test.go
  • internal/sshx/auth_test.go
  • internal/sshx/client.go
  • internal/sshx/client_dial_test.go
  • internal/sshx/client_redial_test.go
  • internal/sshx/client_run_test.go
  • internal/sshx/pool.go
  • internal/sshx/pr2_regression_test.go
  • internal/sshx/retry.go
  • internal/sshx/retry_dial_test.go
  • internal/sshx/stream_bridge_test.go

Comment thread internal/sshx/auth.go Outdated
Address review feedback on the private-key auth change.

- AuthFromHost now rejects a HostConfig with both ssh_pass and ssh_key_path
  set, and a passphrase paired with a password (no key), mirroring
  config.validate. Production callers always go through config.Load (which
  already rejects these), so this is defense-in-depth for a HostConfig built
  programmatically without Load: it can no longer silently select the key when
  both methods are set, or silently drop a passphrase.
- Add tests: the destination ssh_key_path resolves against the config-file
  directory, and an empty ssh_key_path stays empty after Load.
@tis24dev
tis24dev merged commit e4b1cb0 into main Jul 16, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant