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
1,612 changes: 874 additions & 738 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ message UserCertsRequest {
reserved 18;
reserved "RequesterName";

// SSHLogin is the OS Login for the SSH session that the certificate will be used for.
// This login is used when performing RBAC checks to determine if MFA is required
// to access the resource.
string SSHLogin = 19;

// AttestationStatement is an attestation statement for the given public key.
teleport.attestation.v1.AttestationStatement attestation_statement = 20;
}
Expand Down Expand Up @@ -947,6 +952,20 @@ enum DeviceUsage {
DEVICE_USAGE_PASSWORDLESS = 2;
}

// MFARequired indicates if MFA is required to access a
// resource.
enum MFARequired {
// Indicates the client/server are either old and don't support
// checking if MFA is required during the ceremony or that there
// was a catastrophic error checking RBAC to determine if completing
// an MFA ceremony will grant access to a resource.
MFA_REQUIRED_UNSPECIFIED = 0;
// Completing an MFA ceremony will grant access to a resource.
MFA_REQUIRED_YES = 1;
// Completing an MFA ceremony will not grant access to a resource.
MFA_REQUIRED_NO = 2;
}

// MFAAuthenticateChallenge is a challenge for all MFA devices registered for a
// user.
message MFAAuthenticateChallenge {
Expand All @@ -961,6 +980,11 @@ message MFAAuthenticateChallenge {
// credentials for the ceremony (one for each U2F or Webauthn device
// registered by the user).
webauthn.CredentialAssertion WebauthnChallenge = 3;
// MFARequired indicates whether proceeding with the MFA ceremony will
// grant access to the resource. If `MFA_REQUIRED_NO` is returned by the
// server then the stream will be terminated to prevent a fruitless MFA ceremony from
// proceeding.
MFARequired MFARequired = 4;
}

// MFAAuthenticateResponse is a response to MFAAuthenticateChallenge using one
Expand Down
36 changes: 22 additions & 14 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3720,23 +3720,17 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck
if t.Node.Login == "" {
return nil, trace.BadParameter("empty Login field")
}

// Find the target node and check whether MFA is required.
nodes, err := a.GetNodes(ctx, apidefaults.Namespace)
matches, err := client.GetResourcesWithFilters(ctx, a, proto.ListResourcesRequest{
ResourceType: types.KindNode,
Namespace: apidefaults.Namespace,
SearchKeywords: []string{t.Node.Node},
})
if err != nil {
return nil, trace.Wrap(err)
}
var matches []types.Server
for _, n := range nodes {
// Get the server address without port number.
addr, _, err := net.SplitHostPort(n.GetAddr())
if err != nil {
addr = n.GetAddr()
}
// Match NodeName to UUID, hostname or self-reported server address.
if n.GetName() == t.Node.Node || n.GetHostname() == t.Node.Node || addr == t.Node.Node {
matches = append(matches, n)
}
}

if len(matches) == 0 {
// If t.Node.Node is not a known registered node, it may be an
// unregistered host running OpenSSH with a certificate created via
Expand All @@ -3752,7 +3746,21 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck
// Check RBAC against all matching nodes and return the first error.
// If at least one node requires MFA, we'll catch it.
for _, n := range matches {
err := checker.CheckAccess(
srv, ok := n.(types.Server)
if !ok {
continue
}
// Get the server address without port number.
addr, _, err := net.SplitHostPort(srv.GetAddr())
if err != nil {
addr = srv.GetAddr()
}
// Filter out any matches on labels before checking access
if n.GetName() != t.Node.Node && srv.GetHostname() != t.Node.Node && addr != t.Node.Node {
continue
}

err = checker.CheckAccess(
n,
services.AccessMFAParams{},
services.NewLoginMatcher(t.Node.Login),
Expand Down
56 changes: 54 additions & 2 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2411,9 +2411,26 @@ func (g *GRPCServer) GenerateUserSingleUseCerts(stream proto.AuthService_Generat
return trace.Wrap(err)
}

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,
},
},
}))
}

mfaRequired = proto.MFARequired_MFA_REQUIRED_YES
}

