diff --git a/lib/auth/join.go b/lib/auth/join.go index b5348fbfa9e19..7ed55f055a6bf 100644 --- a/lib/auth/join.go +++ b/lib/auth/join.go @@ -115,6 +115,87 @@ func setRemoteAddrFromContext(ctx context.Context, req *types.RegisterUsingToken return nil } +// handleJoinFailure logs and audits the failure of a join. It is intentionally +// designed to handle potential nullness of the input parameters. +func (a *Server) handleJoinFailure( + origErr error, + pt types.ProvisionToken, + attributeSource joinAttributeSourcer, + req *types.RegisterUsingTokenRequest, +) { + fields := logrus.Fields{} + if req != nil { + fields["role"] = req.Role + fields["host_id"] = req.HostID + fields["node_name"] = req.NodeName + } + + // Fetch and encode attributes if they are available. + var attributesProto *apievents.Struct + if attributeSource != nil { + var err error + attributes, err := attributeSource.JoinAuditAttributes() + if err != nil { + log.WithError(err).Warn("Unable to fetch join attributes from join method") + } + fields["attributes"] = attributes + attributesProto, err = apievents.EncodeMap(attributes) + if err != nil { + log.WithError(err).Warn("Unable to encode join attributes for audit event") + } + } + + // Add log fields from token if available. + if pt != nil { + fields["join_method"] = string(pt.GetJoinMethod()) + fields["token_name"] = pt.GetSafeName() + } + log.WithError(origErr).WithFields(fields).Warn("Failure to join cluster occurred") + + var evt apievents.AuditEvent + status := apievents.Status{ + Success: false, + Error: origErr.Error(), + } + if req != nil && req.Role == types.RoleBot { + botJoinEvent := &apievents.BotJoin{ + Metadata: apievents.Metadata{ + Type: events.BotJoinEvent, + Code: events.BotJoinFailureCode, + }, + Status: status, + Attributes: attributesProto, + } + if pt != nil { + botJoinEvent.Method = string(pt.GetJoinMethod()) + botJoinEvent.TokenName = pt.GetSafeName() + } + evt = botJoinEvent + } else { + instanceJoinEvent := &apievents.InstanceJoin{ + Metadata: apievents.Metadata{ + Type: events.InstanceJoinEvent, + Code: events.InstanceJoinFailureCode, + }, + Status: status, + Attributes: attributesProto, + } + if pt != nil { + instanceJoinEvent.Method = string(pt.GetJoinMethod()) + instanceJoinEvent.TokenName = pt.GetSafeName() + } + if req != nil { + instanceJoinEvent.Role = string(req.Role) + instanceJoinEvent.NodeName = req.NodeName + instanceJoinEvent.HostID = req.HostID + } + evt = instanceJoinEvent + } + if err := a.emitter.EmitAuditEvent(a.closeCtx, evt); err != nil { + log.WithError(err).Warn("Failed to emit failed join event") + } +} + // RegisterUsingToken returns credentials for a new node to join the Teleport // cluster using a previously issued token. // @@ -126,30 +207,21 @@ func setRemoteAddrFromContext(ctx context.Context, req *types.RegisterUsingToken // // If the token includes a specific join method, the rules for that join method // will be checked. -func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsingTokenRequest) (_ *proto.Certs, err error) { +func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsingTokenRequest) (certs *proto.Certs, err error) { + var joinAttributeSrc joinAttributeSourcer + var provisionToken types.ProvisionToken + defer func() { + // Emit a log message and audit event on join failure. + if err != nil { + a.handleJoinFailure(err, provisionToken, joinAttributeSrc, req) + } + }() + if err := req.CheckAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } method := a.tokenJoinMethod(ctx, req.Token) - defer func() { - if err == nil { - return - } - level := logrus.WarnLevel - if trace.IsAccessDenied(err) { - level = logrus.DebugLevel - } - log.WithFields(logrus.Fields{ - "node_name": req.NodeName, - "host_id": req.HostID, - "role": req.Role, - "method": method, - logrus.ErrorKey: err, - }).Log(level, "Agent has failed to join the cluster.") - }() - - var joinAttributeSrc joinAttributeSourcer switch method { case types.JoinMethodEC2: if err := a.checkEC2JoinRequest(ctx, req); err != nil { @@ -162,40 +234,52 @@ func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsin "sure your node is configured to use the %s join method", method, method) case types.JoinMethodGitHub: claims, err := a.checkGitHubJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodGitLab: claims, err := a.checkGitLabJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodCircleCI: claims, err := a.checkCircleCIJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodKubernetes: claims, err := a.checkKubernetesJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodGCP: claims, err := a.checkGCPJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodSpacelift: claims, err := a.checkSpaceliftJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodToken: // carry on to common token checking logic default: @@ -206,7 +290,7 @@ func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsin } // perform common token checks - provisionToken, err := a.checkTokenJoinRequestCommon(ctx, req) + provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req) if err != nil { return nil, trace.Wrap(err) } @@ -214,10 +298,10 @@ func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsin // With all elements of the token validated, we can now generate & return // certificates. if req.Role == types.RoleBot { - certs, err := a.generateCertsBot(ctx, provisionToken, req, joinAttributeSrc) + certs, err = a.generateCertsBot(ctx, provisionToken, req, joinAttributeSrc) return certs, trace.Wrap(err) } - certs, err := a.generateCerts(ctx, provisionToken, req, joinAttributeSrc) + certs, err = a.generateCerts(ctx, provisionToken, req, joinAttributeSrc) return certs, trace.Wrap(err) } diff --git a/lib/auth/join_azure.go b/lib/auth/join_azure.go index 282edb6634b9b..7447c56ea0944 100644 --- a/lib/auth/join_azure.go +++ b/lib/auth/join_azure.go @@ -340,7 +340,22 @@ func generateAzureChallenge() (string, error) { // The caller must provide a ChallengeResponseFunc which returns a // *proto.RegisterUsingAzureMethodRequest with a signed attested data document // including the challenge as a nonce. -func (a *Server) RegisterUsingAzureMethod(ctx context.Context, challengeResponse client.RegisterAzureChallengeResponseFunc, opts ...azureRegisterOption) (*proto.Certs, error) { +func (a *Server) RegisterUsingAzureMethod( + ctx context.Context, + challengeResponse client.RegisterAzureChallengeResponseFunc, + opts ...azureRegisterOption, +) (certs *proto.Certs, err error) { + var provisionToken types.ProvisionToken + var joinRequest *types.RegisterUsingTokenRequest + defer func() { + // Emit a log message and audit event on join failure. + if err != nil { + a.handleJoinFailure( + err, provisionToken, nil, joinRequest, + ) + } + }() + cfg := &azureRegisterConfig{} for _, opt := range opts { opt(cfg) @@ -362,7 +377,7 @@ func (a *Server) RegisterUsingAzureMethod(ctx context.Context, challengeResponse return nil, trace.Wrap(err) } - provisionToken, err := a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) + provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) if err != nil { return nil, trace.Wrap(err) } @@ -380,7 +395,7 @@ func (a *Server) RegisterUsingAzureMethod(ctx context.Context, challengeResponse ) return certs, trace.Wrap(err) } - certs, err := a.generateCerts( + certs, err = a.generateCerts( ctx, provisionToken, req.RegisterUsingTokenRequest, diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 93a9837d40bbd..333282198be0a 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -35,7 +35,6 @@ import ( "github.com/aws/aws-sdk-go/service/sts" "github.com/coreos/go-semver/semver" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client" @@ -340,7 +339,22 @@ func withFips(fips bool) iamRegisterOption { // The caller must provide a ChallengeResponseFunc which returns a // *types.RegisterUsingTokenRequest with a signed sts:GetCallerIdentity request // including the challenge as a signed header. -func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse client.RegisterIAMChallengeResponseFunc, opts ...iamRegisterOption) (_ *proto.Certs, err error) { +func (a *Server) RegisterUsingIAMMethod( + ctx context.Context, + challengeResponse client.RegisterIAMChallengeResponseFunc, + opts ...iamRegisterOption, +) (certs *proto.Certs, err error) { + var provisionToken types.ProvisionToken + var joinRequest *types.RegisterUsingTokenRequest + defer func() { + // Emit a log message and audit event on join failure. + if err != nil { + a.handleJoinFailure( + err, provisionToken, nil, joinRequest, + ) + } + }() + cfg := defaultIAMRegisterConfig(a.fips) for _, opt := range opts { opt(cfg) @@ -360,30 +374,11 @@ func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse c return nil, trace.Wrap(err) } - var method types.JoinMethod = "unknown" - defer func() { - if err == nil { - return - } - level := logrus.WarnLevel - if trace.IsAccessDenied(err) { - level = logrus.DebugLevel - } - log.WithFields(logrus.Fields{ - "node_name": req.RegisterUsingTokenRequest.NodeName, - "host_id": req.RegisterUsingTokenRequest.HostID, - "role": req.RegisterUsingTokenRequest.Role, - "method": method, - logrus.ErrorKey: err, - }).Log(level, "Agent has failed to join the cluster.") - }() - // perform common token checks - provisionToken, err := a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) + provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) if err != nil { return nil, trace.Wrap(err) } - method = provisionToken.GetJoinMethod() // check that the GetCallerIdentity request is valid and matches the token if err := a.checkIAMRequest(ctx, challenge, req, cfg); err != nil { @@ -394,7 +389,7 @@ func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse c certs, err := a.generateCertsBot(ctx, provisionToken, req.RegisterUsingTokenRequest, nil) return certs, trace.Wrap(err) } - certs, err := a.generateCerts(ctx, provisionToken, req.RegisterUsingTokenRequest, nil) + certs, err = a.generateCerts(ctx, provisionToken, req.RegisterUsingTokenRequest, nil) return certs, trace.Wrap(err) } diff --git a/lib/events/codes.go b/lib/events/codes.go index 8be9e27432fd8..1add2e52a7fe7 100644 --- a/lib/events/codes.go +++ b/lib/events/codes.go @@ -420,8 +420,12 @@ const ( // BotJoinCode is the 'bot.join' event code. BotJoinCode = "TJ001I" + // BotJoinFailureCode is the 'bot.join' event code for failures. + BotJoinFailureCode = "TJ001E" // InstanceJoinCode is the 'node.join' event code. InstanceJoinCode = "TJ002I" + // InstanceJoinFailureCode is the 'node.join' event code for failures. + InstanceJoinFailureCode = "TJ002E" // BotCreateCode is the `bot.create` event code. BotCreateCode = "TB001I" diff --git a/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx b/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx index 332efffedb31c..9fa7bbd2a500c 100644 --- a/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx +++ b/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx @@ -227,7 +227,9 @@ const EventIconMap: Record = { [eventCodes.SSMRUN_SUCCESS]: Icons.Info, [eventCodes.SSMRUN_FAIL]: Icons.Info, [eventCodes.BOT_JOIN]: Icons.Info, + [eventCodes.BOT_JOIN_FAILURE]: Icons.Warning, [eventCodes.INSTANCE_JOIN]: Icons.Info, + [eventCodes.INSTANCE_JOIN_FAILURE]: Icons.Warning, [eventCodes.LOGIN_RULE_CREATE]: Icons.Info, [eventCodes.LOGIN_RULE_DELETE]: Icons.Info, [eventCodes.SAML_IDP_AUTH_ATTEMPT]: Icons.Info, diff --git a/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap b/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap index 29f107ddfdf77..5981fb9b101fb 100644 --- a/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap +++ b/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap @@ -406,12 +406,12 @@ exports[`list of all events 1`] = ` - - 239 + 240 of - 239 + 240 + + { + return `Bot [${bot_name || 'unknown'}] failed to join the cluster`; + }, + }, [eventCodes.INSTANCE_JOIN]: { type: 'instance.join', desc: 'Instance Joined', @@ -1484,6 +1491,13 @@ export const formatters: Formatters = { return `Instance [${node_name}] joined the cluster with the [${role}] role using the [${method}] join method`; }, }, + [eventCodes.INSTANCE_JOIN_FAILURE]: { + type: 'instance.join', + desc: 'Instance Join Failed', + format: ({ node_name }) => { + return `Instance [${node_name || 'unknown'}] failed to join the cluster`; + }, + }, [eventCodes.BOT_CREATED]: { type: 'bot.create', desc: 'Bot Created', diff --git a/web/packages/teleport/src/services/audit/types.ts b/web/packages/teleport/src/services/audit/types.ts index 15ec80def7e90..3eb421d5ab8c1 100644 --- a/web/packages/teleport/src/services/audit/types.ts +++ b/web/packages/teleport/src/services/audit/types.ts @@ -245,7 +245,9 @@ export const eventCodes = { CERTIFICATE_CREATED: 'TC000I', UPGRADE_WINDOW_UPDATED: 'TUW01I', BOT_JOIN: 'TJ001I', + BOT_JOIN_FAILURE: 'TJ001E', INSTANCE_JOIN: 'TJ002I', + INSTANCE_JOIN_FAILURE: 'TJ002E', BOT_CREATED: 'TB001I', BOT_UPDATED: 'TB002I', BOT_DELETED: 'TB003I', @@ -1317,6 +1319,13 @@ export type RawEvents = { method: string; } >; + [eventCodes.BOT_JOIN_FAILURE]: RawEvent< + typeof eventCodes.BOT_JOIN, + { + bot_name: string; + method: string; + } + >; [eventCodes.INSTANCE_JOIN]: RawEvent< typeof eventCodes.INSTANCE_JOIN, { @@ -1325,6 +1334,14 @@ export type RawEvents = { role: string; } >; + [eventCodes.INSTANCE_JOIN_FAILURE]: RawEvent< + typeof eventCodes.INSTANCE_JOIN, + { + node_name: string; + method: string; + role: string; + } + >; [eventCodes.BOT_CREATED]: RawEvent; [eventCodes.BOT_UPDATED]: RawEvent; [eventCodes.BOT_DELETED]: RawEvent;