Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v10] Allow connections to nodes when Auth is offline #18914

Merged
merged 2 commits into from
Dec 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 14 additions & 13 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1696,21 +1696,25 @@ func testInvalidLogins(t *testing.T, suite *integrationTestSuite) {
tr := utils.NewTracer(utils.ThisFunction()).Start()
defer tr.Stop()

teleport := suite.newTeleport(t, nil, true)
defer teleport.StopAll()
instance := suite.newTeleport(t, nil, true)
defer func() {
require.NoError(t, instance.StopAll())
}()

cmd := []string{"echo", "success"}

// try the wrong site:
tc, err := teleport.NewClient(ClientConfig{
tc, err := instance.NewClient(ClientConfig{
Login: suite.me.Username,
Cluster: "wrong-site",
Host: Host,
Port: teleport.GetPortSSHInt(),
Port: instance.GetPortSSHInt(),
})
require.NoError(t, err)
err = tc.SSH(context.TODO(), cmd, false)
require.Contains(t, err.Error(), `unknown cluster \"wrong-site\"`)

err = tc.SSH(context.Background(), cmd, false)
require.True(t, trace.IsConnectionProblem(err))
require.Contains(t, err.Error(), `unknown cluster "wrong-site"`)
}

// TestTwoClustersTunnel creates two teleport clusters: "a" and "b" and creates a
Expand Down Expand Up @@ -2557,9 +2561,9 @@ func trustedClusters(t *testing.T, suite *integrationTestSuite, test trustedClus
require.Len(t, remoteClusters, 1)
require.Equal(t, clusterAux, remoteClusters[0].GetName())

// after removing the remote cluster, the connection will start failing
err = main.Process.GetAuthServer().DeleteRemoteCluster(clusterAux)
require.NoError(t, err)
// after removing the remote cluster and trusted cluster, the connection will start failing
require.NoError(t, main.Process.GetAuthServer().DeleteRemoteCluster(clusterAux))
require.NoError(t, aux.Process.GetAuthServer().DeleteTrustedCluster(ctx, trustedCluster.GetName()))
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 50)
err = tc.SSH(ctx, cmd, false)
Expand All @@ -2569,10 +2573,7 @@ func trustedClusters(t *testing.T, suite *integrationTestSuite, test trustedClus
}
require.Error(t, err, "expected tunnel to close and SSH client to start failing")

// remove trusted cluster from aux cluster side, and recreate right after
// this should re-establish connection
err = aux.Process.GetAuthServer().DeleteTrustedCluster(ctx, trustedCluster.GetName())
require.NoError(t, err)
// recreating the trusted cluster should re-establish connection
_, err = aux.Process.GetAuthServer().UpsertTrustedCluster(ctx, trustedCluster)
require.NoError(t, err)

Expand Down
206 changes: 175 additions & 31 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1901,15 +1901,129 @@ func (tc *TeleportClient) SSH(ctx context.Context, command []string, runLocally
}

if len(nodeAddrs) > 1 {
return tc.runShellOrCommandOnMultipleNodes(ctx, tc.SiteName, nodeAddrs, proxyClient, command)
return tc.runShellOrCommandOnMultipleNodes(ctx, nodeAddrs, proxyClient, command)
}
return tc.runShellOrCommandOnSingleNode(ctx, tc.SiteName, nodeAddrs[0], proxyClient, command, runLocally)
return tc.runShellOrCommandOnSingleNode(ctx, nodeAddrs[0], proxyClient, command, runLocally)
}

func (tc *TeleportClient) runShellOrCommandOnSingleNode(ctx context.Context, siteName string, nodeAddr string, proxyClient *ProxyClient, command []string, runLocally bool) error {
nodeClient, err := proxyClient.ConnectToNode(
// ConnectToNode attempts to establish a connection to the node resolved to by the provided
// NodeDetails. If the connection fails due to an Access Denied error, Auth is queried to
// determine if per-session MFA is required for the node. If it is required then the MFA
// ceremony is performed and another connection is attempted with the freshly minted
// certificates. If it is not required, then the original Access Denied error from the node
// is returned.
func (tc *TeleportClient) ConnectToNode(ctx context.Context, proxyClient *ProxyClient, nodeDetails NodeDetails, user string) (*NodeClient, error) {
node := nodeName(nodeDetails.Addr)
ctx, span := tc.Tracer.Start(
ctx,
NodeDetails{Addr: nodeAddr, Namespace: tc.Namespace, Cluster: siteName},
"teleportClient/ConnectToNode",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("site", nodeDetails.Cluster),
attribute.String("node", node),
),
)
defer span.End()

// attempt to use the existing credentials first
authMethods := proxyClient.authMethods

// if per-session mfa is required, perform the mfa ceremony to get
// new certificates and use them instead
if nodeDetails.MFACheck != nil && nodeDetails.MFACheck.Required {
am, err := proxyClient.sessionSSHCertificate(ctx, nodeDetails)
if err != nil {
return nil, trace.Wrap(err)
}

authMethods = am
}

// grab the cluster details
details, err := proxyClient.clusterDetails(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

// try connecting to the node
nodeClient, connectErr := proxyClient.ConnectToNode(ctx, nodeDetails, user, details, authMethods)
switch {
case connectErr == nil: // no error return client
return nodeClient, nil
case nodeDetails.MFACheck != nil: // per-session mfa ceremony was already performed, return the results
return nodeClient, trace.Wrap(connectErr)
case connectErr != nil && !trace.IsAccessDenied(connectErr): // catastrophic error, return it
return nil, trace.Wrap(connectErr)
}

// access was denied, determine if it was because per-session mfa is required
clt, err := proxyClient.ConnectToCluster(ctx, nodeDetails.Cluster)
if err != nil {
// return the connection error instead of any errors from connecting to auth
return nil, trace.Wrap(connectErr)
}

check, err := clt.IsMFARequired(ctx, &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Node{
Node: &proto.NodeLogin{
Node: node,
Login: proxyClient.hostLogin,
},
},
})
if err != nil {
return nil, trace.Wrap(connectErr)
}

// per-session mfa isn't required, the user simply does not
// have access to the provided node
if !check.Required {
return nil, trace.Wrap(connectErr)
}

// per-session mfa is required, perform the mfa ceremony
key, err := proxyClient.IssueUserCertsWithMFA(
ctx,
ReissueParams{
NodeName: node,
RouteToCluster: nodeDetails.Cluster,
MFACheck: check,
AuthClient: clt,
},
func(ctx context.Context, proxyAddr string, c *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) {
return tc.PromptMFAChallenge(ctx, proxyAddr, c, nil /* applyOpts */)
},
)
if err != nil {
return nil, trace.Wrap(err)
}

// try connecting to the node again with the newly acquired certificates
newAuthMethods, err := key.AsAuthMethod()
if err != nil {
return nil, trace.Wrap(err)
}

nodeClient, err = proxyClient.ConnectToNode(ctx, nodeDetails, user, details, []ssh.AuthMethod{newAuthMethods})
return nodeClient, trace.Wrap(err)
}

func (tc *TeleportClient) runShellOrCommandOnSingleNode(ctx context.Context, nodeAddr string, proxyClient *ProxyClient, command []string, runLocally bool) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/runShellOrCommandOnSingleNode",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("site", tc.SiteName),
attribute.String("node", nodeAddr),
),
)
defer span.End()

nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{Addr: nodeAddr, Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
if err != nil {
Expand Down Expand Up @@ -1951,24 +2065,38 @@ func (tc *TeleportClient) runShellOrCommandOnSingleNode(ctx context.Context, sit
return tc.runShell(ctx, nodeClient, types.SessionPeerMode, nil, nil)
}

func (tc *TeleportClient) runShellOrCommandOnMultipleNodes(ctx context.Context, siteName string, nodeAddrs []string, proxyClient *ProxyClient, command []string) error {
if len(command) < 1 {
// Issue "shell" request to run single node.
fmt.Printf("\x1b[1mWARNING\x1b[0m: Multiple nodes match the label selector, picking first: %v\n", nodeAddrs[0])
nodeClient, err := proxyClient.ConnectToNode(
ctx,
NodeDetails{Addr: nodeAddrs[0], Namespace: tc.Namespace, Cluster: siteName},
tc.Config.HostLogin,
)
if err != nil {
tc.ExitStatus = 1
return trace.Wrap(err)
}
defer nodeClient.Close()
return tc.runShell(ctx, nodeClient, types.SessionPeerMode, nil, nil)
func (tc *TeleportClient) runShellOrCommandOnMultipleNodes(ctx context.Context, nodeAddrs []string, proxyClient *ProxyClient, command []string) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/runShellOrCommandOnMultipleNodes",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(
attribute.String("site", tc.SiteName),
attribute.StringSlice("node", nodeAddrs),
),
)
defer span.End()

// There was a command provided, run a non-interactive session against each match
if len(command) > 0 {
fmt.Printf("\x1b[1mWARNING\x1b[0m: Multiple nodes matched label selector, running command on all.\n")
return tc.runCommandOnNodes(ctx, tc.SiteName, nodeAddrs, proxyClient, command)
}
fmt.Printf("\x1b[1mWARNING\x1b[0m: Multiple nodes matched label selector, running command on all.\n")
return tc.runCommandOnNodes(ctx, siteName, nodeAddrs, proxyClient, command)

// Issue "shell" request to the first matching node.
fmt.Printf("\x1b[1mWARNING\x1b[0m: Multiple nodes match the label selector, picking first: %q\n", nodeAddrs[0])
nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{Addr: nodeAddrs[0], Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
if err != nil {
tc.ExitStatus = 1
return trace.Wrap(err)
}
defer nodeClient.Close()
return tc.runShell(ctx, nodeClient, types.SessionPeerMode, nil, nil)

}

Expand Down Expand Up @@ -2047,11 +2175,11 @@ func (tc *TeleportClient) Join(ctx context.Context, mode types.SessionParticipan
}

// connect to server:
nc, err := proxyClient.ConnectToNode(ctx, NodeDetails{
Addr: session.GetAddress() + ":0",
Namespace: tc.Namespace,
Cluster: tc.SiteName,
}, tc.Config.HostLogin)
nc, err := tc.ConnectToNode(ctx,
proxyClient,
NodeDetails{Addr: session.GetAddress() + ":0", Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -2220,8 +2348,9 @@ func (tc *TeleportClient) ExecuteSCP(ctx context.Context, cmd scp.Command) (err
return trace.BadParameter("no target host specified")
}

nodeClient, err := proxyClient.ConnectToNode(
nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{Addr: nodeAddrs[0], Namespace: tc.Namespace, Cluster: tc.SiteName},
tc.Config.HostLogin,
)
Expand Down Expand Up @@ -2284,9 +2413,15 @@ func (tc *TeleportClient) SCP(ctx context.Context, args []string, port int, flag
if hostLogin == "" {
hostLogin = tc.Config.HostLogin
}
return proxyClient.ConnectToNode(

return tc.ConnectToNode(
ctx,
NodeDetails{Addr: addr, Namespace: tc.Namespace, Cluster: tc.SiteName},
proxyClient,
NodeDetails{
Addr: addr,
Namespace: tc.Namespace,
Cluster: tc.SiteName,
},
hostLogin,
)
}
Expand Down Expand Up @@ -2768,8 +2903,17 @@ func (tc *TeleportClient) runCommandOnNodes(
for _, address := range nodeAddresses {
address := address
g.Go(func() error {
nodeClient, err := proxyClient.ConnectToNode(
ctx, span := tc.Tracer.Start(
gctx,
"teleportClient/executingCommand",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("node", address)),
)
defer span.End()

nodeClient, err := tc.ConnectToNode(
ctx,
proxyClient,
NodeDetails{
Addr: address,
Namespace: tc.Namespace,
Expand Down
4 changes: 3 additions & 1 deletion lib/client/api_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
Expand Down Expand Up @@ -276,6 +277,7 @@ func TestTeleportClient_PromptMFAChallenge(t *testing.T) {
// MFA opts.
AuthenticatorAttachment: wancli.AttachmentAuto,
PreferOTP: false,
Tracer: tracing.NoopProvider().Tracer("test"),
},
}

Expand All @@ -286,6 +288,7 @@ func TestTeleportClient_PromptMFAChallenge(t *testing.T) {
// MFA opts.
AuthenticatorAttachment: wancli.AttachmentCrossPlatform,
PreferOTP: true,
Tracer: tracing.NoopProvider().Tracer("test"),
},
}

Expand Down Expand Up @@ -346,7 +349,6 @@ func TestTeleportClient_PromptMFAChallenge(t *testing.T) {
gotOpts *client.PromptMFAChallengeOpts,
) (*proto.MFAAuthenticateResponse, error) {
promptCalled = true
assert.Equal(t, ctx, gotCtx, "ctx mismatch")
assert.Equal(t, challenge, gotChallenge, "challenge mismatch")
assert.Equal(t, test.wantProxy, gotProxy, "proxy mismatch")
assert.Equal(t, test.wantOpts, gotOpts, "opts mismatch")
Expand Down