// 2. send MFAChallenge
// 3. receive and validate MFAResponse
mfaDev, err := userSingleUseCertsAuthChallenge(actx, stream)
mfaDev, err := userSingleUseCertsAuthChallenge(actx, stream, mfaRequired)
if err != nil {
g.Entry.Debugf("Failed to perform single-use cert challenge: %v", err)
return trace.Wrap(err)
Expand Down Expand Up @@ -2472,7 +2489,39 @@ func validateUserSingleUseCertRequest(ctx context.Context, actx *grpcContext, re
return nil
}

func userSingleUseCertsAuthChallenge(gctx *grpcContext, stream proto.AuthService_GenerateUserSingleUseCertsServer) (*types.MFADevice, error) {
// isMFARequiredForSingleUseCertRequest validates that mfa is actually required for
// the target of the single-use user cert.
func isMFARequiredForSingleUseCertRequest(ctx context.Context, actx *grpcContext, req *proto.UserCertsRequest) (bool, error) {
mfaReq := &proto.IsMFARequiredRequest{}

switch req.Usage {
case proto.UserCertsRequest_SSH:
// An old or non-conforming client did not provide a login which means rbac
// won't be able to accurately determine if mfa is required.
if req.SSHLogin == "" {
return false, trace.BadParameter("no ssh login provided")
}

mfaReq.Target = &proto.IsMFARequiredRequest_Node{Node: &proto.NodeLogin{Node: req.NodeName, Login: req.SSHLogin}}
case proto.UserCertsRequest_Kubernetes:
mfaReq.Target = &proto.IsMFARequiredRequest_KubernetesCluster{KubernetesCluster: req.KubernetesCluster}
case proto.UserCertsRequest_Database:
mfaReq.Target = &proto.IsMFARequiredRequest_Database{Database: &req.RouteToDatabase}
case proto.UserCertsRequest_WindowsDesktop:
mfaReq.Target = &proto.IsMFARequiredRequest_WindowsDesktop{WindowsDesktop: &req.RouteToWindowsDesktop}
default:
return false, trace.BadParameter("unknown certificate Usage %q", req.Usage)
}

resp, err := actx.IsMFARequired(ctx, mfaReq)
if err != nil {
return false, trace.Wrap(err)
}

return resp.Required, nil
}

func userSingleUseCertsAuthChallenge(gctx *grpcContext, stream proto.AuthService_GenerateUserSingleUseCertsServer, mfaRequired proto.MFARequired) (*types.MFADevice, error) {
ctx := stream.Context()
auth := gctx.authServer
user := gctx.User.GetName()
Expand All @@ -2485,6 +2534,9 @@ func userSingleUseCertsAuthChallenge(gctx *grpcContext, stream proto.AuthService
if challenge.TOTP == nil && challenge.WebauthnChallenge == nil {
return nil, trace.AccessDenied("MFA is required to access this resource but user has no MFA devices; use 'tsh mfa add' to register MFA devices")
}

challenge.MFARequired = mfaRequired

if err := stream.Send(&proto.UserSingleUseCertsResponse{
Response: &proto.UserSingleUseCertsResponse_MFAChallenge{MFAChallenge: challenge},
}); err != nil {
Expand Down
115 changes: 95 additions & 20 deletions lib/auth/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/tls"
"encoding/base32"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -764,7 +765,7 @@ func TestGenerateUserSingleUseCert(t *testing.T) {

// Register an SSH node.
node := &types.ServerV2{
Kind: types.KindKubeService,
Kind: types.KindNode,
Version: types.V2,
Metadata: types.Metadata{
Name: "node-a",
Expand All @@ -775,18 +776,15 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
}
_, err = srv.Auth().UpsertNode(ctx, node)
require.NoError(t, err)
// Register a k8s cluster.
k8sSrv := &types.ServerV2{
Kind: types.KindKubeService,
Version: types.V2,
Metadata: types.Metadata{
Name: "kube-a",
},
Spec: types.ServerSpecV2{
KubernetesClusters: []*types.KubernetesCluster{{Name: "kube-a"}},
},
}
_, err = srv.Auth().UpsertKubeServiceV2(ctx, k8sSrv)

kube, err := types.NewKubernetesClusterV3(types.Metadata{
Name: "kube-a",
}, types.KubernetesClusterSpecV3{})

require.NoError(t, err)
kubeServer, err := types.NewKubernetesServerV3FromCluster(kube, "kube-a", "kube-a")
require.NoError(t, err)
_, err = srv.Auth().UpsertKubernetesServer(ctx, kubeServer)
require.NoError(t, err)
// Register a database.
db, err := types.NewDatabaseServerV3(types.Metadata{
Expand All @@ -808,6 +806,9 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
// Make sure MFA is required for this user.
roleOpt := role.GetOptions()
roleOpt.RequireMFAType = types.RequireMFAType_SESSION
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.SetOptions(roleOpt)
err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
Expand Down Expand Up @@ -846,8 +847,12 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
SSHLogin: "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) {
Expand Down Expand Up @@ -875,8 +880,12 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
Expires: clock.Now().Add(2 * teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
SSHLogin: "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) {
Expand Down Expand Up @@ -904,6 +913,9 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
KubernetesCluster: "kube-a",
},
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) {
Expand Down Expand Up @@ -934,9 +946,13 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
Usage: proto.UserCertsRequest_Database,
RouteToDatabase: proto.RouteToDatabase{
ServiceName: "db-a",
Database: "db-a",
},
},
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) {
Expand Down Expand Up @@ -980,8 +996,57 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
SSHLogin: "role",
},
checkInitErr: require.NoError,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, required)
},
authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
// Return no challenge response.
return &proto.MFAAuthenticateResponse{}
},
checkAuthErr: require.Error,
},
},
{
desc: "fail - mfa not required when RBAC prevents access",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
SSHLogin: "llama", // not an allowed login which prevents access
},
checkInitErr: require.NoError,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_NO, required)
},
authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
// Return no challenge response.
return &proto.MFAAuthenticateResponse{}
},
checkAuthErr: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorIs(t, err, io.EOF, i...)
},
},
},
{
desc: "mfa unspecified when no SSHLogin provided",
opts: generateUserSingleUseCertTestOpts{
initReq: &proto.UserCertsRequest{
PublicKey: pub,
Username: user.GetName(),
Expires: clock.Now().Add(teleport.UserSingleUseCertTTL),
Usage: proto.UserCertsRequest_SSH,
NodeName: "node-a",
},
checkInitErr: require.NoError,
mfaRequiredHandler: func(t *testing.T, required proto.MFARequired) {
require.Equal(t, proto.MFARequired_MFA_REQUIRED_UNSPECIFIED, required)
},
authHandler: func(t *testing.T, req *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse {
// Return no challenge response.
return &proto.MFAAuthenticateResponse{}
Expand All @@ -998,11 +1063,12 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
}

type generateUserSingleUseCertTestOpts struct {
initReq *proto.UserCertsRequest
checkInitErr require.ErrorAssertionFunc
authHandler func(*testing.T, *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse
checkAuthErr require.ErrorAssertionFunc
validateCert func(*testing.T, *proto.SingleUseUserCert)
initReq *proto.UserCertsRequest
checkInitErr require.ErrorAssertionFunc
authHandler func(*testing.T, *proto.MFAAuthenticateChallenge) *proto.MFAAuthenticateResponse
mfaRequiredHandler func(*testing.T, proto.MFARequired)
checkAuthErr require.ErrorAssertionFunc
validateCert func(*testing.T, *proto.SingleUseUserCert)
}

func testGenerateUserSingleUseCert(ctx context.Context, t *testing.T, cl *Client, opts generateUserSingleUseCertTestOpts) {
Expand All @@ -1016,9 +1082,18 @@ func testGenerateUserSingleUseCert(ctx context.Context, t *testing.T, cl *Client
if err != nil {
return
}
authResp := opts.authHandler(t, authChallenge.GetMFAChallenge())

challenge := authChallenge.GetMFAChallenge()
opts.mfaRequiredHandler(t, challenge.MFARequired)

authResp := opts.authHandler(t, challenge)
err = stream.Send(&proto.UserSingleUseCertsRequest{Request: &proto.UserSingleUseCertsRequest_MFAResponse{MFAResponse: authResp}})
require.NoError(t, err)
if challenge.MFARequired == proto.MFARequired_MFA_REQUIRED_NO && err != nil {
require.ErrorIs(t, err, io.EOF, "Want the server to close the stream when MFA is not required")
return
} else {
require.NoError(t, err)
}

certs, err := stream.Recv()
opts.checkAuthErr(t, err)
Expand Down
Loading