return an error when attempting to join a session of an OpenSSH node (#31472)

* return an error when attempting to join a session of an OpenSSH node

* remove item from test plan and note to docs

* add test coverage to integration test

* fix integration test

* fixed linter issue
This commit is contained in:
Andrew LeFevre 2023-09-08 18:16:02 -04:00 committed by GitHub
parent a50f967d27
commit 5054cb685c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1560 additions and 1458 deletions

View file

@ -183,8 +183,6 @@ as well as an upgrade of the previous version of Teleport.
- [ ] tsh ssh \<agentless-node-remote-cluster\> ls
- [ ] tsh join \<regular-node\>
- [ ] tsh join \<node-remote-cluster\>
- [ ] tsh join \<agentless-node\>
- [ ] tsh join \<agentless-node-remote-cluster\>
- [ ] tsh play \<regular-node\>
- [ ] tsh play \<node-remote-cluster\>
- [ ] tsh play \<agentless-node\>

View file

@ -4917,6 +4917,9 @@ message SessionTrackerSpecV1 {
// It's useful for Kubernetes moderated sessions when running in high availabilty
// otherwise kube proxy is not able to know which agent runs the session.
string HostID = 21 [(gogoproto.jsontag) = "host_id,omitempty"];
// TargetSubKind is the sub kind of the target server.
string TargetSubKind = 22 [(gogoproto.jsontag) = "target_sub_kind,omitempty"];
}
// SessionTrackerPolicySet is a set of RBAC policies held by the session tracker

View file

@ -119,8 +119,12 @@ type SessionTracker interface {
// GetLastActive returns the time at which the session was last active (i.e used by any participant).
GetLastActive() time.Time
// HostID is the target host id that created the session tracker.
GetHostID() string
// GetTargetSubKind returns the sub kind of the target server.
GetTargetSubKind() string
}
func NewSessionTracker(spec SessionTrackerSpecV1) (SessionTracker, error) {
@ -335,6 +339,11 @@ func (s *SessionTrackerV1) GetLastActive() time.Time {
return last
}
// GetTargetSubKind returns the sub kind of the target server.
func (s *SessionTrackerV1) GetTargetSubKind() string {
return s.Spec.TargetSubKind
}
// Match checks if a given session tracker matches this filter.
func (f *SessionTrackerFilter) Match(s SessionTracker) bool {
if f.Kind != "" && string(s.GetSessionKind()) != f.Kind {

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@ and running, but in the long run, we would recommend replacing `sshd` with `tele
- RBAC and resource filtering based on [dynamically updated labels](../../management/admin/labels.mdx)
- [Session recording without SSH connection termination](recording-proxy-mode.mdx)
- [Session sharing](../../connect-your-client/tsh.mdx)
- [Advanced session recording](bpf-session-recording.mdx)
- [Restricting outbound network connections in SSH sessions](restricted-session.mdx)

View file

@ -21,7 +21,6 @@ import (
"bytes"
"context"
"crypto/x509"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
@ -95,6 +94,7 @@ import (
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
rsession "github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/tlsca"
@ -3779,14 +3779,23 @@ func testTrustedClusterAgentless(t *testing.T, suite *integrationTestSuite) {
})
require.NoError(t, err)
tc, err := main.NewClientWithCreds(helpers.ClientConfig{
// create client for leaf cluster through root cluster
leafTC, err := main.NewClientWithCreds(helpers.ClientConfig{
Login: username,
Cluster: clusterAux,
Host: aux.InstanceListeners.ReverseTunnel,
}, *creds)
require.NoError(t, err)
testAgentlessConn(t, tc, node)
// create client for root cluster
tc, err := main.NewClient(helpers.ClientConfig{
Login: suite.Me.Username,
Cluster: clusterMain,
Host: main.InstanceListeners.ReverseTunnel,
})
require.NoError(t, err)
testAgentlessConn(t, leafTC, tc, node)
// Stop clusters and remaining nodes.
require.NoError(t, main.StopAll())
@ -7866,8 +7875,8 @@ func testAgentlessConnection(t *testing.T, suite *integrationTestSuite) {
})
// get OpenSSH CA public key and create host certs
authClient := teleInst.Process.GetAuthServer()
node := createAgentlessNode(t, authClient, helpers.Site, "agentless-node")
authSrv := teleInst.Process.GetAuthServer()
node := createAgentlessNode(t, authSrv, helpers.Site, "agentless-node")
// create client
tc, err := teleInst.NewClient(helpers.ClientConfig{
@ -7877,10 +7886,12 @@ func testAgentlessConnection(t *testing.T, suite *integrationTestSuite) {
})
require.NoError(t, err)
testAgentlessConn(t, tc, node)
testAgentlessConn(t, tc, tc, node)
}
func createAgentlessNode(t *testing.T, authServer *auth.Server, clusterName, nodeHostname string) *types.ServerV2 {
t.Helper()
ctx := context.Background()
openSSHCA, err := authServer.GetCertAuthority(ctx, types.CertAuthID{
Type: types.OpenSSHCA,
@ -7965,6 +7976,8 @@ func createAgentlessNode(t *testing.T, authServer *auth.Server, clusterName, nod
// OpenSSH (agentless) server. The SSH server started only handles a small
// subset of SSH requests necessary for testing.
func startSSHServer(t *testing.T, caPubKeys []ssh.PublicKey, hostKey ssh.Signer) string {
t.Helper()
sshCfg := ssh.ServerConfig{
PublicKeyCallback: func(_ ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
cert, ok := key.(*ssh.Certificate)
@ -8010,7 +8023,7 @@ func startSSHServer(t *testing.T, caPubKeys []ssh.PublicKey, hostKey ssh.Signer)
go ssh.DiscardRequests(reqs)
var agentForwarded bool
var cmdRequested bool
var shellRequested bool
for channelReq := range channels {
assert.Equal(t, "session", channelReq.ChannelType())
channel, reqs, err := channelReq.Accept()
@ -8026,25 +8039,24 @@ func startSSHServer(t *testing.T, caPubKeys []ssh.PublicKey, hostKey ssh.Signer)
}
if req.Type == sshutils.AgentForwardRequest {
agentForwarded = true
} else if req.Type == sshutils.ExecRequest {
_, err = channel.SendRequest("exit-status", false, binary.BigEndian.AppendUint32(nil, 0))
assert.NoError(t, err)
err = channel.Close()
assert.NoError(t, err)
} else if req.Type == sshutils.ShellRequest {
assert.NoError(t, channel.Close())
cmdRequested = true
shellRequested = true
break
}
}
}
assert.True(t, agentForwarded)
assert.True(t, cmdRequested)
assert.True(t, shellRequested)
}()
return lis.Addr().String()
}
func testAgentlessConn(t *testing.T, tc *client.TeleportClient, node *types.ServerV2) {
func testAgentlessConn(t *testing.T, tc, joinTC *client.TeleportClient, node *types.ServerV2) {
t.Helper()
// connect to cluster
ctx := context.Background()
clt, err := tc.ConnectToCluster(ctx)
@ -8053,6 +8065,16 @@ func testAgentlessConn(t *testing.T, tc *client.TeleportClient, node *types.Serv
require.NoError(t, clt.Close())
})
// connect to other cluster if needed
joinClt := clt
if tc != joinTC {
joinClt, err = joinTC.ConnectToCluster(ctx)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, joinClt.Close())
})
}
// connect to node
_, port, err := net.SplitHostPort(node.Spec.Addr)
require.NoError(t, err)
@ -8106,9 +8128,26 @@ func testAgentlessConn(t *testing.T, tc *client.TeleportClient, node *types.Serv
require.NoError(t, agent.RequestAgentForwarding(session))
// run a command
err = session.Run("cmd")
require.NoError(t, err)
// request a shell so Teleport starts tracking this session
session.Stderr = io.Discard
session.Stdout = io.Discard
require.NoError(t, session.Shell())
var sessTracker types.SessionTracker
require.Eventually(t, func() bool {
trackers, err := joinClt.AuthClient.GetActiveSessionTrackers(ctx)
require.NoError(t, err)
if len(trackers) == 1 {
sessTracker = trackers[0]
return true
}
return false
}, 3*time.Second, 100*time.Millisecond)
// test that attempting to join the session returns an error
err = joinTC.Join(ctx, types.SessionPeerMode, tc.Namespace, rsession.ID(sessTracker.GetSessionID()), nil)
require.True(t, trace.IsBadParameter(err))
require.ErrorContains(t, err, "session joining is only supported for Teleport nodes, not OpenSSH nodes")
// test that SSH agent channel is closed properly
select {

View file

@ -1918,6 +1918,9 @@ func (tc *TeleportClient) Join(ctx context.Context, mode types.SessionParticipan
if session.GetSessionKind() != types.SSHSessionKind {
return trace.BadParameter("session joining is only supported for ssh sessions, not %q sessions", session.GetSessionKind())
}
if types.IsOpenSSHNodeSubKind(session.GetTargetSubKind()) {
return trace.BadParameter("session joining is only supported for Teleport nodes, not OpenSSH nodes")
}
// connect to server:
nc, err := tc.ConnectToNode(ctx,

View file

@ -591,7 +591,7 @@ func (s *Server) Serve() {
if s.targetServer != nil && s.targetServer.IsOpenSSHNode() {
// OpenSSH nodes don't support moderated sessions, send an error to
// the user and gracefully fail the user is attempting to create one.
// the user and gracefully fail if the user is attempting to create one.
policySets := s.identityContext.AccessChecker.SessionPolicySets()
evaluator := auth.NewSessionAccessEvaluator(policySets, types.SSHSessionKind, s.identityContext.TeleportUser)
if evaluator.IsModerated() {

View file

@ -1981,7 +1981,8 @@ func (s *session) trackSession(ctx context.Context, teleportUser string, policyS
LastActive: s.registry.clock.Now().UTC(),
},
},
HostID: s.registry.Srv.ID(),
HostID: s.registry.Srv.ID(),
TargetSubKind: s.serverMeta.ServerSubKind,
}
if s.scx.env[teleport.EnvSSHSessionInvited] != "" {