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,762 changes: 949 additions & 813 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 @@ -319,6 +319,11 @@ message UserCertsRequest {
// and the ID of the validated MFA device will be stored in the certificate.
MFAAuthenticateResponse MFAResponse = 18 [(gogoproto.jsontag) = "mfa_response,omitempty"];

// 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 @@ -1090,6 +1095,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 @@ -1104,6 +1123,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 @@ -4316,23 +4316,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 @@ -4348,7 +4342,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.AccessState{},
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 @@ -2586,9 +2586,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 @@ -2657,14 +2674,46 @@ func validateUserSingleUseCertRequest(ctx context.Context, actx *grpcContext, re
return nil
}

// 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
}

// isDBLocalProxyTunnelCertReq returns whether a cert request is for
// a database cert and the requester is a local proxy tunnel.
func isDBLocalProxyTunnelCertReq(req *proto.UserCertsRequest) bool {
return req.Usage == proto.UserCertsRequest_Database &&
req.RequesterName == proto.UserCertsRequest_TSH_DB_LOCAL_PROXY_TUNNEL
}

func userSingleUseCertsAuthChallenge(gctx *grpcContext, stream proto.AuthService_GenerateUserSingleUseCertsServer) (*types.MFADevice, error) {
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 @@ -2677,6 +2726,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
Loading