Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
34 changes: 22 additions & 12 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2439,20 +2439,30 @@ func (g *GRPCServer) GenerateUserSingleUseCerts(stream proto.AuthService_Generat
}

mfaRequired := proto.MFARequired_MFA_REQUIRED_UNSPECIFIED
if required, err := isMFARequiredForSingleUseCertRequest(ctx, actx, initReq); err == nil {
// If MFA is not required to gain access to the resource then let the client
// know and abort the ceremony.
if !required {
return trace.Wrap(stream.Send(&proto.UserSingleUseCertsResponse{
Response: &proto.UserSingleUseCertsResponse_MFAChallenge{
MFAChallenge: &proto.MFAAuthenticateChallenge{
MFARequired: proto.MFARequired_MFA_REQUIRED_NO,
clusterName, err := actx.GetClusterName()
if err != nil {
return trace.Wrap(err)
}

// Only check if MFA is required for resources within the current cluster. Determining if
// MFA is required for a resource in a leaf cluster will result in a not found error and
// prevent users from accessing resources in leaf clusters.
if initReq.RouteToCluster == "" || clusterName.GetClusterName() == initReq.RouteToCluster {
if required, err := isMFARequiredForSingleUseCertRequest(ctx, actx, initReq); err == nil {
// If MFA is not required to gain access to the resource then let the client
// know and abort the ceremony.
if !required {
return trace.Wrap(stream.Send(&proto.UserSingleUseCertsResponse{
Response: &proto.UserSingleUseCertsResponse_MFAChallenge{
MFAChallenge: &proto.MFAAuthenticateChallenge{
MFARequired: proto.MFARequired_MFA_REQUIRED_NO,
},
},
},
}))
}
}))
}

mfaRequired = proto.MFARequired_MFA_REQUIRED_YES
mfaRequired = proto.MFARequired_MFA_REQUIRED_YES
}
}

// 2. send MFAChallenge
Expand Down
177 changes: 177 additions & 0 deletions lib/auth/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,20 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
_, err = srv.Auth().UpsertDatabaseServer(ctx, db)
require.NoError(t, err)

desktop, err := types.NewWindowsDesktopV3("desktop", nil, types.WindowsDesktopSpecV3{
Addr: "localhost",
HostID: "test",
})
require.NoError(t, err)

require.NoError(t, srv.Auth().CreateWindowsDesktop(ctx, desktop))

leaf, err := types.NewRemoteCluster("leaf")
require.NoError(t, err)

// create remote cluster
require.NoError(t, srv.Auth().CreateRemoteCluster(leaf))

// Create a fake user.
user, role, err := CreateUserAndRole(srv.Auth(), "mfa-user", []string{"role"}, nil)
require.NoError(t, err)
Expand All @@ -1236,6 +1250,8 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
role.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
role.SetDatabaseLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
role.SetDatabaseNames(types.Allow, []string{types.Wildcard})
role.SetWindowsLogins(types.Allow, []string{"role"})
role.SetWindowsDesktopLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
role.SetOptions(roleOpt)
err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
Expand Down Expand Up @@ -1492,6 +1508,44 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
},
},
},
{
desc: "desktops",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_WindowsDesktop,
RouteToWindowsDesktop: proto.RouteToWindowsDesktop{
WindowsDesktop: "desktop",
Login: "role",
},
},
checkInitErr: require.NoError,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authHandler: registered.webAuthHandler,
checkAuthErr: require.NoError,
validateCert: func(t *testing.T, c *proto.SingleUseUserCert) {
crt := c.GetTLS()
require.NotEmpty(t, crt)

cert, err := tlsca.ParseCertificatePEM(crt)
require.NoError(t, err)
require.Equal(t, cert.NotAfter, clock.Now().Add(teleport.UserSingleUseCertTTL))

identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.LoginIP).IsLoopback())
require.Equal(t, []string{teleport.UsageWindowsDesktopOnly}, identity.Usage)
},
},
},
{
desc: "fail - wrong usage",
opts: generateUserSingleUseCertTestOpts{
Expand Down Expand Up @@ -1669,6 +1723,129 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
checkAuthErr: require.Error,
},
},
{
desc: "k8s in leaf cluster",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_Kubernetes,
KubernetesCluster: "kube-b",
RouteToCluster: "leaf",
},
checkInitErr: require.NoError,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authHandler: registered.webAuthHandler,
checkAuthErr: require.NoError,
validateCert: func(t *testing.T, c *proto.SingleUseUserCert) {
crt := c.GetTLS()
require.NotEmpty(t, crt)

cert, err := tlsca.ParseCertificatePEM(crt)
require.NoError(t, err)
require.Equal(t, cert.NotAfter, clock.Now().Add(teleport.UserSingleUseCertTTL))

identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.LoginIP).IsLoopback())
require.Equal(t, []string{teleport.UsageKubeOnly}, identity.Usage)
require.Equal(t, "kube-b", identity.KubernetesCluster)
},
},
},
{
desc: "db in leaf cluster",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_Database,
RouteToDatabase: proto.RouteToDatabase{
ServiceName: "db-b",
Database: "db-b",
},
RouteToCluster: "leaf",
},
checkInitErr: require.NoError,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authHandler: registered.webAuthHandler,
checkAuthErr: require.NoError,
validateCert: func(t *testing.T, c *proto.SingleUseUserCert) {
crt := c.GetTLS()
require.NotEmpty(t, crt)

cert, err := tlsca.ParseCertificatePEM(crt)
require.NoError(t, err)
require.Equal(t, clock.Now().Add(teleport.UserSingleUseCertTTL), cert.NotAfter)

identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.LoginIP).IsLoopback())
require.Equal(t, []string{teleport.UsageDatabaseOnly}, identity.Usage)
require.Equal(t, identity.RouteToDatabase.ServiceName, "db-b")
},
},
},
{
desc: "ssh in leaf node",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
// This expiry is longer than allowed, should be
// automatically adjusted.
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-b",
SSHLogin: "role",
RouteToCluster: "leaf",
},
checkInitErr: require.NoError,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authHandler: registered.webAuthHandler,
checkAuthErr: require.NoError,
validateCert: func(t *testing.T, c *proto.SingleUseUserCert) {
sshCertBytes := c.GetSSH()
require.NotEmpty(t, sshCertBytes)

cert, err := sshutils.ParseCertificate(sshCertBytes)
require.NoError(t, err)

require.Equal(t, webDevID, cert.Extensions[teleport.CertExtensionMFAVerified])
require.Equal(t, userCertExpires.Format(time.RFC3339), cert.Extensions[teleport.CertExtensionPreviousIdentityExpires])
require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionLoginIP]).IsLoopback())
require.Equal(t, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()), cert.ValidBefore)
},
},
},
{
desc: "fail - app access not supported",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_App,
},
checkInitErr: require.Error,
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
Expand Down
27 changes: 20 additions & 7 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1605,14 +1605,22 @@ func (tc *TeleportClient) ConnectToNode(ctx context.Context, clt *ClusterClient,
mfaCancel()
directCancel()

// Only return the error from connecting with mfa if the error
// originates from the mfa ceremony. If mfa is not required then
// the error from the direct connection to the node must be returned.
if mfaErr != nil && !errors.Is(mfaErr, io.EOF) && !errors.Is(mfaErr, MFARequiredUnknownErr{}) && !errors.Is(mfaErr, services.ErrSessionMFANotRequired) {
switch {
// No MFA errors, return any errors from the direct connection
case mfaErr == nil:
return nil, trace.Wrap(directErr)
// Any direct connection errors other than access denied, which should be returned
// if MFA is required, take precedent over MFA errors due to users not having any
// enrolled devices.
case !trace.IsAccessDenied(directErr) && errors.Is(mfaErr, auth.ErrNoMFADevices):
return nil, trace.Wrap(directErr)
case !errors.Is(mfaErr, io.EOF) && // Ignore any errors from MFA due to locks being enforced, the direct error will be friendlier
!errors.Is(mfaErr, MFARequiredUnknownErr{}) && // Ignore any failures that occurred before determining if MFA was required
!errors.Is(mfaErr, services.ErrSessionMFANotRequired): // Ignore any errors caused by attempting the MFA ceremony when MFA will not grant access
return nil, trace.Wrap(mfaErr)
default:
return nil, trace.Wrap(directErr)
}

return nil, trace.Wrap(directErr)
}

// MFARequiredUnknownErr indicates that connections to an instance failed
Expand Down Expand Up @@ -2817,7 +2825,12 @@ func (tc *TeleportClient) ConnectToCluster(ctx context.Context) (*ClusterClient,
cluster = connected
}

aclt, err := auth.NewClient(pclt.ClientConfig(ctx, cluster))
cltConfig := pclt.ClientConfig(ctx, cluster)
cltConfig.DialOpts = append(cltConfig.DialOpts,
grpc.WithStreamInterceptor(utils.GRPCClientStreamErrorInterceptor),
grpc.WithUnaryInterceptor(utils.GRPCClientUnaryErrorInterceptor),
)
aclt, err := auth.NewClient(cltConfig)
if err != nil {
return nil, trace.NewAggregate(err, pclt.Close())
}
Expand Down
3 changes: 1 addition & 2 deletions lib/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"time"

"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
"github.com/moby/term"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
Expand Down Expand Up @@ -576,7 +575,7 @@ func (proxy *ProxyClient) IssueUserCertsWithMFA(ctx context.Context, params Reis
// challenge and will terminate the stream with an auth.ErrNoMFADevices error.
// In this case for all protocols other than SSH fall back to reissuing
// certs without MFA.
if errors.Is(trail.FromGRPC(err), auth.ErrNoMFADevices) {
if errors.Is(err, auth.ErrNoMFADevices) {
if params.usage() != proto.UserCertsRequest_SSH {
return proxy.reissueUserCerts(ctx, CertCacheKeep, params)
}
Expand Down
18 changes: 14 additions & 4 deletions lib/client/cluster_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import (
"go.opentelemetry.io/otel/attribute"
oteltrace "go.opentelemetry.io/otel/trace"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"

"github.com/gravitational/teleport/api/client/proto"
proxyclient "github.com/gravitational/teleport/api/client/proxy"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)

// ClusterClient facilitates communicating with both the
Expand Down Expand Up @@ -85,7 +87,13 @@ func (c *ClusterClient) SessionSSHConfig(ctx context.Context, user string, targe

mfaClt := c
if target.Cluster != rootClusterName {
aclt, err := auth.NewClient(c.ProxyClient.ClientConfig(ctx, rootClusterName))
cltConfig := c.ProxyClient.ClientConfig(ctx, rootClusterName)
cltConfig.DialOpts = append(cltConfig.DialOpts,
grpc.WithStreamInterceptor(utils.GRPCClientStreamErrorInterceptor),
grpc.WithUnaryInterceptor(utils.GRPCClientUnaryErrorInterceptor),
)

aclt, err := auth.NewClient(cltConfig)
if err != nil {
return nil, trace.Wrap(MFARequiredUnknown(err))
}
Expand All @@ -102,7 +110,7 @@ func (c *ClusterClient) SessionSSHConfig(ctx context.Context, user string, targe
}

log.Debug("Attempting to issue a single-use user certificate with an MFA check.")
key, err = performMFACeremony(ctx, mfaClt,
key, err = c.performMFACeremony(ctx, mfaClt,
ReissueParams{
NodeName: nodeName(target.Addr),
RouteToCluster: target.Cluster,
Expand Down Expand Up @@ -236,7 +244,7 @@ func (c *ClusterClient) prepareUserCertsRequest(params ReissueParams, key *Key)

// performMFACeremony runs the mfa ceremony to completion. If successful the returned
// [Key] will be authorized to connect to the target.
func performMFACeremony(ctx context.Context, clt *ClusterClient, params ReissueParams, key *Key) (*Key, error) {
func (c *ClusterClient) performMFACeremony(ctx context.Context, clt *ClusterClient, params ReissueParams, key *Key) (*Key, error) {
stream, err := clt.AuthClient.GenerateUserSingleUseCerts(ctx)
if err != nil {
if trace.IsNotImplemented(err) {
Expand Down Expand Up @@ -285,7 +293,9 @@ func performMFACeremony(ctx context.Context, clt *ClusterClient, params ReissueP
case proto.MFARequired_MFA_REQUIRED_NO:
return nil, trace.Wrap(services.ErrSessionMFANotRequired)
case proto.MFARequired_MFA_REQUIRED_UNSPECIFIED:
check, err := clt.AuthClient.IsMFARequired(ctx, params.isMFARequiredRequest(clt.tc.HostLogin))
// check if MFA is required with the auth client for this cluster and
// not the root client
check, err := c.AuthClient.IsMFARequired(ctx, params.isMFARequiredRequest(clt.tc.HostLogin))
if err != nil {
return nil, trace.Wrap(MFARequiredUnknown(err))
}
Expand Down
Loading