Skip to content

Commit

Permalink
mfa: audit events for adding/removing devices (#5665)
Browse files Browse the repository at this point in the history
* mfa: audit events for adding/removing devices

Also add `With2FA` field to `UserLogin` when an MFA device is used
during login.

* Address review feedback

* mfa: reorganize audit event structure to be flat
  • Loading branch information
Andrew Lytvynov committed Mar 29, 2021
1 parent 8f5157e commit 5785c58
Show file tree
Hide file tree
Showing 13 changed files with 1,953 additions and 823 deletions.
2,496 changes: 1,736 additions & 760 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions api/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ message UserLogin {
// IdentityAttributes is a map of user attributes received from identity provider
google.protobuf.Struct IdentityAttributes = 5
[ (gogoproto.jsontag) = "attributes,omitempty", (gogoproto.casttype) = "Struct" ];

// MFA is the MFA device used during the login.
MFADeviceMetadata MFADevice = 6 [ (gogoproto.jsontag) = "mfa_device,omitempty" ];
}

// ResourceMetadata is a common resource metadata
Expand Down Expand Up @@ -1176,6 +1179,42 @@ message DatabaseSessionEnd {
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
}

// MFADeviceMetadata is a common MFA device metadata.
message MFADeviceMetadata {
// Name is the user-specified name of the MFA device.
string DeviceName = 1 [ (gogoproto.jsontag) = "mfa_device_name" ];
// ID is the UUID of the MFA device generated by Teleport.
string DeviceID = 2 [ (gogoproto.jsontag) = "mfa_device_uuid" ];
// Type is the type of this MFA device.
string DeviceType = 3 [ (gogoproto.jsontag) = "mfa_device_type" ];
}

// MFADeviceAdd is emitted when a user adds an MFA device.
message MFADeviceAdd {
// Metadata is a common event metadata.
Metadata Metadata = 1
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
// User is a common user event metadata.
UserMetadata User = 2
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
// Device is the new MFA device added by the user.
MFADeviceMetadata Device = 3
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
}

// MFADeviceDelete is emitted when a user deletes an MFA device.
message MFADeviceDelete {
// Metadata is a common event metadata.
Metadata Metadata = 1
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
// User is a common user event metadata.
UserMetadata User = 2
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
// Device is the MFA device deleted by the user.
MFADeviceMetadata Device = 3
[ (gogoproto.nullable) = false, (gogoproto.embed) = true, (gogoproto.jsontag) = "" ];
}

// OneOf is a union of one of audit events submitted to the auth service
message OneOf {
// Event is one of the audit events
Expand Down Expand Up @@ -1223,6 +1262,8 @@ message OneOf {
events.DatabaseSessionEnd DatabaseSessionEnd = 41;
events.DatabaseSessionQuery DatabaseSessionQuery = 42;
events.SessionUpload SessionUpload = 43;
events.MFADeviceAdd MFADeviceAdd = 44;
events.MFADeviceDelete MFADeviceDelete = 45;
}
}

Expand Down
12 changes: 12 additions & 0 deletions api/types/events/oneof.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ func ToOneOf(in AuditEvent) (*OneOf, error) {
out.Event = &OneOf_SessionUpload{
SessionUpload: e,
}
case *MFADeviceAdd:
out.Event = &OneOf_MFADeviceAdd{
MFADeviceAdd: e,
}
case *MFADeviceDelete:
out.Event = &OneOf_MFADeviceDelete{
MFADeviceDelete: e,
}
default:
return nil, trace.BadParameter("event type %T is not supported", in)
}
Expand Down Expand Up @@ -299,6 +307,10 @@ func FromOneOf(in OneOf) (AuditEvent, error) {
return e, nil
} else if e := in.GetSessionUpload(); e != nil {
return e, nil
} else if e := in.GetMFADeviceAdd(); e != nil {
return e, nil
} else if e := in.GetMFADeviceDelete(); e != nil {
return e, nil
} else {
if in.Event == nil {
return nil, trace.BadParameter("failed to parse event, session record is corrupted")
Expand Down
26 changes: 14 additions & 12 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ func (a *Server) GetMFAAuthenticateChallenge(user string, password []byte) (*MFA
ctx := context.TODO()

err := a.WithUserLock(user, func() error {
return a.CheckPasswordWOToken(user, password)
return a.checkPasswordWOToken(user, password)
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -933,15 +933,15 @@ func (a *Server) GetMFAAuthenticateChallenge(user string, password []byte) (*MFA
return chal, nil
}

func (a *Server) CheckU2FSignResponse(ctx context.Context, user string, response *u2f.AuthenticateChallengeResponse) error {
func (a *Server) CheckU2FSignResponse(ctx context.Context, user string, response *u2f.AuthenticateChallengeResponse) (*types.MFADevice, error) {
// before trying to register a user, see U2F is actually setup on the backend
cap, err := a.GetAuthPreference()
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}
_, err = cap.GetU2F()
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}

return a.checkU2F(ctx, user, *response, a.Identity)
Expand Down Expand Up @@ -2060,24 +2060,26 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, u2fStorage u
}

func (a *Server) validateMFAAuthResponse(ctx context.Context, user string, resp *proto.MFAAuthenticateResponse, u2fStorage u2f.AuthenticationStorage) error {
var err error
switch res := resp.Response.(type) {
case *proto.MFAAuthenticateResponse_TOTP:
return a.checkOTP(user, res.TOTP.Code)
_, err = a.checkOTP(user, res.TOTP.Code)
case *proto.MFAAuthenticateResponse_U2F:
return a.checkU2F(ctx, user, u2f.AuthenticateChallengeResponse{
_, err = a.checkU2F(ctx, user, u2f.AuthenticateChallengeResponse{
KeyHandle: res.U2F.KeyHandle,
ClientData: res.U2F.ClientData,
SignatureData: res.U2F.Signature,
}, u2fStorage)
default:
return trace.BadParameter("unknown or missing MFAAuthenticateResponse type %T", resp.Response)
err = trace.BadParameter("unknown or missing MFAAuthenticateResponse type %T", resp.Response)
}
return trace.Wrap(err)
}

func (a *Server) checkU2F(ctx context.Context, user string, res u2f.AuthenticateChallengeResponse, u2fStorage u2f.AuthenticationStorage) error {
func (a *Server) checkU2F(ctx context.Context, user string, res u2f.AuthenticateChallengeResponse, u2fStorage u2f.AuthenticationStorage) (*types.MFADevice, error) {
devs, err := a.GetMFADevices(ctx, user)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}
for _, dev := range devs {
u2fDev := dev.GetU2F()
Expand All @@ -2098,11 +2100,11 @@ func (a *Server) checkU2F(ctx context.Context, user string, res u2f.Authenticate
Clock: a.clock,
}); err != nil {
// Since key handles are unique, no need to check other devices.
return trace.AccessDenied("U2F response validation failed for device %q: %v", dev.GetName(), err)
return nil, trace.AccessDenied("U2F response validation failed for device %q: %v", dev.GetName(), err)
}
return nil
return dev, nil
}
return trace.AccessDenied("U2F response validation failed: no device matches the response")
return nil, trace.AccessDenied("U2F response validation failed: no device matches the response")
}

// WithClock is a functional server option that sets the server's clock
Expand Down
3 changes: 2 additions & 1 deletion lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,8 @@ func (a *ServerWithRoles) CheckPassword(user string, password []byte, otpToken s
if err := a.currentUserAction(user); err != nil {
return trace.Wrap(err)
}
return a.authServer.CheckPassword(user, password, otpToken)
_, err := a.authServer.checkPassword(user, password, otpToken)
return trace.Wrap(err)
}

func (a *ServerWithRoles) PreAuthenticatedSignIn(user string) (services.WebSession, error) {
Expand Down
58 changes: 57 additions & 1 deletion lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import (
"net"
"time"

"github.com/golang/protobuf/ptypes/empty"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/auth/u2f"
Expand All @@ -36,6 +36,8 @@ import (
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/utils"

"github.com/golang/protobuf/ptypes/empty"
"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
"github.com/jonboulle/clockwork"
Expand Down Expand Up @@ -1356,6 +1358,24 @@ func (g *GRPCServer) AddMFADevice(stream proto.AuthService_AddMFADeviceServer) e
return trail.ToGRPC(err)
}

clusterName, err := actx.GetClusterName()
if err != nil {
return trail.ToGRPC(err)
}
if err := g.Emitter.EmitAuditEvent(g.Context, &apievents.MFADeviceAdd{
Metadata: apievents.Metadata{
Type: events.MFADeviceAddEvent,
Code: events.MFADeviceAddEventCode,
ClusterName: clusterName.GetClusterName(),
},
UserMetadata: apievents.UserMetadata{
User: actx.Identity.GetIdentity().Username,
},
MFADeviceMetadata: mfaDeviceEventMetadata(dev),
}); err != nil {
return trail.ToGRPC(err)
}

// 6. send Ack
if err := stream.Send(&proto.AddMFADeviceResponse{
Response: &proto.AddMFADeviceResponse_Ack{Ack: &proto.AddMFADeviceResponseAck{Device: dev}},
Expand Down Expand Up @@ -1576,6 +1596,25 @@ func (g *GRPCServer) DeleteMFADevice(stream proto.AuthService_DeleteMFADeviceSer
if err := auth.DeleteMFADevice(ctx, user, d.Id); err != nil {
return trail.ToGRPC(err)
}

clusterName, err := actx.GetClusterName()
if err != nil {
return trail.ToGRPC(err)
}
if err := g.Emitter.EmitAuditEvent(g.Context, &apievents.MFADeviceDelete{
Metadata: apievents.Metadata{
Type: events.MFADeviceDeleteEvent,
Code: events.MFADeviceDeleteEventCode,
ClusterName: clusterName.GetClusterName(),
},
UserMetadata: apievents.UserMetadata{
User: actx.Identity.GetIdentity().Username,
},
MFADeviceMetadata: mfaDeviceEventMetadata(d),
}); err != nil {
return trail.ToGRPC(err)
}

// 4. send Ack
if err := stream.Send(&proto.DeleteMFADeviceResponse{
Response: &proto.DeleteMFADeviceResponse_Ack{Ack: &proto.DeleteMFADeviceResponseAck{}},
Expand Down Expand Up @@ -1621,6 +1660,23 @@ func deleteMFADeviceAuthChallenge(gctx *grpcContext, stream proto.AuthService_De
return nil
}

func mfaDeviceEventMetadata(d *types.MFADevice) apievents.MFADeviceMetadata {
m := apievents.MFADeviceMetadata{
DeviceName: d.Metadata.Name,
DeviceID: d.Id,
}
switch d.Device.(type) {
case *types.MFADevice_Totp:
m.DeviceType = string(constants.SecondFactorOTP)
case *types.MFADevice_U2F:
m.DeviceType = string(constants.SecondFactorU2F)
default:
m.DeviceType = "unknown"
log.Warningf("Unknown MFA device type %T when generating audit event metadata", d.Device)
}
return m
}

func (g *GRPCServer) GetMFADevices(ctx context.Context, req *proto.GetMFADevicesRequest) (*proto.GetMFADevicesResponse, error) {
actx, err := g.authenticate(ctx)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions lib/auth/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ func (a *TestAuthServer) NewTestTLSServer() (*TestTLSServer, error) {
Authorizer: a.Authorizer,
SessionService: a.SessionServer,
AuditLog: a.AuditLog,
Emitter: a.AuthServer.emitter,
}
srv, err := NewTestTLSServer(TestTLSServerConfig{
APIConfig: apiConfig,
Expand Down
Loading

0 comments on commit 5785c58

Please sign in to comment.