diff --git a/lib/auth/grpcserver_test.go b/lib/auth/grpcserver_test.go index ec07a762bd22e..7bab650aa9df0 100644 --- a/lib/auth/grpcserver_test.go +++ b/lib/auth/grpcserver_test.go @@ -30,6 +30,7 @@ import ( "net/http" "net/http/httptest" "sort" + "strings" "testing" "time" @@ -66,6 +67,7 @@ import ( "github.com/gravitational/teleport/api/trail" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/autoupdate" + apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/types/installers" "github.com/gravitational/teleport/api/types/vnet" "github.com/gravitational/teleport/api/utils" @@ -82,6 +84,7 @@ import ( "github.com/gravitational/teleport/lib/cryptosuites" "github.com/gravitational/teleport/lib/defaults" dtauthz "github.com/gravitational/teleport/lib/devicetrust/authz" + "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/integrations/awsra/createsession" iterstream "github.com/gravitational/teleport/lib/itertools/stream" "github.com/gravitational/teleport/lib/modules" @@ -6512,3 +6515,56 @@ func TestRoleVersionV8ToV7Downgrade(t *testing.T) { }) } } + +func TestSessionRejectedAudit(t *testing.T) { + t.Parallel() + server := newTestTLSServer(t) + ctx := context.Background() + username := "locked-user" + + role, _ := types.NewRole("allow-all", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Namespaces: []string{"*"}, + Rules: []types.Rule{{Resources: []string{"*"}, Verbs: []string{"*"}}}, + }, + }) + server.Auth().UpsertRole(ctx, role) + + user, _ := types.NewUser(username) + user.SetRoles([]string{"allow-all"}) + server.Auth().UpsertUser(ctx, user) + + lock, _ := types.NewLock("test-lock", types.LockSpecV2{ + Target: types.LockTarget{User: username}, + }) + server.Auth().UpsertLock(ctx, lock) + + client, err := server.NewClient(authtest.TestUser(username)) + require.NoError(t, err) + defer client.Close() + + _, err = client.GetClusterName(ctx) + + require.Error(t, err) + require.True(t, + strings.Contains(err.Error(), "lock") || strings.Contains(err.Error(), "access denied"), + "Expected lock error, got: %v", err) + + require.Eventually(t, func() bool { + events, _, _ := server.Auth().SearchEvents(ctx, events.SearchEventsRequest{ + From: time.Now().Add(-time.Minute), + To: time.Now().Add(time.Minute), + EventTypes: []string{events.AuthAttemptEvent}, + Limit: 100, + }) + + for _, e := range events { + if attempt, ok := e.(*apievents.AuthAttempt); ok { + if attempt.UserMetadata.User == username && !attempt.Status.Success { + return true + } + } + } + return false + }, 5*time.Second, 100*time.Millisecond, "Expected AuthAttempt event for %v not found", username) +} diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index 2bb999a842d28..ce8e43db39617 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -32,6 +32,8 @@ import ( "github.com/google/uuid" "github.com/gravitational/trace" "github.com/vulcand/predicate/builder" + "google.golang.org/grpc" + "google.golang.org/grpc/peer" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" @@ -44,6 +46,7 @@ import ( "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/keys" dtauthz "github.com/gravitational/teleport/lib/devicetrust/authz" + "github.com/gravitational/teleport/lib/events" scopedaccess "github.com/gravitational/teleport/lib/scopes/access" "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/services/readonly" @@ -77,6 +80,7 @@ type DeviceAuthorizationOpts struct { // AuthorizerOpts holds creation options for [NewAuthorizer]. type AuthorizerOpts struct { + TEST string ClusterName string AccessPoint AuthorizerAccessPoint ReadOnlyAccessPoint ReadOnlyAuthorizerAccessPoint @@ -84,6 +88,7 @@ type AuthorizerOpts struct { MFAAuthenticator MFAAuthenticator LockWatcher *services.LockWatcher Logger *slog.Logger + Emitter apievents.Emitter // DeviceAuthorization holds Device Trust authorization options. // @@ -129,6 +134,7 @@ func newAuthorizer(opts AuthorizerOpts) (*authorizer, error) { } return &authorizer{ + TEST: opts.TEST, clusterName: opts.ClusterName, accessPoint: opts.AccessPoint, readOnlyAccessPoint: opts.ReadOnlyAccessPoint, @@ -136,6 +142,7 @@ func newAuthorizer(opts AuthorizerOpts) (*authorizer, error) { mfaAuthenticator: opts.MFAAuthenticator, lockWatcher: opts.LockWatcher, logger: logger, + emitter: opts.Emitter, disableGlobalDeviceMode: opts.DeviceAuthorization.DisableGlobalMode, disableRoleDeviceMode: opts.DeviceAuthorization.DisableRoleMode, }, nil @@ -221,6 +228,7 @@ type MFAAuthData struct { // authorizer creates new local authorizer type authorizer struct { + TEST string clusterName string accessPoint AuthorizerAccessPoint readOnlyAccessPoint ReadOnlyAuthorizerAccessPoint @@ -228,6 +236,7 @@ type authorizer struct { mfaAuthenticator MFAAuthenticator lockWatcher *services.LockWatcher logger *slog.Logger + emitter apievents.Emitter disableGlobalDeviceMode bool disableRoleDeviceMode bool @@ -430,6 +439,7 @@ func (a *authorizer) Authorize(ctx context.Context) (authCtx *Context, err error } if err := CheckIPPinning(ctx, authContext.Identity.GetIdentity(), authContext.Checker.PinSourceIP(), a.logger); err != nil { + a.emitAuthorizeFailure(ctx, authContext.Identity, err) return nil, trace.Wrap(err) } @@ -441,28 +451,88 @@ func (a *authorizer) Authorize(ctx context.Context) (authCtx *Context, err error if lockErr := a.lockWatcher.CheckLockInForce( authContext.Checker.LockingMode(authPref.GetLockingMode()), authContext.LockTargets()...); lockErr != nil { + a.emitAuthorizeFailure(ctx, authContext.Identity, lockErr) return nil, trace.Wrap(lockErr) } // Enforce required private key policy if set. if err := a.enforcePrivateKeyPolicy(ctx, authContext, authPref); err != nil { + a.emitAuthorizeFailure(ctx, authContext.Identity, err) return nil, trace.Wrap(err) } // Device Trust: authorize device extensions. if !a.disableGlobalDeviceMode { if err := dtauthz.VerifyTLSUser(ctx, authPref.GetDeviceTrust(), authContext.Identity.GetIdentity()); err != nil { + a.emitAuthorizeFailure(ctx, authContext.Identity, err) return nil, trace.Wrap(err) } } if err := a.checkAdminActionVerification(ctx, authContext); err != nil { + a.emitAuthorizeFailure(ctx, authContext.Identity, err) return nil, trace.Wrap(err) } return authContext, nil } +func (a *authorizer) emitAuthorizeFailure(ctx context.Context, user IdentityGetter, err error) { + if a.emitter == nil { + return + } + identity := user.GetIdentity() + username := identity.Username + + // Filter out failures caused by system components so that we only audit events for real users or bots + if len(identity.SystemRoles) == 0 { + errorMsg := trace.UserMessage(err) + if errorMsg == "" { + errorMsg = err.Error() + } + + methodName, _ := grpc.Method(ctx) + if methodName == "" { + // If no gRPC method is found in the context, attempt to get the request method from the context + // (e.g. for kube requests). If that also fails, default to "unknown". + if val := GetRequestMethod(ctx); val != "" { + methodName = val + } else { + methodName = "unknown" + } + } + + userMsg := fmt.Sprintf("authorization failed for method %s: %s component %s", methodName, errorMsg, a.TEST) + + event := &apievents.AuthAttempt{ + Metadata: apievents.Metadata{ + Type: events.AuthAttemptEvent, + Code: events.AuthAttemptFailureCode, + }, + UserMetadata: apievents.UserMetadata{ + User: username, + }, + Status: apievents.Status{ + Success: false, + Error: errorMsg, + UserMessage: userMsg, + }, + ConnectionMetadata: apievents.ConnectionMetadata{}, + ServerMetadata: apievents.ServerMetadata{ + ServerVersion: teleport.Version, + }, + } + + if p, _ := peer.FromContext(ctx); p != nil { + event.ConnectionMetadata.RemoteAddr = p.Addr.String() + } + + if emitErr := a.emitter.EmitAuditEvent(ctx, event); emitErr != nil { + a.logger.WarnContext(ctx, "Failed to emit detailed audit event", "error", emitErr) + } + } +} + func (a *authorizer) enforcePrivateKeyPolicy(ctx context.Context, authContext *Context, authPref readonly.AuthPreference) error { switch authContext.Identity.(type) { case BuiltinRole, RemoteBuiltinRole: @@ -1465,11 +1535,28 @@ const ( // contextConn is a connection in the context associated with the request contextConn contextKey = "teleport-connection" + + // contextMethod is used to store the request method/path/action for audit logs. + contextRequestMethod contextKey = "teleport-request-method" ) // WithDelegator alias for backwards compatibility var WithDelegator = utils.WithDelegator +// GetRequestMethod returns the request method string from the context. +func GetRequestMethod(ctx context.Context) string { + val, ok := ctx.Value(contextRequestMethod).(string) + if !ok { + return "" + } + return val +} + +// WithRequestMethod returns a new context with the request method injected. +func WithRequestMethod(ctx context.Context, method string) context.Context { + return context.WithValue(ctx, contextRequestMethod, method) +} + // ClientUsername returns the username of a remote HTTP client making the call. // If ctx didn't pass through auth middleware or did not come from an HTTP // request, teleport.UserSystem is returned. diff --git a/lib/kube/proxy/forwarder.go b/lib/kube/proxy/forwarder.go index a5d33d6538e71..520a450630cec 100644 --- a/lib/kube/proxy/forwarder.go +++ b/lib/kube/proxy/forwarder.go @@ -564,6 +564,9 @@ func (f *Forwarder) authenticate(req *http.Request) (*authContext, error) { return nil, trace.AccessDenied("%s", accessDeniedMsg) } + kubeMethod := fmt.Sprintf("KUBE: %s %s", req.Method, req.URL.Path) + ctx = authz.WithRequestMethod(ctx, kubeMethod) + userContext, err := f.cfg.Authz.Authorize(ctx) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/service/db.go b/lib/service/db.go index cedc84a1d2feb..26240682ac395 100644 --- a/lib/service/db.go +++ b/lib/service/db.go @@ -89,6 +89,16 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) { databases = append(databases, database) } + asyncEmitter, err := process.NewAsyncEmitter(conn.Client) + if err != nil { + return trace.Wrap(err) + } + defer func() { + if retErr != nil { + warnOnErr(process.ExitContext(), asyncEmitter.Close(), logger) + } + }() + lockWatcher, err := services.NewLockWatcher(process.ExitContext(), services.LockWatcherConfig{ ResourceWatcherConfig: services.ResourceWatcherConfig{ Component: teleport.ComponentDatabase, @@ -106,6 +116,7 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) { AccessPoint: accessPoint, LockWatcher: lockWatcher, Logger: process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentDatabase, process.id)), + Emitter: asyncEmitter, }) if err != nil { return trace.Wrap(err) @@ -115,16 +126,6 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) { return trace.Wrap(err) } - asyncEmitter, err := process.NewAsyncEmitter(conn.Client) - if err != nil { - return trace.Wrap(err) - } - defer func() { - if retErr != nil { - warnOnErr(process.ExitContext(), asyncEmitter.Close(), logger) - } - }() - connLimiter, err := limiter.NewLimiter(process.Config.Databases.Limiter) if err != nil { return trace.Wrap(err) diff --git a/lib/service/desktop.go b/lib/service/desktop.go index 1a6f8737ddbb9..14dc77e033541 100644 --- a/lib/service/desktop.go +++ b/lib/service/desktop.go @@ -157,10 +157,12 @@ func (process *TeleportProcess) initWindowsDesktopServiceRegistered(logger *slog clusterName := conn.ClusterName() authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + TEST: "desktop", ClusterName: clusterName, AccessPoint: accessPoint, LockWatcher: lockWatcher, Logger: process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentWindowsDesktop, process.id)), + Emitter: conn.Client, DeviceAuthorization: authz.DeviceAuthorizationOpts{ // Ignore the global device_trust.mode toggle, but allow role-based // settings to be applied. diff --git a/lib/service/service.go b/lib/service/service.go index ac93bf7a48d47..e75b3378ab95e 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -2595,6 +2595,7 @@ func (process *TeleportProcess) initAuthService() error { // client based on their certificate (user, server, admin, etc) authorizerOpts := authz.AuthorizerOpts{ + TEST: "initauth", ClusterName: clusterName, AccessPoint: authServer, ReadOnlyAccessPoint: authServer, @@ -2605,6 +2606,7 @@ func (process *TeleportProcess) initAuthService() error { // Auth Server does explicit device authorization. // Various Auth APIs must allow access to unauthorized devices, otherwise it // is not possible to acquire device-aware certificates in the first place. + Emitter: checkingEmitter, DeviceAuthorization: authz.DeviceAuthorizationOpts{ DisableGlobalMode: true, DisableRoleMode: true, @@ -5285,10 +5287,12 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { } authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + TEST: "initproxyapp", ClusterName: cn.GetClusterName(), AccessPoint: accessPoint, LockWatcher: lockWatcher, Logger: process.logger, + Emitter: asyncEmitter, PermitCaching: process.Config.CachePolicy.Enabled, }) if err != nil { @@ -5775,10 +5779,12 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { if listeners.kube != nil && !process.Config.Proxy.DisableReverseTunnel { authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + TEST: "initproxykub", ClusterName: clusterName, AccessPoint: accessPoint, LockWatcher: lockWatcher, Logger: process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentReverseTunnelServer, process.id)), + Emitter: asyncEmitter, PermitCaching: process.Config.CachePolicy.Enabled, }) if err != nil { @@ -5898,10 +5904,12 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { // framework. if (!listeners.db.Empty() || alpnRouter != nil) && !process.Config.Proxy.DisableReverseTunnel { authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + TEST: "initproxydb", ClusterName: clusterName, AccessPoint: accessPoint, LockWatcher: lockWatcher, Logger: process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentReverseTunnelServer, process.id)), + Emitter: asyncEmitter, PermitCaching: process.Config.CachePolicy.Enabled, }) if err != nil { @@ -6653,6 +6661,16 @@ func (process *TeleportProcess) initApps() { } } + asyncEmitter, err := process.NewAsyncEmitter(conn.Client) + if err != nil { + return trace.Wrap(err) + } + defer func() { + if !shouldSkipCleanup { + warnOnErr(process.ExitContext(), asyncEmitter.Close(), logger) + } + }() + lockWatcher, err := services.NewLockWatcher(process.ExitContext(), services.LockWatcherConfig{ ResourceWatcherConfig: services.ResourceWatcherConfig{ Component: teleport.ComponentApp, @@ -6664,6 +6682,7 @@ func (process *TeleportProcess) initApps() { return trace.Wrap(err) } authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + TEST: "initapps", ClusterName: clusterName, AccessPoint: accessPoint, LockWatcher: lockWatcher, @@ -6673,6 +6692,7 @@ func (process *TeleportProcess) initApps() { // settings to be applied. DisableGlobalMode: true, }, + Emitter: asyncEmitter, PermitCaching: process.Config.CachePolicy.Enabled, }) if err != nil { @@ -6683,16 +6703,6 @@ func (process *TeleportProcess) initApps() { return trace.Wrap(err) } - asyncEmitter, err := process.NewAsyncEmitter(conn.Client) - if err != nil { - return trace.Wrap(err) - } - defer func() { - if !shouldSkipCleanup { - warnOnErr(process.ExitContext(), asyncEmitter.Close(), logger) - } - }() - proxyGetter := reversetunnel.NewConnectedProxyGetter() connMonitor, err := srv.NewConnectionMonitor(srv.ConnectionMonitorConfig{ @@ -7232,10 +7242,12 @@ func (process *TeleportProcess) initSecureGRPCServer(cfg initSecureGRPCServerCfg } authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + TEST: "initsecureGRPCServer", ClusterName: clusterName, AccessPoint: cfg.accessPoint, LockWatcher: cfg.lockWatcher, Logger: process.logger.With(teleport.ComponentKey, teleport.Component(teleport.ComponentProxySecureGRPC, process.id)), + Emitter: cfg.emitter, PermitCaching: process.Config.CachePolicy.Enabled, }) if err != nil {