diff --git a/go.mod b/go.mod index 3896a796865c6..ed2336173cf28 100644 --- a/go.mod +++ b/go.mod @@ -364,6 +364,9 @@ replace ( github.com/pkg/sftp => github.com/gravitational/sftp v1.13.6-0.20220927202521-0e74d42f8055 github.com/sirupsen/logrus => github.com/gravitational/logrus v1.4.4-0.20210817004754-047e20245621 github.com/vulcand/predicate => github.com/gravitational/predicate v1.2.1 + // Our fork includes support for RFC 8308 https://go-review.googlesource.com/c/crypto/+/396714/ + // It should be dropped as soon as this PR is merged into upstream. + golang.org/x/crypto => github.com/gravitational/crypto v0.0.0-20221005181411-e69ec148c162 ) // Exclude etcd/v3 from the modules graph. diff --git a/go.sum b/go.sum index 3ac438f900b32..3ad1f9538ca88 100644 --- a/go.sum +++ b/go.sum @@ -695,6 +695,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gravitational/configure v0.0.0-20180808141939-c3428bd84c23 h1:havbccuFO5fRj0O67oHXI7doShLig3rSIXfMrd/UnkA= github.com/gravitational/configure v0.0.0-20180808141939-c3428bd84c23/go.mod h1:XL9nebvlfNVvRzRPWdDcWootcyA0l7THiH/A+W1233g= +github.com/gravitational/crypto v0.0.0-20221005181411-e69ec148c162 h1:R+PxEtx6oEmd9JthL3ieb+dpM3Q9eKsE1QTq6pvVODo= +github.com/gravitational/crypto v0.0.0-20221005181411-e69ec148c162/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= github.com/gravitational/form v0.0.0-20151109031454-c4048f792f70 h1:To76nCJtM3DI0mdq3nGLzXqTV1wNOJByxv01+u9/BxM= github.com/gravitational/form v0.0.0-20151109031454-c4048f792f70/go.mod h1:88hFR45MpUd23d2vNWE/dYtesU50jKsbz0I9kH7UaBY= github.com/gravitational/go-libfido2 v1.5.3-0.20220630200200-45a8c53e4500 h1:54z7/KbhT1dTmM1HnFQ5ggu5GZ4nUFARYaO6MNsxB1M= @@ -1477,36 +1479,6 @@ go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220126234351-aa10faf2a1f8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1571,7 +1543,6 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -1689,7 +1660,6 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1784,7 +1754,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/integration/helpers/helpers.go b/integration/helpers/helpers.go index 869ed55498a6b..f332b1ee55375 100644 --- a/integration/helpers/helpers.go +++ b/integration/helpers/helpers.go @@ -129,7 +129,11 @@ func CreateAgent(me *user.User, key *client.Key) (*teleagent.AgentServer, string } // create a (unstarted) agent and add the agent key(s) to it - keyring := agent.NewKeyring() + keyring, ok := agent.NewKeyring().(agent.ExtendedAgent) + if !ok { + return nil, "", "", trace.Errorf("unexpected keyring type: %T, expected agent.ExtendedKeyring", keyring) + } + if err := keyring.Add(agentKey); err != nil { return nil, "", "", trace.Wrap(err) } diff --git a/lib/client/api.go b/lib/client/api.go index f5c95f36f63e1..ddf1ce1b29bba 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -239,7 +239,7 @@ type Config struct { UseKeyPrincipals bool // Agent is used when SkipLocalAuth is true - Agent agent.Agent + Agent agent.ExtendedAgent // PreloadKey is a key with which to initialize a local in-memory keystore. PreloadKey *Key @@ -4209,7 +4209,7 @@ func loopbackPool(proxyAddr string) *x509.CertPool { } // connectToSSHAgent connects to the system SSH agent and returns an agent.Agent. -func connectToSSHAgent() agent.Agent { +func connectToSSHAgent() agent.ExtendedAgent { socketPath := os.Getenv(teleport.SSHAuthSock) conn, err := agentconn.Dial(socketPath) if err != nil { diff --git a/lib/client/api_test.go b/lib/client/api_test.go index e834e7c336be4..6cc8ac784d12f 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -546,7 +546,7 @@ func TestApplyProxySettings(t *testing.T) { type mockAgent struct { // Agent is embedded to avoid redeclaring all interface methods. // Only the Signers method is implemented by testAgent. - agent.Agent + agent.ExtendedAgent ValidPrincipals []string } diff --git a/lib/client/client.go b/lib/client/client.go index af7bac1f5584c..8bd419195262f 100644 --- a/lib/client/client.go +++ b/lib/client/client.go @@ -1570,7 +1570,7 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress NodeDet if proxy.teleportClient.localAgent == nil { return nil, trace.BadParameter("cluster is in proxy recording mode and requires agent forwarding for connections, but no agent was initialized") } - err = agent.ForwardToAgent(proxy.Client.Client, proxy.teleportClient.localAgent.Agent) + err = agent.ForwardToAgent(proxy.Client.Client, proxy.teleportClient.localAgent.ExtendedAgent) if err != nil && !strings.Contains(err.Error(), "agent: already have handler for") { return nil, trace.Wrap(err) } @@ -1676,7 +1676,7 @@ func (proxy *ProxyClient) PortForwardToNode(ctx context.Context, nodeAddress Nod if proxy.teleportClient.localAgent == nil { return nil, trace.BadParameter("cluster is in proxy recording mode and requires agent forwarding for connections, but no agent was initialized") } - err = agent.ForwardToAgent(proxy.Client.Client, proxy.teleportClient.localAgent.Agent) + err = agent.ForwardToAgent(proxy.Client.Client, proxy.teleportClient.localAgent.ExtendedAgent) if err != nil && !strings.Contains(err.Error(), "agent: already have handler for") { return nil, trace.Wrap(err) } diff --git a/lib/client/keyagent.go b/lib/client/keyagent.go index c07a302c114cd..fc55b8113035e 100644 --- a/lib/client/keyagent.go +++ b/lib/client/keyagent.go @@ -46,14 +46,14 @@ type LocalKeyAgent struct { // log holds the structured logger. log *logrus.Entry - // Agent is the teleport agent - agent.Agent + // ExtendedAgent is the teleport agent + agent.ExtendedAgent // keyStore is the storage backend for certificates and keys keyStore LocalKeyStore // sshAgent is the system ssh agent - sshAgent agent.Agent + sshAgent agent.ExtendedAgent // noHosts is a in-memory map used in tests to track which hosts a user has // manually (via keyboard input) refused connecting to. @@ -139,7 +139,7 @@ func shouldAddKeysToAgent(addKeysToAgent string) bool { // LocalAgentConfig contains parameters for creating the local keys agent. type LocalAgentConfig struct { Keystore LocalKeyStore - Agent agent.Agent + Agent agent.ExtendedAgent ProxyHost string Username string KeysOption string @@ -152,20 +152,24 @@ type LocalAgentConfig struct { // and loads them into the local and system agent func NewLocalAgent(conf LocalAgentConfig) (a *LocalKeyAgent, err error) { if conf.Agent == nil { - conf.Agent = agent.NewKeyring() + keyring, ok := agent.NewKeyring().(agent.ExtendedAgent) + if !ok { + return nil, trace.Errorf("unexpected keyring type: %T, expected agent.ExtendedKeyring", keyring) + } + conf.Agent = keyring } a = &LocalKeyAgent{ log: logrus.WithFields(logrus.Fields{ trace.Component: teleport.ComponentKeyAgent, }), - Agent: conf.Agent, - keyStore: conf.Keystore, - noHosts: make(map[string]bool), - username: conf.Username, - proxyHost: conf.ProxyHost, - insecure: conf.Insecure, - siteName: conf.Site, - loadAllCAs: conf.LoadAllCAs, + ExtendedAgent: conf.Agent, + keyStore: conf.Keystore, + noHosts: make(map[string]bool), + username: conf.Username, + proxyHost: conf.ProxyHost, + insecure: conf.Insecure, + siteName: conf.Site, + loadAllCAs: conf.LoadAllCAs, } if shouldAddKeysToAgent(conf.KeysOption) { @@ -242,7 +246,7 @@ func (a *LocalKeyAgent) LoadKey(key Key) error { } a.log.Infof("Loading SSH key for user %q and cluster %q.", a.username, key.ClusterName) - agents := []agent.Agent{a.Agent} + agents := []agent.ExtendedAgent{a.ExtendedAgent} if a.sshAgent != nil { agents = append(agents, a.sshAgent) } @@ -271,7 +275,7 @@ func (a *LocalKeyAgent) LoadKey(key Key) error { // UnloadKey will unload key for user from the teleport ssh agent as well as // the system agent. func (a *LocalKeyAgent) UnloadKey() error { - agents := []agent.Agent{a.Agent} + agents := []agent.ExtendedAgent{a.ExtendedAgent} if a.sshAgent != nil { agents = append(agents, a.sshAgent) } @@ -301,7 +305,7 @@ func (a *LocalKeyAgent) UnloadKey() error { // UnloadKeys will unload all Teleport keys from the teleport agent as well as // the system agent. func (a *LocalKeyAgent) UnloadKeys() error { - agents := []agent.Agent{a.Agent} + agents := []agent.ExtendedAgent{a.ExtendedAgent} if a.sshAgent != nil { agents = append(agents, a.sshAgent) } diff --git a/lib/client/keyagent_test.go b/lib/client/keyagent_test.go index 242f3c52bb7d2..8757b7c5fb4de 100644 --- a/lib/client/keyagent_test.go +++ b/lib/client/keyagent_test.go @@ -121,7 +121,7 @@ func TestAddKey(t *testing.T) { } // get all agent keys from teleport agent and system agent - teleportAgentKeys, err := lka.Agent.List() + teleportAgentKeys, err := lka.ExtendedAgent.List() require.NoError(t, err) systemAgentKeys, err := lka.sshAgent.List() require.NoError(t, err) @@ -182,7 +182,7 @@ func TestLoadKey(t *testing.T) { require.NoError(t, err) // get all the keys in the teleport and system agent - teleportAgentKeys, err := lka.Agent.List() + teleportAgentKeys, err := lka.ExtendedAgent.List() require.NoError(t, err) teleportAgentInitialKeyCount := len(teleportAgentKeys) systemAgentKeys, err := lka.sshAgent.List() @@ -197,7 +197,7 @@ func TestLoadKey(t *testing.T) { require.NoError(t, err) // get all the keys in the teleport and system agent - teleportAgentKeys, err = lka.Agent.List() + teleportAgentKeys, err = lka.ExtendedAgent.List() require.NoError(t, err) systemAgentKeys, err = lka.sshAgent.List() require.NoError(t, err) @@ -207,7 +207,7 @@ func TestLoadKey(t *testing.T) { require.Len(t, systemAgentKeys, systemAgentInitialKeyCount+2) // now sign data using the teleport agent and system agent - teleportAgentSignature, err := lka.Agent.Sign(teleportAgentKeys[0], userdata) + teleportAgentSignature, err := lka.ExtendedAgent.Sign(teleportAgentKeys[0], userdata) require.NoError(t, err) systemAgentSignature, err := lka.sshAgent.Sign(systemAgentKeys[0], userdata) require.NoError(t, err) diff --git a/lib/client/session.go b/lib/client/session.go index 05a59bcd1731c..adfcb10f40d8c 100644 --- a/lib/client/session.go +++ b/lib/client/session.go @@ -267,14 +267,14 @@ func (ns *NodeSession) createServerSession(ctx context.Context) (*tracessh.Sessi // selectKeyAgent picks the appropriate key agent for forwarding to the // server, if any. -func selectKeyAgent(tc *TeleportClient) agent.Agent { +func selectKeyAgent(tc *TeleportClient) agent.ExtendedAgent { switch tc.ForwardAgent { case ForwardAgentYes: log.Debugf("Selecting system key agent.") return tc.localAgent.sshAgent case ForwardAgentLocal: log.Debugf("Selecting local Teleport key agent.") - return tc.localAgent.Agent + return tc.localAgent.ExtendedAgent default: log.Debugf("No Key Agent selected.") return nil diff --git a/lib/config/openssh.go b/lib/config/openssh.go new file mode 100644 index 0000000000000..9685ccd1510a6 --- /dev/null +++ b/lib/config/openssh.go @@ -0,0 +1,218 @@ +/* + + Copyright 2022 Gravitational, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +package config + +import ( + "bytes" + "os/exec" + "regexp" + "strconv" + "strings" + "text/template" + + "github.com/coreos/go-semver/semver" + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/trace" + "github.com/sirupsen/logrus" +) + +var sshConfigTemplate = template.Must(template.New("ssh-config").Parse( + `# Begin generated Teleport configuration for {{ .ProxyHost }} by {{ .AppName }} +{{$dot := . }} +{{- range $clusterName := .ClusterNames }} +# Common flags for all {{ $clusterName }} hosts +Host *.{{ $clusterName }} {{ $dot.ProxyHost }} + UserKnownHostsFile "{{ $dot.KnownHostsPath }}" + IdentityFile "{{ $dot.IdentityFilePath }}" + CertificateFile "{{ $dot.CertificateFilePath }}" + HostKeyAlgorithms {{ if $dot.NewerHostKeyAlgorithmsSupported }}rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,{{ end }}ssh-rsa-cert-v01@openssh.com + +# Flags for all {{ $clusterName }} hosts except the proxy +Host *.{{ $clusterName }} !{{ $dot.ProxyHost }} + Port 3022 + {{- if eq $dot.AppName "tsh" }} + ProxyCommand "{{ $dot.ExecutablePath }}" proxy ssh --cluster={{ $clusterName }} --proxy={{ $dot.ProxyHost }} %r@%h:%p +{{- end }}{{- if eq $dot.AppName "tbot" }} + ProxyCommand "{{ $dot.ExecutablePath }}" proxy --destination-dir={{ $dot.DestinationDir }} --proxy={{ $dot.ProxyHost }} ssh --cluster={{ $clusterName }} %r@%h:%p +{{- end }} +{{- end }} + +# End generated Teleport configuration +`)) + +// SSHConfigParameters is a set of SSH related parameters used to generate ~/.ssh/config file. +type SSHConfigParameters struct { + AppName SSHConfigApps + ClusterNames []string + KnownHostsPath string + IdentityFilePath string + CertificateFilePath string + ProxyHost string + ExecutablePath string + DestinationDir string +} + +type sshTmplParams struct { + SSHConfigParameters + sshConfigOptions +} + +// openSSHVersionRegex is a regex used to parse OpenSSH version strings. +var openSSHVersionRegex = regexp.MustCompile(`^OpenSSH_(?P\d+)\.(?P\d+)(?:p(?P\d+))?`) + +// openSSHMinVersionForHostAlgos is the first version that understands all host keys required by us. +// HostKeyAlgorithms will be added to ssh config if the version is above listed here. +var openSSHMinVersionForHostAlgos = semver.New("7.8.0") + +// SSHConfigApps represent apps that support ssh config generation. +type SSHConfigApps string + +const ( + TshApp SSHConfigApps = teleport.ComponentTSH + TbotApp SSHConfigApps = teleport.ComponentTBot +) + +// parseSSHVersion attempts to parse the local SSH version, used to determine +// certain config template parameters for client version compatibility. +func parseSSHVersion(versionString string) (*semver.Version, error) { + versionTokens := strings.Split(versionString, " ") + if len(versionTokens) == 0 { + return nil, trace.BadParameter("invalid version string: %s", versionString) + } + + versionID := versionTokens[0] + matches := openSSHVersionRegex.FindStringSubmatch(versionID) + if matches == nil { + return nil, trace.BadParameter("cannot parse version string: %q", versionID) + } + + major, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, trace.Wrap(err, "invalid major version number: %s", matches[1]) + } + + minor, err := strconv.Atoi(matches[2]) + if err != nil { + return nil, trace.Wrap(err, "invalid minor version number: %s", matches[2]) + } + + patch := 0 + if matches[3] != "" { + patch, err = strconv.Atoi(matches[3]) + if err != nil { + return nil, trace.Wrap(err, "invalid patch version number: %s", matches[3]) + } + } + + return &semver.Version{ + Major: int64(major), + Minor: int64(minor), + Patch: int64(patch), + }, nil +} + +// GetSystemSSHVersion attempts to query the system SSH for its current version. +func GetSystemSSHVersion() (*semver.Version, error) { + var out bytes.Buffer + + cmd := exec.Command("ssh", "-V") + cmd.Stderr = &out + + err := cmd.Run() + if err != nil { + return nil, trace.Wrap(err) + } + + return parseSSHVersion(out.String()) +} + +type sshConfigOptions struct { + // NewerHostKeyAlgorithmsSupported when true sets HostKeyAlgorithms OpenSSH configuration option + // to SHA256/512 compatible algorithms. Otherwise, SHA-1 is being used. + NewerHostKeyAlgorithmsSupported bool +} + +func (c *sshConfigOptions) String() string { + sb := &strings.Builder{} + sb.WriteString("sshConfigOptions:") + + if c.NewerHostKeyAlgorithmsSupported { + sb.WriteString("HostKeyAlgorithms will include SHA-256, SHA-512 and SHA-1") + } else { + sb.WriteString("HostKeyAlgorithms will include SHA-1") + } + + return sb.String() +} + +func isNewerHostKeyAlgorithmsSupported(ver *semver.Version) bool { + return !ver.LessThan(*openSSHMinVersionForHostAlgos) +} + +func getSSHConfigOptions(sshVer *semver.Version) *sshConfigOptions { + return &sshConfigOptions{ + NewerHostKeyAlgorithmsSupported: isNewerHostKeyAlgorithmsSupported(sshVer), + } +} + +func getDefaultSSHConfigOptions() *sshConfigOptions { + return &sshConfigOptions{ + NewerHostKeyAlgorithmsSupported: true, + } +} + +type SSHConfig struct { + getSSHVersion func() (*semver.Version, error) + log logrus.FieldLogger +} + +// NewSSHConfig creates a SSHConfig initialized with provided values or defaults otherwise. +func NewSSHConfig(getSSHVersion func() (*semver.Version, error), log logrus.FieldLogger) *SSHConfig { + if getSSHVersion == nil { + getSSHVersion = GetSystemSSHVersion + } + if log == nil { + log = utils.NewLogger() + } + return &SSHConfig{getSSHVersion: getSSHVersion, log: log} +} + +func (c *SSHConfig) GetSSHConfig(sb *strings.Builder, config *SSHConfigParameters) error { + var sshOptions *sshConfigOptions + version, err := c.getSSHVersion() + if err != nil { + c.log.WithError(err).Debugf("Could not determine SSH version, using default SSH config") + sshOptions = getDefaultSSHConfigOptions() + } else { + c.log.Debugf("Found OpenSSH version %s", version) + sshOptions = getSSHConfigOptions(version) + } + + c.log.Debugf("Using SSH options: %s", sshOptions) + + if err := sshConfigTemplate.Execute(sb, sshTmplParams{ + SSHConfigParameters: *config, + sshConfigOptions: *sshOptions, + }); err != nil { + return trace.Wrap(err) + } + + return nil +} diff --git a/lib/config/openssh_test.go b/lib/config/openssh_test.go new file mode 100644 index 0000000000000..1a754bebf08ce --- /dev/null +++ b/lib/config/openssh_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2022 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "strings" + "testing" + + "github.com/coreos/go-semver/semver" + "github.com/gravitational/teleport/lib/utils/golden" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func TestParseSSHVersion(t *testing.T) { + tests := []struct { + str string + version *semver.Version + err bool + }{ + { + str: "OpenSSH_8.2p1 Ubuntu-4ubuntu0.4, OpenSSL 1.1.1f 31 Mar 2020", + version: semver.New("8.2.1"), + }, + { + str: "OpenSSH_8.8p1, OpenSSL 1.1.1m 14 Dec 2021", + version: semver.New("8.8.1"), + }, + { + str: "OpenSSH_7.5p1, OpenSSL 1.0.2s-freebsd 28 May 2019", + version: semver.New("7.5.1"), + }, + { + str: "OpenSSH_7.9p1 Raspbian-10+deb10u2, OpenSSL 1.1.1d 10 Sep 2019", + version: semver.New("7.9.1"), + }, + { + // Couldn't find a full example but in theory patch is optional: + str: "OpenSSH_8.1 foo", + version: semver.New("8.1.0"), + }, + { + str: "Teleport v8.0.0-dev.40 git:v8.0.0-dev.40-0-ge9194c256 go1.17.2", + err: true, + }, + } + + for _, test := range tests { + version, err := parseSSHVersion(test.str) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + require.True(t, version.Equal(*test.version), "got version = %v, want = %v", version, test.version) + } + } +} + +func TestSSHConfig_GetSSHConfig(t *testing.T) { + tests := []struct { + name string + sshVersion string + config *SSHConfigParameters + }{ + { + name: "legacy OpenSSH - single cluster", + sshVersion: "6.4.0", + config: &SSHConfigParameters{ + AppName: TshApp, + ClusterNames: []string{"example.com"}, + KnownHostsPath: "/home/alice/.tsh/known_hosts", + IdentityFilePath: "/home/alice/.tsh/keys/example.com/bob", + CertificateFilePath: "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub", + ProxyHost: "proxy.example.com", + ExecutablePath: "/tmp/tsh", + }, + }, + { + name: "modern OpenSSH - single cluster", + sshVersion: "9.0.0", + config: &SSHConfigParameters{ + AppName: TshApp, + ClusterNames: []string{"example.com"}, + KnownHostsPath: "/home/alice/.tsh/known_hosts", + IdentityFilePath: "/home/alice/.tsh/keys/example.com/bob", + CertificateFilePath: "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub", + ProxyHost: "proxy.example.com", + ExecutablePath: "/tmp/tsh", + }, + }, + { + name: "modern OpenSSH - multiple clusters", + sshVersion: "9.0.0", + config: &SSHConfigParameters{ + AppName: TshApp, + ClusterNames: []string{"root", "leaf"}, + KnownHostsPath: "/home/alice/.tsh/known_hosts", + IdentityFilePath: "/home/alice/.tsh/keys/example.com/bob", + CertificateFilePath: "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub", + ProxyHost: "proxy.example.com", + ExecutablePath: "/tmp/tsh", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SSHConfig{ + getSSHVersion: func() (*semver.Version, error) { + return semver.New(tt.sshVersion), nil + }, + log: logrus.New(), + } + + sb := &strings.Builder{} + err := c.GetSSHConfig(sb, tt.config) + require.NoError(t, err) + require.Equal(t, string(golden.Get(t)), sb.String()) + }) + } +} diff --git a/lib/config/testdata/TestSSHConfig_GetSSHConfig/legacy_OpenSSH_-_single_cluster.golden b/lib/config/testdata/TestSSHConfig_GetSSHConfig/legacy_OpenSSH_-_single_cluster.golden new file mode 100644 index 0000000000000..4797ecd1a0e81 --- /dev/null +++ b/lib/config/testdata/TestSSHConfig_GetSSHConfig/legacy_OpenSSH_-_single_cluster.golden @@ -0,0 +1,15 @@ +# Begin generated Teleport configuration for proxy.example.com by tsh + +# Common flags for all example.com hosts +Host *.example.com proxy.example.com + UserKnownHostsFile "/home/alice/.tsh/known_hosts" + IdentityFile "/home/alice/.tsh/keys/example.com/bob" + CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub" + HostKeyAlgorithms ssh-rsa-cert-v01@openssh.com + +# Flags for all example.com hosts except the proxy +Host *.example.com !proxy.example.com + Port 3022 + ProxyCommand "/tmp/tsh" proxy ssh --cluster=example.com --proxy=proxy.example.com %r@%h:%p + +# End generated Teleport configuration diff --git a/lib/config/testdata/TestSSHConfig_GetSSHConfig/modern_OpenSSH_-_multiple_clusters.golden b/lib/config/testdata/TestSSHConfig_GetSSHConfig/modern_OpenSSH_-_multiple_clusters.golden new file mode 100644 index 0000000000000..ceaca490e07b7 --- /dev/null +++ b/lib/config/testdata/TestSSHConfig_GetSSHConfig/modern_OpenSSH_-_multiple_clusters.golden @@ -0,0 +1,26 @@ +# Begin generated Teleport configuration for proxy.example.com by tsh + +# Common flags for all root hosts +Host *.root proxy.example.com + UserKnownHostsFile "/home/alice/.tsh/known_hosts" + IdentityFile "/home/alice/.tsh/keys/example.com/bob" + CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all root hosts except the proxy +Host *.root !proxy.example.com + Port 3022 + ProxyCommand "/tmp/tsh" proxy ssh --cluster=root --proxy=proxy.example.com %r@%h:%p +# Common flags for all leaf hosts +Host *.leaf proxy.example.com + UserKnownHostsFile "/home/alice/.tsh/known_hosts" + IdentityFile "/home/alice/.tsh/keys/example.com/bob" + CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all leaf hosts except the proxy +Host *.leaf !proxy.example.com + Port 3022 + ProxyCommand "/tmp/tsh" proxy ssh --cluster=leaf --proxy=proxy.example.com %r@%h:%p + +# End generated Teleport configuration diff --git a/lib/config/testdata/TestSSHConfig_GetSSHConfig/modern_OpenSSH_-_single_cluster.golden b/lib/config/testdata/TestSSHConfig_GetSSHConfig/modern_OpenSSH_-_single_cluster.golden new file mode 100644 index 0000000000000..de95d49fc5d30 --- /dev/null +++ b/lib/config/testdata/TestSSHConfig_GetSSHConfig/modern_OpenSSH_-_single_cluster.golden @@ -0,0 +1,15 @@ +# Begin generated Teleport configuration for proxy.example.com by tsh + +# Common flags for all example.com hosts +Host *.example.com proxy.example.com + UserKnownHostsFile "/home/alice/.tsh/known_hosts" + IdentityFile "/home/alice/.tsh/keys/example.com/bob" + CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all example.com hosts except the proxy +Host *.example.com !proxy.example.com + Port 3022 + ProxyCommand "/tmp/tsh" proxy ssh --cluster=example.com --proxy=proxy.example.com %r@%h:%p + +# End generated Teleport configuration diff --git a/lib/sshutils/ctx.go b/lib/sshutils/ctx.go index 725648814b711..32584270cc3fa 100644 --- a/lib/sshutils/ctx.go +++ b/lib/sshutils/ctx.go @@ -104,7 +104,7 @@ func NewConnectionContext(ctx context.Context, nconn net.Conn, sconn *ssh.Server // allowing the underlying ssh.Channel to be closed when the agent // is no longer needed. type agentChannel struct { - agent.Agent + agent.ExtendedAgent ch ssh.Channel } @@ -141,8 +141,8 @@ func (c *ConnectionContext) StartAgentChannel() (teleagent.Agent, error) { return nil, trace.Wrap(err) } return &agentChannel{ - Agent: agent.NewClient(ch), - ch: ch, + ExtendedAgent: agent.NewClient(ch), + ch: ch, }, nil } diff --git a/lib/tbot/config/config_test.go b/lib/tbot/config/config_test.go index 366c54b90441b..9a0ada980c418 100644 --- a/lib/tbot/config/config_test.go +++ b/lib/tbot/config/config_test.go @@ -24,7 +24,6 @@ import ( "testing" "time" - "github.com/coreos/go-semver/semver" "github.com/stretchr/testify/require" ) @@ -147,50 +146,6 @@ func TestLoadTokenFromFile(t *testing.T) { require.Equal(t, token, "xxxyyy") } -func TestParseSSHVersion(t *testing.T) { - tests := []struct { - str string - version *semver.Version - err bool - }{ - { - str: "OpenSSH_8.2p1 Ubuntu-4ubuntu0.4, OpenSSL 1.1.1f 31 Mar 2020", - version: semver.New("8.2.1"), - }, - { - str: "OpenSSH_8.8p1, OpenSSL 1.1.1m 14 Dec 2021", - version: semver.New("8.8.1"), - }, - { - str: "OpenSSH_7.5p1, OpenSSL 1.0.2s-freebsd 28 May 2019", - version: semver.New("7.5.1"), - }, - { - str: "OpenSSH_7.9p1 Raspbian-10+deb10u2, OpenSSL 1.1.1d 10 Sep 2019", - version: semver.New("7.9.1"), - }, - { - // Couldn't find a full example but in theory patch is optional: - str: "OpenSSH_8.1 foo", - version: semver.New("8.1.0"), - }, - { - str: "Teleport v8.0.0-dev.40 git:v8.0.0-dev.40-0-ge9194c256 go1.17.2", - err: true, - }, - } - - for _, test := range tests { - version, err := parseSSHVersion(test.str) - if test.err { - require.Error(t, err) - } else { - require.NoError(t, err) - require.True(t, version.Equal(*test.version), "got version = %v, want = %v", version, test.version) - } - } -} - const exampleConfigFile = ` auth_server: auth.example.com renewal_interval: 5m diff --git a/lib/tbot/config/configtemplate_ssh_client.go b/lib/tbot/config/configtemplate_ssh_client.go index a08d5579e09aa..de07ae501838b 100644 --- a/lib/tbot/config/configtemplate_ssh_client.go +++ b/lib/tbot/config/configtemplate_ssh_client.go @@ -17,21 +17,17 @@ limitations under the License. package config import ( - "bytes" "context" "fmt" "os" - "os/exec" "path/filepath" - "regexp" - "strconv" "strings" "sync" - "text/template" "github.com/coreos/go-semver/semver" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth" + "github.com/gravitational/teleport/lib/config" "github.com/gravitational/teleport/lib/tbot/bot" "github.com/gravitational/teleport/lib/tbot/identity" "github.com/gravitational/teleport/lib/utils" @@ -47,13 +43,6 @@ type TemplateSSHClient struct { getExecutablePath func() (string, error) } -// openSSHVersionRegex is a regex used to parse OpenSSH version strings. -var openSSHVersionRegex = regexp.MustCompile(`^OpenSSH_(?P\d+)\.(?P\d+)(?:p(?P\d+))?`) - -// openSSHMinVersionForRSAWorkaround is the OpenSSH version after which the -// RSA deprecation workaround should be added to generated ssh_config. -var openSSHMinVersionForRSAWorkaround = semver.New("8.5.0") - const ( // sshConfigName is the name of the ssh_config file on disk sshConfigName = "ssh_config" @@ -62,66 +51,12 @@ const ( knownHostsName = "known_hosts" ) -// parseSSHVersion attempts to parse the local SSH version, used to determine -// certain config template parameters for client version compatibility. -func parseSSHVersion(versionString string) (*semver.Version, error) { - versionTokens := strings.Split(versionString, " ") - if len(versionTokens) == 0 { - return nil, trace.BadParameter("invalid version string: %s", versionString) - } - - versionID := versionTokens[0] - matches := openSSHVersionRegex.FindStringSubmatch(versionID) - if matches == nil { - return nil, trace.BadParameter("cannot parse version string: %q", versionID) - } - - major, err := strconv.Atoi(matches[1]) - if err != nil { - return nil, trace.Wrap(err, "invalid major version number: %s", matches[1]) - } - - minor, err := strconv.Atoi(matches[2]) - if err != nil { - return nil, trace.Wrap(err, "invalid minor version number: %s", matches[2]) - } - - patch := 0 - if matches[3] != "" { - patch, err = strconv.Atoi(matches[3]) - if err != nil { - return nil, trace.Wrap(err, "invalid patch version number: %s", matches[3]) - } - } - - return &semver.Version{ - Major: int64(major), - Minor: int64(minor), - Patch: int64(patch), - }, nil -} - -// getSystemSSHVersion attempts to query the system SSH for its current version. -func getSystemSSHVersion() (*semver.Version, error) { - var out bytes.Buffer - - cmd := exec.Command("ssh", "-V") - cmd.Stderr = &out - - err := cmd.Run() - if err != nil { - return nil, trace.Wrap(err) - } - - return parseSSHVersion(out.String()) -} - func (c *TemplateSSHClient) CheckAndSetDefaults() error { if c.ProxyPort != 0 { log.Warn("ssh_client's proxy_port parameter is deprecated and will be removed in a future release.") } if c.getSSHVersion == nil { - c.getSSHVersion = getSystemSSHVersion + c.getSSHVersion = config.GetSystemSSHVersion } if c.getExecutablePath == nil { c.getExecutablePath = os.Executable @@ -155,7 +90,7 @@ func (c *TemplateSSHClient) Describe(destination bot.Destination) []FileDescript // using non-filesystem backends. var sshConfigUnsupportedWarning sync.Once -func (c *TemplateSSHClient) Render(ctx context.Context, bot Bot, currentIdentity *identity.Identity, destination *DestinationConfig) error { +func (c *TemplateSSHClient) Render(ctx context.Context, bot Bot, _ *identity.Identity, destination *DestinationConfig) error { dest, err := destination.GetDestination() if err != nil { return trace.Wrap(err) @@ -215,18 +150,6 @@ func (c *TemplateSSHClient) Render(ctx context.Context, bot Bot, currentIdentity return nil } - // Default to including the RSA deprecation workaround. - rsaWorkaround := true - version, err := c.getSSHVersion() - if err != nil { - log.WithError(err).Debugf("Could not determine SSH version, will include RSA workaround.") - } else if version.LessThan(*openSSHMinVersionForRSAWorkaround) { - log.Debugf("OpenSSH version %s does not require workaround for RSA deprecation", version) - rsaWorkaround = false - } else { - log.Debugf("OpenSSH version %s will use workaround for RSA deprecation", version) - } - executablePath, err := c.getExecutablePath() if err != nil { return trace.Wrap(err) @@ -236,15 +159,17 @@ func (c *TemplateSSHClient) Render(ctx context.Context, bot Bot, currentIdentity knownHostsPath := filepath.Join(destDir, knownHostsName) identityFilePath := filepath.Join(destDir, identity.PrivateKeyKey) certificateFilePath := filepath.Join(destDir, identity.SSHCertKey) - if err := sshConfigTemplate.Execute(&sshConfigBuilder, sshConfigParameters{ - ClusterName: clusterName.GetClusterName(), - ProxyHost: proxyHost, - KnownHostsPath: knownHostsPath, - IdentityFilePath: identityFilePath, - CertificateFilePath: certificateFilePath, - IncludeRSAWorkaround: rsaWorkaround, - TBotPath: executablePath, - DestinationDir: destDir, + + sshConf := config.NewSSHConfig(c.getSSHVersion, log) + if err := sshConf.GetSSHConfig(&sshConfigBuilder, &config.SSHConfigParameters{ + AppName: config.TbotApp, + ClusterNames: []string{clusterName.GetClusterName()}, + KnownHostsPath: knownHostsPath, + IdentityFilePath: identityFilePath, + CertificateFilePath: certificateFilePath, + ProxyHost: proxyHost, + ExecutablePath: executablePath, + DestinationDir: destDir, }); err != nil { return trace.Wrap(err) } @@ -256,44 +181,6 @@ func (c *TemplateSSHClient) Render(ctx context.Context, bot Bot, currentIdentity return nil } -type sshConfigParameters struct { - ClusterName string - KnownHostsPath string - IdentityFilePath string - CertificateFilePath string - ProxyHost string - TBotPath string - DestinationDir string - - // IncludeRSAWorkaround controls whether the RSA deprecation workaround is - // included in the generated configuration. Newer versions of OpenSSH - // deprecate RSA certificates and, due to a bug in golang's ssh package, - // Teleport wrongly advertises its unaffected certificates as a - // now-deprecated certificate type. The workaround includes a config - // override to re-enable RSA certs for just Teleport hosts, however it is - // only supported on OpenSSH 8.5 and later. - IncludeRSAWorkaround bool -} - -var sshConfigTemplate = template.Must(template.New("ssh-config").Parse(` -# Begin generated Teleport configuration for {{ .ProxyHost }} by tbot - -# Common flags for all {{ .ClusterName }} hosts -Host *.{{ .ClusterName }} {{ .ProxyHost }} - UserKnownHostsFile "{{ .KnownHostsPath }}" - IdentityFile "{{ .IdentityFilePath }}" - CertificateFile "{{ .CertificateFilePath }}" - HostKeyAlgorithms ssh-rsa-cert-v01@openssh.com{{- if .IncludeRSAWorkaround }} - PubkeyAcceptedAlgorithms +ssh-rsa-cert-v01@openssh.com{{- end }} - -# Flags for all {{ .ClusterName }} hosts except the proxy -Host *.{{ .ClusterName }} !{{ .ProxyHost }} - Port 3022 - ProxyCommand "{{ .TBotPath }}" proxy --destination-dir={{ .DestinationDir }} --proxy={{ .ProxyHost }} ssh --cluster={{ .ClusterName }} %r@%h:%p - -# End generated Teleport configuration -`)) - func fetchKnownHosts(ctx context.Context, client auth.ClientI, clusterName, proxyHosts string) (string, error) { ca, err := client.GetCertAuthority(ctx, types.CertAuthID{ Type: types.HostCA, diff --git a/lib/tbot/config/configtemplate_ssh_client_test.go b/lib/tbot/config/configtemplate_ssh_client_test.go index 187cd5904677d..0722373ea3259 100644 --- a/lib/tbot/config/configtemplate_ssh_client_test.go +++ b/lib/tbot/config/configtemplate_ssh_client_test.go @@ -31,55 +31,73 @@ import ( ) func TestTemplateSSHClient_Render(t *testing.T) { - dir := t.TempDir() - mockAuth := newMockAuth(t) - - cfg, err := NewDefaultConfig("example.com") - require.NoError(t, err) - - mockBot := newMockBot(cfg, mockAuth) - template := TemplateSSHClient{ - ProxyPort: 1337, - getSSHVersion: func() (*semver.Version, error) { - return openSSHMinVersionForRSAWorkaround, nil + tests := []struct { + Name string + Version string + }{ + { + Name: "legacy OpenSSH", + Version: "6.5.0", }, - getExecutablePath: func() (string, error) { - return "/path/to/tbot", nil - }, - } - // ident is passed in, but not used. - var ident *identity.Identity - dest := &DestinationConfig{ - DestinationMixin: DestinationMixin{ - Directory: &DestinationDirectory{ - Path: dir, - Symlinks: botfs.SymlinksInsecure, - ACLs: botfs.ACLOff, - }, + { + Name: "latest OpenSSH", + Version: "9.0.0", }, } - err = template.Render(context.Background(), mockBot, ident, dest) - require.NoError(t, err) + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + dir := t.TempDir() + mockAuth := newMockAuth(t) - replaceTestDir := func(b []byte) []byte { - return bytes.ReplaceAll(b, []byte(dir), []byte("/test/dir")) - } + cfg, err := NewDefaultConfig("example.com") + require.NoError(t, err) + + mockBot := newMockBot(cfg, mockAuth) + template := TemplateSSHClient{ + ProxyPort: 1337, + getSSHVersion: func() (*semver.Version, error) { + return semver.New(tc.Version), nil + }, + getExecutablePath: func() (string, error) { + return "/path/to/tbot", nil + }, + } + // ident is passed in, but not used. + var ident *identity.Identity + dest := &DestinationConfig{ + DestinationMixin: DestinationMixin{ + Directory: &DestinationDirectory{ + Path: dir, + Symlinks: botfs.SymlinksInsecure, + ACLs: botfs.ACLOff, + }, + }, + } + + err = template.Render(context.Background(), mockBot, ident, dest) + require.NoError(t, err) + + replaceTestDir := func(b []byte) []byte { + return bytes.ReplaceAll(b, []byte(dir), []byte("/test/dir")) + } - knownHostBytes, err := os.ReadFile(filepath.Join(dir, knownHostsName)) - require.NoError(t, err) - knownHostBytes = replaceTestDir(knownHostBytes) - sshConfigBytes, err := os.ReadFile(filepath.Join(dir, sshConfigName)) - require.NoError(t, err) - sshConfigBytes = replaceTestDir(sshConfigBytes) - if golden.ShouldSet() { - golden.SetNamed(t, "known_hosts", knownHostBytes) - golden.SetNamed(t, "ssh_config", sshConfigBytes) + knownHostBytes, err := os.ReadFile(filepath.Join(dir, knownHostsName)) + require.NoError(t, err) + knownHostBytes = replaceTestDir(knownHostBytes) + sshConfigBytes, err := os.ReadFile(filepath.Join(dir, sshConfigName)) + require.NoError(t, err) + sshConfigBytes = replaceTestDir(sshConfigBytes) + if golden.ShouldSet() { + golden.SetNamed(t, "known_hosts", knownHostBytes) + golden.SetNamed(t, "ssh_config", sshConfigBytes) + } + require.Equal( + t, string(golden.GetNamed(t, "known_hosts")), string(knownHostBytes), + ) + require.Equal( + t, string(golden.GetNamed(t, "ssh_config")), string(sshConfigBytes), + ) + }) } - require.Equal( - t, string(golden.GetNamed(t, "known_hosts")), string(knownHostBytes), - ) - require.Equal( - t, string(golden.GetNamed(t, "ssh_config")), string(sshConfigBytes), - ) } diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/known_hosts.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/known_hosts.golden similarity index 100% rename from lib/tbot/config/testdata/TestTemplateSSHClient_Render/known_hosts.golden rename to lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/known_hosts.golden diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/ssh_config.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/ssh_config.golden new file mode 100644 index 0000000000000..87eedbc20e11c --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/ssh_config.golden @@ -0,0 +1,15 @@ +# Begin generated Teleport configuration for tele.blackmesa.gov by tbot + +# Common flags for all tele.blackmesa.gov hosts +Host *.tele.blackmesa.gov tele.blackmesa.gov + UserKnownHostsFile "/test/dir/known_hosts" + IdentityFile "/test/dir/key" + CertificateFile "/test/dir/key-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all tele.blackmesa.gov hosts except the proxy +Host *.tele.blackmesa.gov !tele.blackmesa.gov + Port 3022 + ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy=tele.blackmesa.gov ssh --cluster=tele.blackmesa.gov %r@%h:%p + +# End generated Teleport configuration diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/known_hosts.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/known_hosts.golden new file mode 100644 index 0000000000000..ce2267be63210 --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/known_hosts.golden @@ -0,0 +1,2 @@ +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/ssh_config.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/ssh_config.golden similarity index 91% rename from lib/tbot/config/testdata/TestTemplateSSHClient_Render/ssh_config.golden rename to lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/ssh_config.golden index 34f8596be6c97..347c718781f85 100644 --- a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/ssh_config.golden +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/ssh_config.golden @@ -1,4 +1,3 @@ - # Begin generated Teleport configuration for tele.blackmesa.gov by tbot # Common flags for all tele.blackmesa.gov hosts @@ -7,7 +6,6 @@ Host *.tele.blackmesa.gov tele.blackmesa.gov IdentityFile "/test/dir/key" CertificateFile "/test/dir/key-cert.pub" HostKeyAlgorithms ssh-rsa-cert-v01@openssh.com - PubkeyAcceptedAlgorithms +ssh-rsa-cert-v01@openssh.com # Flags for all tele.blackmesa.gov hosts except the proxy Host *.tele.blackmesa.gov !tele.blackmesa.gov diff --git a/lib/teleagent/agent.go b/lib/teleagent/agent.go index 290abdcfccf7b..5e207d0833d97 100644 --- a/lib/teleagent/agent.go +++ b/lib/teleagent/agent.go @@ -31,26 +31,26 @@ import ( "golang.org/x/crypto/ssh/agent" ) -// Agent extends the agent.Agent interface. +// Agent extends the agent.ExtendedAgent interface. // APIs which accept this interface promise to // call `Close()` when they are done using the // supplied agent. type Agent interface { - agent.Agent + agent.ExtendedAgent io.Closer } // nopCloser wraps an agent.Agent in the extended // Agent interface by adding a NOP closer. type nopCloser struct { - agent.Agent + agent.ExtendedAgent } func (n nopCloser) Close() error { return nil } // NopCloser wraps an agent.Agent with a NOP closer, allowing it // to be passed to APIs which expect the extended agent interface. -func NopCloser(std agent.Agent) Agent { +func NopCloser(std agent.ExtendedAgent) Agent { return nopCloser{std} } diff --git a/lib/web/sessions.go b/lib/web/sessions.go index 3ef7ff366d99c..ae7002ae24982 100644 --- a/lib/web/sessions.go +++ b/lib/web/sessions.go @@ -285,7 +285,7 @@ func (c *SessionContext) extendWebSession(ctx context.Context, req renewSessionR // GetAgent returns agent that can be used to answer challenges // for the web to ssh connection as well as certificate -func (c *SessionContext) GetAgent() (agent.Agent, *ssh.Certificate, error) { +func (c *SessionContext) GetAgent() (agent.ExtendedAgent, *ssh.Certificate, error) { cert, err := c.GetSSHCertificate() if err != nil { return nil, nil, trace.Wrap(err) @@ -298,7 +298,10 @@ func (c *SessionContext) GetAgent() (agent.Agent, *ssh.Certificate, error) { return nil, nil, trace.Wrap(err, "failed to parse SSH private key") } - keyring := agent.NewKeyring() + keyring, ok := agent.NewKeyring().(agent.ExtendedAgent) + if !ok { + return nil, nil, trace.Errorf("unexpected keyring type: %T, expected agent.ExtendedKeyring", keyring) + } err = keyring.Add(agent.AddedKey{ PrivateKey: privateKey, Certificate: cert, diff --git a/tool/tsh/config.go b/tool/tsh/config.go index f638d798daf9f..5446bcc78cd0c 100644 --- a/tool/tsh/config.go +++ b/tool/tsh/config.go @@ -20,52 +20,23 @@ import ( "fmt" "net" "strings" - "text/template" + "github.com/coreos/go-semver/semver" + "github.com/gravitational/teleport/lib/config" "github.com/gravitational/trace" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/utils/keypaths" ) -// TODO: remove PubkeyAcceptedKeyTypes once we finish deprecating SHA1 -const sshConfigTemplate = ` -# Common flags for all {{ .ClusterName }} hosts -Host *.{{ .ClusterName }} {{ .ProxyHost }} - UserKnownHostsFile "{{ .KnownHostsPath }}" - IdentityFile "{{ .IdentityFilePath }}" - CertificateFile "{{ .CertificateFilePath }}" - PubkeyAcceptedKeyTypes +ssh-rsa-cert-v01@openssh.com - HostKeyAlgorithms ssh-rsa-cert-v01@openssh.com - -# Flags for all {{ .ClusterName }} hosts except the proxy -Host *.{{ .ClusterName }} !{{ .ProxyHost }} - Port 3022 - ProxyCommand "{{ .TSHPath }}" proxy ssh --cluster={{ .ClusterName }} --proxy={{ .ProxyHost }} %r@%h:%p -` - -type hostConfigParameters struct { - ClusterName string - KnownHostsPath string - IdentityFilePath string - CertificateFilePath string - ProxyHost string - TSHPath string -} - // writeSSHConfig generates an OpenSSH config block from the `sshConfigTemplate` // template string. -func writeSSHConfig(sb *strings.Builder, params hostConfigParameters) error { - t, err := template.New("ssh-config").Parse(sshConfigTemplate) - if err != nil { +func writeSSHConfig(sb *strings.Builder, params *config.SSHConfigParameters, getSSHVersion func() (*semver.Version, error)) error { + sshConf := config.NewSSHConfig(getSSHVersion, log) + if err := sshConf.GetSSHConfig(sb, params); err != nil { return trace.Wrap(err) } - err = t.Execute(sb, params) - if err != nil { - return trace.WrapWithMessage(err, "error generating SSH configuration from template") - } - return nil } @@ -104,40 +75,24 @@ func onConfig(cf *CLIConf) error { knownHostsPath := keypaths.KnownHostsPath(keysDir) identityFilePath := keypaths.UserKeyPath(keysDir, proxyHost, tc.Config.Username) - var sb strings.Builder - - // Start with a newline in case an existing config file does not end with - // one. - fmt.Fprintln(&sb) - fmt.Fprintf(&sb, "#\n# Begin generated Teleport configuration for %s from `tsh config`\n#\n", tc.Config.WebProxyAddr) - - err = writeSSHConfig(&sb, hostConfigParameters{ - ClusterName: rootClusterName, - KnownHostsPath: knownHostsPath, - IdentityFilePath: identityFilePath, - CertificateFilePath: keypaths.SSHCertPath(keysDir, proxyHost, tc.Config.Username, rootClusterName), - ProxyHost: proxyHost, - TSHPath: cf.executablePath, - }) - if err != nil { - return trace.Wrap(err) - } - + leafClustersNames := make([]string, 0, len(leafClusters)) for _, leafCluster := range leafClusters { - err = writeSSHConfig(&sb, hostConfigParameters{ - ClusterName: leafCluster.GetName(), - KnownHostsPath: knownHostsPath, - IdentityFilePath: identityFilePath, - CertificateFilePath: keypaths.SSHCertPath(keysDir, proxyHost, tc.Config.Username, rootClusterName), - ProxyHost: proxyHost, - TSHPath: cf.executablePath, - }) - if err != nil { - return trace.Wrap(err) - } + leafClustersNames = append(leafClustersNames, leafCluster.GetName()) } - fmt.Fprintf(&sb, "\n# End generated Teleport configuration\n") + var sb strings.Builder + if err := writeSSHConfig(&sb, &config.SSHConfigParameters{ + AppName: config.TshApp, + ClusterNames: append([]string{rootClusterName}, leafClustersNames...), + KnownHostsPath: knownHostsPath, + IdentityFilePath: identityFilePath, + CertificateFilePath: keypaths.SSHCertPath(keysDir, proxyHost, + tc.Config.Username, rootClusterName), + ProxyHost: proxyHost, + ExecutablePath: cf.executablePath, + }, nil); err != nil { + return trace.Wrap(err) + } stdout := cf.Stdout() fmt.Fprint(stdout, sb.String()) diff --git a/tool/tsh/config_test.go b/tool/tsh/config_test.go index cf468ad62176e..5a547897715dc 100644 --- a/tool/tsh/config_test.go +++ b/tool/tsh/config_test.go @@ -20,34 +20,41 @@ import ( "strings" "testing" + "github.com/coreos/go-semver/semver" + "github.com/gravitational/teleport/lib/config" "github.com/stretchr/testify/require" ) // TestWriteSSHConfig tests the writeSSHConfig template output. func TestWriteSSHConfig(t *testing.T) { - want := ` + want := `# Begin generated Teleport configuration for localhost by tsh + # Common flags for all test-cluster hosts Host *.test-cluster localhost UserKnownHostsFile "/tmp/know_host" IdentityFile "/tmp/alice" CertificateFile "/tmp/localhost-cert.pub" - PubkeyAcceptedKeyTypes +ssh-rsa-cert-v01@openssh.com - HostKeyAlgorithms ssh-rsa-cert-v01@openssh.com + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com # Flags for all test-cluster hosts except the proxy Host *.test-cluster !localhost Port 3022 ProxyCommand "/bin/tsh" proxy ssh --cluster=test-cluster --proxy=localhost %r@%h:%p + +# End generated Teleport configuration ` var sb strings.Builder - err := writeSSHConfig(&sb, hostConfigParameters{ - ClusterName: "test-cluster", + err := writeSSHConfig(&sb, &config.SSHConfigParameters{ + AppName: "tsh", + ClusterNames: []string{"test-cluster"}, KnownHostsPath: "/tmp/know_host", IdentityFilePath: "/tmp/alice", CertificateFilePath: "/tmp/localhost-cert.pub", ProxyHost: "localhost", - TSHPath: "/bin/tsh", + ExecutablePath: "/bin/tsh", + }, func() (*semver.Version, error) { + return semver.New("9.0.0"), nil }) require.NoError(t, err) require.Equal(t, want, sb.String()) diff --git a/tool/tsh/proxy_test.go b/tool/tsh/proxy_test.go index e2415792d1c46..409a07907ad5e 100644 --- a/tool/tsh/proxy_test.go +++ b/tool/tsh/proxy_test.go @@ -446,7 +446,9 @@ func createAgent(t *testing.T) string { sockDir := "test" sockName := "agent.sock" - keyring := agent.NewKeyring() + keyring, ok := agent.NewKeyring().(agent.ExtendedAgent) + require.True(t, ok) + teleAgent := teleagent.NewServer(func() (teleagent.Agent, error) { return teleagent.NopCloser(keyring), nil }) diff --git a/tool/tsh/tsh_test.go b/tool/tsh/tsh_test.go index cb882fff632a2..974cf628eb118 100644 --- a/tool/tsh/tsh_test.go +++ b/tool/tsh/tsh_test.go @@ -758,11 +758,11 @@ func TestMakeClient(t *testing.T) { require.NotNil(t, tc) require.Equal(t, proxyWebAddr.String(), tc.Config.WebProxyAddr) require.Equal(t, proxySSHAddr.Addr, tc.Config.SSHProxyAddr) - require.NotNil(t, tc.LocalAgent().Agent) + require.NotNil(t, tc.LocalAgent().ExtendedAgent) // Client should have an in-memory agent with keys loaded, in case agent // forwarding is required for proxy recording mode. - agentKeys, err := tc.LocalAgent().Agent.List() + agentKeys, err := tc.LocalAgent().ExtendedAgent.List() require.NoError(t, err) require.Greater(t, len(agentKeys), 0) }