From 1d6f5b87d5f6dc93f2550476f32305a5d8b09a7f Mon Sep 17 00:00:00 2001 From: kavix Date: Sat, 13 Jun 2026 01:50:41 +0530 Subject: [PATCH] feat(ssh): add --confirm flag to require agent confirmation --- command/ssh/certificate.go | 7 ++++++- command/ssh/login.go | 12 ++++++++++-- command/ssh/proxycommand.go | 7 ++++++- command/ssh/ssh.go | 11 ++++++++++- internal/sshutil/agent.go | 20 +++++++++++++++----- internal/sshutil/agent_test.go | 21 +++++++++++++++++++++ 6 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 internal/sshutil/agent_test.go diff --git a/command/ssh/certificate.go b/command/ssh/certificate.go index ebccd787e..7a9efe847 100644 --- a/command/ssh/certificate.go +++ b/command/ssh/certificate.go @@ -190,6 +190,7 @@ $ step ssh certificate --kty OKP --curve Ed25519 mariano@work id_ed25519 sshPrivateKeyFlag, sshProvisionerPasswordFlag, sshSignFlag, + sshConfirmFlag, flags.KTY, flags.Curve, flags.Size, @@ -531,7 +532,11 @@ func certificateAction(ctx *cli.Context) error { ui.Printf(`{{ "%s" | red }} {{ "SSH Agent:" | bold }} %v`+"\n", ui.IconBad, err) } else { defer agent.Close() - if err := agent.AddCertificate(comment, resp.Certificate.Certificate, priv); err != nil { + var opts []sshutil.AgentOption + if ctx.Bool("confirm") { + opts = append(opts, sshutil.WithConfirmBeforeUse()) + } + if err := agent.AddCertificate(comment, resp.Certificate.Certificate, priv, opts...); err != nil { ui.Printf(`{{ "%s" | red }} {{ "SSH Agent:" | bold }} %v`+"\n", ui.IconBad, err) } else { ui.PrintSelected("SSH Agent", "yes") diff --git a/command/ssh/login.go b/command/ssh/login.go index 8eda7acdf..952391ce0 100644 --- a/command/ssh/login.go +++ b/command/ssh/login.go @@ -107,6 +107,7 @@ $ step ssh certificate --kty OKP --curve Ed25519 mariano@work id_ed25519 flags.Curve, flags.Size, flags.Insecure, + sshConfirmFlag, }, } } @@ -283,20 +284,27 @@ func loginAction(ctx *cli.Context) error { } // Attempt to add key to agent if private key defined. - if err := agent.AddCertificate(comment, resp.Certificate.Certificate, priv); err != nil { + var agentOpts []sshutil.AgentOption + if ctx.Bool("confirm") { + agentOpts = append(agentOpts, sshutil.WithConfirmBeforeUse()) + } + + if err := agent.AddCertificate(comment, resp.Certificate.Certificate, priv, agentOpts...); err != nil { ui.Printf(`{{ "%s" | red }} {{ "SSH Agent:" | bold }} %v`+"\n", ui.IconBad, err) } else { ui.PrintSelected("SSH Agent", "yes") } + if isAddUser { if resp.AddUserCertificate == nil { ui.Printf(`{{ "%s" | red }} {{ "Add User Certificate:" | bold }} failed to create a provisioner certificate`+"\n", ui.IconBad) - } else if err := agent.AddCertificate(comment, resp.AddUserCertificate.Certificate, auPriv); err != nil { + } else if err := agent.AddCertificate(comment, resp.AddUserCertificate.Certificate, auPriv, agentOpts...); err != nil { ui.Printf(`{{ "%s" | red }} {{ "Add User Certificate:" | bold }} %v`+"\n", ui.IconBad, err) } else { ui.PrintSelected("Add User Certificate", "yes") } } + return nil } diff --git a/command/ssh/proxycommand.go b/command/ssh/proxycommand.go index 0a0293b38..5aeaeca04 100644 --- a/command/ssh/proxycommand.go +++ b/command/ssh/proxycommand.go @@ -63,6 +63,7 @@ This command will add the user to the ssh-agent if necessary. flags.CaURL, flags.Root, flags.Context, + sshConfirmFlag, }, } } @@ -212,7 +213,11 @@ func doLoginIfNeeded(ctx *cli.Context, subject string) error { } // Add certificate and private key to agent - return agent.AddCertificate(subject, resp.Certificate.Certificate, priv) + var opts []sshutil.AgentOption + if ctx.Bool("confirm") { + opts = append(opts, sshutil.WithConfirmBeforeUse()) + } + return agent.AddCertificate(subject, resp.Certificate.Certificate, priv, opts...) } func getBastion(ctx *cli.Context, user, host string) (*api.SSHBastionResponse, error) { diff --git a/command/ssh/ssh.go b/command/ssh/ssh.go index 24a4e8d87..418866766 100644 --- a/command/ssh/ssh.go +++ b/command/ssh/ssh.go @@ -159,6 +159,11 @@ var ( Usage: `When signing an existing public key, use this flag to specify the corresponding private key so that the pair can be added to an SSH Agent.`, } + + sshConfirmFlag = cli.BoolFlag{ + Name: "confirm", + Usage: `Require user confirmation for every use of the certificate.`, + } ) func loginOnUnauthorized(ctx *cli.Context) (ca.RetryFunc, error) { @@ -244,7 +249,11 @@ func loginOnUnauthorized(ctx *cli.Context) (ca.RetryFunc, error) { // Add ssh certificate to the agent, ignore errors. if agent, err := sshutil.DialAgent(); err == nil { - agent.AddCertificate(jwt.Payload.Email, resp.Certificate.Certificate, priv) + var opts []sshutil.AgentOption + if ctx.Bool("confirm") { + opts = append(opts, sshutil.WithConfirmBeforeUse()) + } + agent.AddCertificate(jwt.Payload.Email, resp.Certificate.Certificate, priv, opts...) } return true diff --git a/internal/sshutil/agent.go b/internal/sshutil/agent.go index 604f1a9e7..3eef95b98 100644 --- a/internal/sshutil/agent.go +++ b/internal/sshutil/agent.go @@ -15,6 +15,7 @@ import ( type options struct { filterBySignatureKey func(*agent.Key) bool removeExpiredKey func(*Agent, *agent.Key) bool + confirmBeforeUse bool } func newOptions(opts []AgentOption) *options { @@ -28,6 +29,13 @@ func newOptions(opts []AgentOption) *options { // AgentOption is the type used for variadic options in Agent methods. type AgentOption func(o *options) +// WithConfirmBeforeUse requires user confirmation for every use of the key. +func WithConfirmBeforeUse() AgentOption { + return func(o *options) { + o.confirmBeforeUse = true + } +} + // WithSignatureKey filters certificate not signed by the given signing keys. func WithSignatureKey(keys []ssh.PublicKey) AgentOption { signingKeys := make([][]byte, len(keys)) @@ -242,7 +250,7 @@ func (a *Agent) RemoveAllKeys(opts ...AgentOption) (bool, error) { } // AddCertificate adds the given certificate to the agent. -func (a *Agent) AddCertificate(subject string, cert *ssh.Certificate, priv interface{}) error { +func (a *Agent) AddCertificate(subject string, cert *ssh.Certificate, priv interface{}, opts ...AgentOption) error { var ( lifetime uint64 now = cast.Uint64(time.Now().Unix()) @@ -262,10 +270,12 @@ func (a *Agent) AddCertificate(subject string, cert *ssh.Certificate, priv inter lifetime = 0 } + o := newOptions(opts) return errors.Wrap(a.Add(agent.AddedKey{ - PrivateKey: priv, - Certificate: cert, - Comment: subject, - LifetimeSecs: cast.Uint32(lifetime), + PrivateKey: priv, + Certificate: cert, + Comment: subject, + LifetimeSecs: cast.Uint32(lifetime), + ConfirmBeforeUse: o.confirmBeforeUse, }), "error adding key to agent") } diff --git a/internal/sshutil/agent_test.go b/internal/sshutil/agent_test.go new file mode 100644 index 000000000..80474beb7 --- /dev/null +++ b/internal/sshutil/agent_test.go @@ -0,0 +1,21 @@ +package sshutil + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_newOptions(t *testing.T) { + t.Run("default", func(t *testing.T) { + o := newOptions(nil) + require.False(t, o.confirmBeforeUse) + require.Nil(t, o.filterBySignatureKey) + require.Nil(t, o.removeExpiredKey) + }) + + t.Run("confirmBeforeUse", func(t *testing.T) { + o := newOptions([]AgentOption{WithConfirmBeforeUse()}) + require.True(t, o.confirmBeforeUse) + }) +}