Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/client/keyagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,17 @@ func NewLocalAgent(conf LocalAgentConfig) (a *LocalKeyAgent, err error) {

if shouldAddKeysToAgent(conf.KeysOption) {
a.log.DebugContext(context.Background(), "Connecting to the system agent")
// Check whether the system agent is reachable
// and close the connection right away
systemAgent, err := sshagent.NewSystemAgentClient()
if err != nil {
a.log.WarnContext(context.Background(), "Unable to connect to system agent", "error", err)
} else {
a.systemAgent = systemAgent
if err := systemAgent.Close(); err != nil {
a.log.DebugContext(context.Background(), "Failed to close connection to system agent", "error", err)
}
// Build a client that connects to the system agent for each request.
a.systemAgent = sshagent.NewSingleRequestClient(sshagent.NewSystemAgentClient)
}
} else {
log.DebugContext(context.Background(), "Skipping connection to the local ssh-agent.")
Expand Down
85 changes: 63 additions & 22 deletions lib/client/keyagent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"path/filepath"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -63,6 +64,10 @@ type KeyAgentTestSuite struct {
clusterName string
tlsca *tlsca.CertAuthority
tlscaCert authclient.TrustedCerts

// openSystemAgentConns is the number of client connections currently
// open to the agent served on $SSH_AUTH_SOCK.
openSystemAgentConns *atomic.Int32
}

type keyAgentTestSuiteFunc func(opt *keyAgentTestSuiteOpt)
Expand Down Expand Up @@ -96,14 +101,15 @@ func makeSuite(t *testing.T, opts ...keyAgentTestSuiteFunc) *KeyAgentTestSuite {
o(&settings)
}

err := startDebugAgent(t)
openSystemAgentConns, err := startDebugAgent(t)
require.NoError(t, err)

s := &KeyAgentTestSuite{
keyDir: t.TempDir(),
username: "foo",
hostname: settings.hostname,
clusterName: settings.clusterName,
keyDir: t.TempDir(),
username: "foo",
hostname: settings.hostname,
clusterName: settings.clusterName,
openSystemAgentConns: openSystemAgentConns,
}

pemBytes, ok := fixtures.PEMBytes["rsa"]
Expand Down Expand Up @@ -183,6 +189,40 @@ func TestAddKey(t *testing.T) {

}

// TestSystemAgentConnections ensures that a LocalKeyAgent does not hold
// connections to the system agent open.
func TestSystemAgentConnections(t *testing.T) {
s := makeSuite(t)

requireNoOpenConns := func(t *testing.T) {
t.Helper()
require.EventuallyWithT(t, func(t *assert.CollectT) {
assert.Zero(t, s.openSystemAgentConns.Load())
}, 5*time.Second, 10*time.Millisecond, "connections to the system agent were leaked")
}

// Creating many key agents must not accumulate connections to the system agent.
var keyAgents []*LocalKeyAgent
for range 10 {
keyAgent := s.newKeyAgent(t)
require.NotNil(t, keyAgent.systemAgent, "expected the key agent to use the system agent")
keyAgents = append(keyAgents, keyAgent)
}
requireNoOpenConns(t)

for _, keyAgent := range keyAgents {
require.NoError(t, keyAgent.AddKeyRing(s.keyRing))
requireNoOpenConns(t)

_, err := keyAgent.Signers()
require.NoError(t, err)
requireNoOpenConns(t)

require.NoError(t, keyAgent.UnloadKeyRing(s.keyRing.KeyRingIndex))
requireNoOpenConns(t)
}
}

// TestLoadKey ensures correct loading of a key into an agent. This test
// checks the following:
// - Loading a key multiple times overwrites the same key.
Expand Down Expand Up @@ -812,7 +852,10 @@ func (s *KeyAgentTestSuite) makeKeyRing(t *testing.T, username, proxyHost string
}
}

func startDebugAgent(t *testing.T) error {
// startDebugAgent serves an in-memory keyring over $SSH_AUTH_SOCK, mimicking
// the system agent. It returns an atomic integer tracking the number of client
// connections that are currently open to that agent.
func startDebugAgent(t *testing.T) (openConns *atomic.Int32, err error) {
// Create own tmp dir instead of using t.TmpDir
// because net.Listen("unix", path) has dir path length limitation
tempDir, err := os.MkdirTemp("", "teleport-test")
Expand All @@ -824,18 +867,18 @@ func startDebugAgent(t *testing.T) error {
socketpath := filepath.Join(tempDir, "agent.sock")
listener, err := net.Listen("unix", socketpath)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}

systemAgent := agent.NewKeyring()
t.Setenv(teleport.SSHAuthSock, socketpath)

openConns = new(atomic.Int32)
startedC := make(chan struct{})
doneC := make(chan struct{})

var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
wg.Go(func() {
// agent is listening and environment variable is set, unblock now
close(startedC)
for {
Expand All @@ -846,24 +889,22 @@ func startDebugAgent(t *testing.T) error {
}
return
}
wg.Add(2)
go func() {
openConns.Add(1)
wg.Go(func() {
agent.ServeAgent(systemAgent, conn)
wg.Done()
}()
go func() {
openConns.Add(-1)
})
wg.Go(func() {
<-doneC
conn.Close()
wg.Done()
}()
})
}
}()
})

go func() {
wg.Go(func() {
<-doneC
listener.Close()
wg.Done()
}()
})

t.Cleanup(func() {
close(doneC)
Expand All @@ -872,7 +913,7 @@ func startDebugAgent(t *testing.T) error {

// block until agent is started
<-startedC
return nil
return openConns, nil
}

func (s *KeyAgentTestSuite) newKeyAgent(t *testing.T) *LocalKeyAgent {
Expand Down
157 changes: 157 additions & 0 deletions lib/sshagent/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package sshagent

import (
"bytes"
"context"
"errors"
"io"
Expand Down Expand Up @@ -89,6 +90,162 @@ func (c *client) Close() error {
return trace.Wrap(err)
}

// NewSingleRequestClient returns a client that opens a new connection for each request
// and closes it once the request completes.
//
// The returned agent is safe for concurrent use as long as getClient is.
func NewSingleRequestClient(getClient ClientGetter) agent.ExtendedAgent {
return singleRequestClient{getClient: getClient}
}

type singleRequestClient struct {
getClient ClientGetter
}

// withSingleRequestClient opens a new agent connection, runs fn against it,
// and closes the connection.
func withSingleRequestClient[T any](getClient ClientGetter, fn func(Client) (T, error)) (T, error) {
agentClient, err := getClient()
if err != nil {
var zero T
return zero, trace.Wrap(err)
}
defer agentClient.Close()

out, err := fn(agentClient)
return out, trace.Wrap(err)
}

// doWithSingleRequestClient is [withSingleRequestClient] for requests with no return value.
func doWithSingleRequestClient(getClient ClientGetter, fn func(Client) error) error {
_, err := withSingleRequestClient(getClient, func(agentClient Client) (struct{}, error) {
return struct{}{}, fn(agentClient)
})
return trace.Wrap(err)
}

func (s singleRequestClient) List() ([]*agent.Key, error) {
return withSingleRequestClient(s.getClient, func(agentClient Client) ([]*agent.Key, error) {
return agentClient.List()
})
}

func (s singleRequestClient) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
return withSingleRequestClient(s.getClient, func(agentClient Client) (*ssh.Signature, error) {
return agentClient.Sign(key, data)
})
}

func (s singleRequestClient) SignWithFlags(key ssh.PublicKey, data []byte, flags agent.SignatureFlags) (*ssh.Signature, error) {
return withSingleRequestClient(s.getClient, func(agentClient Client) (*ssh.Signature, error) {
return agentClient.SignWithFlags(key, data, flags)
})
}

func (s singleRequestClient) Add(key agent.AddedKey) error {
return doWithSingleRequestClient(s.getClient, func(agentClient Client) error {
return agentClient.Add(key)
})
}

func (s singleRequestClient) Remove(key ssh.PublicKey) error {
return doWithSingleRequestClient(s.getClient, func(agentClient Client) error {
return agentClient.Remove(key)
})
}

func (s singleRequestClient) RemoveAll() error {
return doWithSingleRequestClient(s.getClient, func(agentClient Client) error {
return agentClient.RemoveAll()
})
}

func (s singleRequestClient) Lock(passphrase []byte) error {
return doWithSingleRequestClient(s.getClient, func(agentClient Client) error {
return agentClient.Lock(passphrase)
})
}

func (s singleRequestClient) Unlock(passphrase []byte) error {
return doWithSingleRequestClient(s.getClient, func(agentClient Client) error {
return agentClient.Unlock(passphrase)
})
}

func (s singleRequestClient) Extension(extensionType string, contents []byte) ([]byte, error) {
return withSingleRequestClient(s.getClient, func(agentClient Client) ([]byte, error) {
return agentClient.Extension(extensionType, contents)
})
}

// Signers returns signers for all the keys currently known to the agent.
// The signers do not hold a connection open, they connect to the agent on demand
// for each signature.
func (s singleRequestClient) Signers() ([]ssh.Signer, error) {
keys, err := s.List()
if err != nil {
return nil, trace.Wrap(err)
}

signers := make([]ssh.Signer, 0, len(keys))
for _, key := range keys {
signers = append(signers, singleRequestSigner{getClient: s.getClient, pub: key})
}
return signers, nil
}

// singleRequestSigner is a signer for a key held by an agent that
// opens a new connection for each signature request.
type singleRequestSigner struct {
getClient ClientGetter
pub ssh.PublicKey
}

var _ ssh.AlgorithmSigner = singleRequestSigner{}

func (s singleRequestSigner) PublicKey() ssh.PublicKey {
return s.pub
}

func (s singleRequestSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
// Note: the agent has its own entropy source, so the rand argument is ignored.
return withSingleRequestClient(s.getClient, func(agentClient Client) (*ssh.Signature, error) {
return agentClient.Sign(s.pub, data)
})
}

func (s singleRequestSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) {
return withSingleRequestClient(s.getClient, func(agentClient Client) (*ssh.Signature, error) {
signer, err := agentSigner(agentClient, s.pub)
if err != nil {
return nil, trace.Wrap(err)
}
return signer.SignWithAlgorithm(rand, data, algorithm)
})
}

// agentSigner returns the agent's own signer for the given public key.
func agentSigner(agentClient Client, pub ssh.PublicKey) (ssh.AlgorithmSigner, error) {
signers, err := agentClient.Signers()
if err != nil {
return nil, trace.Wrap(err)
}

pubBytes := pub.Marshal()
for _, signer := range signers {
if !bytes.Equal(signer.PublicKey().Marshal(), pubBytes) {
continue
}
algorithmSigner, ok := signer.(ssh.AlgorithmSigner)
if !ok {
return nil, trace.NotImplemented("agent signer of type %T does not support signing with a specific algorithm", signer)
}
return algorithmSigner, nil
}

return nil, trace.NotFound("agent no longer holds the requested %v key", pub.Type())
}

const channelType = "auth-agent@openssh.com"

// ServeChannelRequests routes agent channel requests to a new agent
Expand Down
Loading
Loading