diff --git a/README.md b/README.md index 57b39bd..fa7d2a4 100644 --- a/README.md +++ b/README.md @@ -219,11 +219,15 @@ first, or pass `-o StrictHostKeyChecking=accept-new` after `--`. Derives the key, serves it from an SSH agent that runs inside the tool on a unix socket in a new private `0700` temporary directory, then runs the system `ssh` -with `-o IdentityAgent=` followed by the host and all remaining -arguments unchanged. The tool exits with `ssh`'s exit status and removes the -socket and directory on the way out. The private key is never written to disk. A -SIGINT, SIGTERM or SIGHUP ends `ssh` and still removes the socket and directory, -and the tool then exits with status 1 unless `ssh` reported one of its own. +with `-o IdentityAgent= -o IdentitiesOnly=no` followed by the host +and all remaining arguments unchanged. Without `IdentitiesOnly=no`, an +`IdentitiesOnly yes` in the user's `ssh` configuration keeps the derived key +from being offered. The agent holds no other key, so turning it off adds nothing +else to what `ssh` offers. The tool exits with `ssh`'s exit status and removes +the socket and directory on the way out. The private key is never written to +disk. A SIGINT, SIGTERM or SIGHUP ends `ssh` and still removes the socket and +directory, and the tool then exits with status 1 unless `ssh` reported one of +its own. ## age identities: `keyfunc age` diff --git a/internal/cli/ssh/to.go b/internal/cli/ssh/to.go index c165796..33a039a 100644 --- a/internal/cli/ssh/to.go +++ b/internal/cli/ssh/to.go @@ -57,8 +57,13 @@ func to() *cobra.Command { defer served.Stop() + // IdentitiesOnly yes in the user's ssh configuration + // would keep the agent's key from being offered. The + // agent holds only the derived key, so turning it off + // adds no other key. argv := slices.Concat([]string{ "-o", "IdentityAgent=" + served.Socket(), + "-o", "IdentitiesOnly=no", }, args) return connect(ctx, argv) diff --git a/internal/cli/ssh_test.go b/internal/cli/ssh_test.go index 3aa8ef8..a669286 100644 --- a/internal/cli/ssh_test.go +++ b/internal/cli/ssh_test.go @@ -587,7 +587,7 @@ func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) { given := recorded(t, arguments) require.Equal(t, "-o", given[0]) - require.Equal(t, []string{host, remoteCommand}, given[2:]) + require.Equal(t, []string{host, remoteCommand}, given[4:]) // The stand-in wrote the path down only because there really was // a socket there while it ran. @@ -596,6 +596,26 @@ func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) { require.NoDirExists(t, filepath.Dir(socket)) } +func TestIdentitiesOnlyIsTurnedOffBeforeTheHost(t *testing.T) { + t.Setenv(mnemonic.Variable, example()) + + arguments, _ := pretendCall(t) + + _, err := execute(t, subcommand, "to", host, remoteCommand) + + var passed ssh.StatusError + + require.ErrorAs(t, err, &passed) + + // IdentitiesOnly yes in the user's ssh configuration keeps ssh + // from offering the agent's key. An option given before the host + // wins over the configuration. + given := recorded(t, arguments) + require.Equal(t, + []string{"-o", "IdentitiesOnly=no", host, remoteCommand}, given[2:], + ) +} + func TestTheToolEndsWithTheStatusSSHEndedWith(t *testing.T) { t.Setenv(mnemonic.Variable, example())