Files
teleport/lib/auth/auth.go
T
Dan Upton 165ec1fdb4 MWI: Generate AutoUpdateBotInstanceReport resource (#59738)
* Add `AutoUpdateBotReport` resource definition

* Generate bot version report once per minute

* Add gRPC endpoint for reading bot report

* Add updater info to the bot heartbeat message

* Fix a couple of minor typos

* Add forgotten cache event plumbing

* Fix duplicate import

* Fix racy test

* Add missing license header

* Fill HostUUID in tests

* Rename "bot report" to "bot instance report"

* Fix formatting

* Make one of the expected values different

* Defend against expired instances being counted

* Closer align `AutoUpdateBotInstanceReport` resource with RFD 153

* Add endpoint for deleting report
2025-10-07 14:32:36 +00:00

8476 lines
291 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package auth implements certificate signing authority and access control server
// Authority server is composed of several parts:
//
// * Authority server itself that implements signing and acl logic
// * HTTP server wrapper for authority server
// * HTTP client wrapper
package auth
import (
"bytes"
"cmp"
"context"
"crypto"
"crypto/rand"
"crypto/subtle"
"crypto/x509"
"encoding/base32"
"encoding/pem"
"errors"
"fmt"
"io"
"log/slog"
mathrand "math/rand/v2"
"net"
"os"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/google/uuid"
liblicense "github.com/gravitational/license"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh"
"golang.org/x/time/rate"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/accessrequest"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
scopesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
mfa "github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/wrappers"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/clientutils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/keys/hardwarekey"
"github.com/gravitational/teleport/api/utils/retryutils"
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/entitlements"
prehogv1a "github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/auth/keystore"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
"github.com/gravitational/teleport/lib/auth/machineid/workloadidentityv1"
"github.com/gravitational/teleport/lib/auth/okta"
"github.com/gravitational/teleport/lib/auth/recordingencryption"
"github.com/gravitational/teleport/lib/auth/summarizer"
"github.com/gravitational/teleport/lib/auth/userloginstate"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/azuredevops"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/bitbucket"
"github.com/gravitational/teleport/lib/boundkeypair"
"github.com/gravitational/teleport/lib/cache"
"github.com/gravitational/teleport/lib/circleci"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/decision"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/devicetrust/assertserver"
dtconfig "github.com/gravitational/teleport/lib/devicetrust/config"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/gcp"
"github.com/gravitational/teleport/lib/githubactions"
"github.com/gravitational/teleport/lib/gitlab"
"github.com/gravitational/teleport/lib/integrations/awsra/createsession"
"github.com/gravitational/teleport/lib/inventory"
iterstream "github.com/gravitational/teleport/lib/itertools/stream"
joinboundkeypair "github.com/gravitational/teleport/lib/join/boundkeypair"
kubetoken "github.com/gravitational/teleport/lib/kube/token"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/loginrule"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/observability/metrics"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/release"
"github.com/gravitational/teleport/lib/resourceusage"
"github.com/gravitational/teleport/lib/scopes"
scopedaccesscache "github.com/gravitational/teleport/lib/scopes/cache/access"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/spacelift"
"github.com/gravitational/teleport/lib/srv/db/common/role"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/terraformcloud"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/tpm"
usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/interval"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/lib/utils/set"
vc "github.com/gravitational/teleport/lib/versioncontrol"
"github.com/gravitational/teleport/lib/versioncontrol/github"
uw "github.com/gravitational/teleport/lib/versioncontrol/upgradewindow"
)
const (
ErrFieldKeyUserMaxedAttempts = "maxed-attempts"
// MaxFailedAttemptsErrMsg is a user friendly error message that tells a user that they are locked.
MaxFailedAttemptsErrMsg = "too many incorrect attempts, please try again later"
)
const (
// githubCacheTimeout is how long Github org entries are cached.
githubCacheTimeout = time.Hour
// mfaDeviceNameMaxLen is the maximum length of a device name.
mfaDeviceNameMaxLen = 30
)
const (
OSSDesktopsCheckPeriod = 5 * time.Minute
OSSDesktopsAlertID = "oss-desktops"
OSSDesktopsAlertMessage = "Your cluster is beyond its allocation of 5 non-Active Directory Windows desktops. " +
"Reach out for unlimited desktops with Teleport Enterprise."
OSSDesktopsAlertLink = "https://goteleport.com/r/upgrade-community?utm_campaign=CTA_windows_local"
OSSDesktopsAlertLinkText = "Contact Sales"
OSSDesktopsLimit = 5
)
const (
dynamicLabelCheckPeriod = time.Hour
dynamicLabelAlertID = "dynamic-labels-in-deny-rules"
dynamicLabelAlertMessage = "One or more roles has deny rules that include dynamic/ labels. " +
"This is not recommended due to the volatility of dynamic/ labels and is not allowed for new roles. " +
"(hint: use 'tctl get roles' to find roles that need updating)"
)
const (
notificationsPageReadInterval = 5 * time.Millisecond
notificationsWriteInterval = 40 * time.Millisecond
accessListsPageReadInterval = 5 * time.Millisecond
)
var ErrRequiresEnterprise = services.ErrRequiresEnterprise
// ServerOption allows setting options as functional arguments to Server
type ServerOption func(*Server) error
// NewServer creates and configures a new Server instance
func NewServer(cfg *InitConfig, opts ...ServerOption) (as *Server, err error) {
closeCtx, cancelFunc := context.WithCancel(context.TODO())
defer func() {
if err != nil {
cancelFunc()
}
}()
err = metrics.RegisterPrometheusCollectors(prometheusCollectors...)
if err != nil {
return nil, trace.Wrap(err)
}
if cfg.VersionStorage == nil {
return nil, trace.BadParameter("version storage is not set")
}
if cfg.Trust == nil {
cfg.Trust = local.NewCAService(cfg.Backend)
}
if cfg.Presence == nil {
cfg.Presence = local.NewPresenceService(cfg.Backend)
}
if cfg.Provisioner == nil {
cfg.Provisioner = local.NewProvisioningService(cfg.Backend)
}
if cfg.Identity == nil {
cfg.Identity, err = local.NewIdentityService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.Access == nil {
cfg.Access = local.NewAccessService(cfg.Backend)
}
if cfg.DynamicAccessExt == nil {
cfg.DynamicAccessExt = local.NewDynamicAccessService(cfg.Backend)
}
if cfg.ClusterConfiguration == nil {
clusterConfig, err := local.NewClusterConfigurationService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
cfg.ClusterConfiguration = clusterConfig
}
if cfg.KeyStore == nil {
keystoreOpts := &keystore.Options{
HostUUID: cfg.HostUUID,
ClusterName: cfg.ClusterName,
AuthPreferenceGetter: cfg.ClusterConfiguration,
FIPS: cfg.FIPS,
}
if cfg.KeyStoreConfig.PKCS11 != (servicecfg.PKCS11Config{}) {
if !modules.GetModules().Features().GetEntitlement(entitlements.HSM).Enabled {
return nil, fmt.Errorf("PKCS11 HSM support requires a license with the HSM feature enabled: %w", ErrRequiresEnterprise)
}
} else if cfg.KeyStoreConfig.GCPKMS != (servicecfg.GCPKMSConfig{}) {
if !modules.GetModules().Features().GetEntitlement(entitlements.HSM).Enabled {
return nil, fmt.Errorf("GCP KMS support requires a license with the HSM feature enabled: %w", ErrRequiresEnterprise)
}
} else if cfg.KeyStoreConfig.AWSKMS != nil {
if !modules.GetModules().Features().GetEntitlement(entitlements.HSM).Enabled {
return nil, fmt.Errorf("AWS KMS support requires a license with the HSM feature enabled: %w", ErrRequiresEnterprise)
}
}
cfg.KeyStore, err = keystore.NewManager(context.Background(), &cfg.KeyStoreConfig, keystoreOpts)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.RecordingEncryption == nil {
localRecordingEncryption, err := local.NewRecordingEncryptionService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
recordingEncryptionManager, err := recordingencryption.NewManager(closeCtx, recordingencryption.ManagerConfig{
Backend: localRecordingEncryption,
Cache: localRecordingEncryption,
ClusterConfig: cfg.ClusterConfiguration,
KeyStore: cfg.KeyStore,
Logger: cfg.Logger,
InitialSessionRecordingConfig: cfg.SessionRecordingConfig,
LockConfig: backend.RunWhileLockedConfig{
LockConfiguration: backend.LockConfiguration{
Backend: cfg.Backend,
TTL: time.Second * 30,
LockNameComponents: []string{"recording_encryption"},
},
},
})
if err != nil {
return nil, trace.Wrap(err, "initializing session recording encryption")
}
cfg.RecordingEncryption = recordingEncryptionManager
cfg.ClusterConfiguration = recordingEncryptionManager
}
if cfg.AutoUpdateService == nil {
cfg.AutoUpdateService, err = local.NewAutoUpdateService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.Restrictions == nil {
cfg.Restrictions = local.NewRestrictionsService(cfg.Backend)
}
if cfg.Apps == nil {
cfg.Apps = local.NewAppService(cfg.Backend)
}
if cfg.Databases == nil {
cfg.Databases = local.NewDatabasesService(cfg.Backend)
}
if cfg.DatabaseServices == nil {
cfg.DatabaseServices = local.NewDatabaseServicesService(cfg.Backend)
}
if cfg.Kubernetes == nil {
cfg.Kubernetes = local.NewKubernetesService(cfg.Backend)
}
if cfg.Status == nil {
cfg.Status = local.NewStatusService(cfg.Backend)
}
if cfg.Events == nil {
cfg.Events = local.NewEventsService(cfg.Backend)
}
if cfg.AuditLog == nil {
cfg.AuditLog = events.NewDiscardAuditLog()
}
if cfg.Emitter == nil {
cfg.Emitter = events.NewDiscardEmitter()
}
if cfg.Streamer == nil {
cfg.Streamer = events.NewDiscardStreamer()
}
if cfg.WindowsDesktops == nil {
cfg.WindowsDesktops = local.NewWindowsDesktopService(cfg.Backend)
}
if cfg.DynamicWindowsDesktops == nil {
cfg.DynamicWindowsDesktops, err = local.NewDynamicWindowsDesktopService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.SAMLIdPServiceProviders == nil {
cfg.SAMLIdPServiceProviders, err = local.NewSAMLIdPServiceProviderService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.UserGroups == nil {
cfg.UserGroups, err = local.NewUserGroupService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.CrownJewels == nil {
cfg.CrownJewels, err = local.NewCrownJewelsService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.ConnectionsDiagnostic == nil {
cfg.ConnectionsDiagnostic = local.NewConnectionsDiagnosticService(cfg.Backend)
}
if cfg.SessionTrackerService == nil {
cfg.SessionTrackerService, err = local.NewSessionTrackerService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.AssertionReplayService == nil {
cfg.AssertionReplayService = local.NewAssertionReplayService(cfg.Backend)
}
if cfg.TraceClient == nil {
cfg.TraceClient = tracing.NewNoopClient()
}
if cfg.UsageReporter == nil {
cfg.UsageReporter = usagereporter.DiscardUsageReporter{}
}
if cfg.Okta == nil {
cfg.Okta, err = local.NewOktaService(cfg.Backend, cfg.Clock)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.SecReports == nil {
cfg.SecReports, err = local.NewSecReportsService(cfg.Backend, cfg.Clock)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.AccessLists == nil {
cfg.AccessLists, err = local.NewAccessListService(cfg.Backend, cfg.Clock, local.WithRunWhileLockedRetryInterval(cfg.RunWhileLockedRetryInterval))
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.DatabaseObjectImportRules == nil {
cfg.DatabaseObjectImportRules, err = local.NewDatabaseObjectImportRuleService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.DatabaseObjects == nil {
cfg.DatabaseObjects, err = local.NewDatabaseObjectService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.Plugins == nil {
cfg.Plugins = local.NewPluginsService(cfg.Backend)
}
if cfg.PluginData == nil {
cfg.PluginData = local.NewPluginData(cfg.Backend, cfg.DynamicAccessExt)
}
if cfg.Integrations == nil {
cfg.Integrations, err = local.NewIntegrationsService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.PluginStaticCredentials == nil {
cfg.PluginStaticCredentials, err = local.NewPluginStaticCredentialsService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.UserTasks == nil {
cfg.UserTasks, err = local.NewUserTasksService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.DiscoveryConfigs == nil {
cfg.DiscoveryConfigs, err = local.NewDiscoveryConfigService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.UserPreferences == nil {
cfg.UserPreferences = local.NewUserPreferencesService(cfg.Backend)
}
if cfg.UserLoginState == nil {
cfg.UserLoginState, err = local.NewUserLoginStateService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.ProvisioningStates == nil {
cfg.ProvisioningStates, err = local.NewProvisioningStateService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.IdentityCenter == nil {
svcCfg := local.IdentityCenterServiceConfig{Backend: cfg.Backend}
cfg.IdentityCenter, err = local.NewIdentityCenterService(svcCfg)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.Notifications == nil {
cfg.Notifications, err = local.NewNotificationsService(cfg.Backend, cfg.Clock)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.BotInstance == nil {
cfg.BotInstance, err = local.NewBotInstanceService(cfg.Backend, cfg.Clock)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.SPIFFEFederations == nil {
cfg.SPIFFEFederations, err = local.NewSPIFFEFederationService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating SPIFFEFederation service")
}
}
if cfg.GitServers == nil {
cfg.GitServers, err = local.NewGitServerService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating GitServer service")
}
}
if cfg.WorkloadIdentity == nil {
workloadIdentity, err := local.NewWorkloadIdentityService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating WorkloadIdentity service")
}
cfg.WorkloadIdentity = workloadIdentity
}
if cfg.Summarizer == nil {
summarizer, err := local.NewSummarizerService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating Summarizer service")
}
cfg.Summarizer = summarizer
}
if cfg.SessionSummarizerProvider == nil {
cfg.SessionSummarizerProvider = summarizer.NewSessionSummarizerProvider()
}
if cfg.WorkloadIdentityX509Revocations == nil {
cfg.WorkloadIdentityX509Revocations, err = local.NewWorkloadIdentityX509RevocationService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating WorkloadIdentityX509Revocation service")
}
}
if cfg.WorkloadIdentityX509Overrides == nil {
cfg.WorkloadIdentityX509Overrides, err = local.NewWorkloadIdentityX509OverridesService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating WorkloadIdentityX509Overrides service")
}
}
if cfg.SigstorePolicies == nil {
cfg.SigstorePolicies, err = local.NewSigstorePolicyService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating SigstorePolicies service")
}
}
if cfg.StableUNIXUsers == nil {
cfg.StableUNIXUsers = &local.StableUNIXUsersService{
Backend: cfg.Backend,
}
}
if cfg.HealthCheckConfig == nil {
cfg.HealthCheckConfig, err = local.NewHealthCheckConfigService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating HealthCheckConfigs service")
}
}
if cfg.BackendInfo == nil {
cfg.BackendInfo, err = local.NewBackendInfoService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating BackendInfo service")
}
}
if cfg.VnetConfigService == nil {
cfg.VnetConfigService, err = local.NewVnetConfigService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err, "creating VnetConfigService")
}
}
if cfg.ScopedAccess == nil {
cfg.ScopedAccess = local.NewScopedAccessService(cfg.Backend)
}
if cfg.Logger == nil {
cfg.Logger = slog.With(teleport.ComponentKey, teleport.ComponentAuth)
}
limiter := limiter.NewConnectionsLimiter(defaults.LimiterMaxConcurrentSignatures)
if cfg.KubeWaitingContainers == nil {
cfg.KubeWaitingContainers, err = local.NewKubeWaitingContainerService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.AccessMonitoringRules == nil {
cfg.AccessMonitoringRules, err = local.NewAccessMonitoringRulesService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
if cfg.StaticHostUsers == nil {
cfg.StaticHostUsers, err = local.NewStaticHostUserService(cfg.Backend)
if err != nil {
return nil, trace.Wrap(err)
}
}
scopedAccessCache, err := scopedaccesscache.NewCache(scopedaccesscache.CacheConfig{
Events: cfg.Events,
Reader: cfg.ScopedAccess,
})
if err != nil {
return nil, trace.Wrap(err)
}
services := &Services{
TrustInternal: cfg.Trust,
PresenceInternal: cfg.Presence,
Provisioner: cfg.Provisioner,
Identity: cfg.Identity,
Access: cfg.Access,
DynamicAccessExt: cfg.DynamicAccessExt,
ClusterConfigurationInternal: cfg.ClusterConfiguration,
AutoUpdateService: cfg.AutoUpdateService,
Restrictions: cfg.Restrictions,
Applications: cfg.Apps,
Kubernetes: cfg.Kubernetes,
Databases: cfg.Databases,
DatabaseServices: cfg.DatabaseServices,
AuditLogSessionStreamer: cfg.AuditLog,
Events: cfg.Events,
WindowsDesktops: cfg.WindowsDesktops,
DynamicWindowsDesktops: cfg.DynamicWindowsDesktops,
SAMLIdPServiceProviders: cfg.SAMLIdPServiceProviders,
UserGroups: cfg.UserGroups,
SessionTrackerService: cfg.SessionTrackerService,
ConnectionsDiagnostic: cfg.ConnectionsDiagnostic,
Integrations: cfg.Integrations,
UserTasks: cfg.UserTasks,
DiscoveryConfigs: cfg.DiscoveryConfigs,
Okta: cfg.Okta,
AccessLists: cfg.AccessLists,
DatabaseObjectImportRules: cfg.DatabaseObjectImportRules,
DatabaseObjects: cfg.DatabaseObjects,
SecReports: cfg.SecReports,
UserLoginStates: cfg.UserLoginState,
StatusInternal: cfg.Status,
UsageReporter: cfg.UsageReporter,
UserPreferences: cfg.UserPreferences,
PluginData: cfg.PluginData,
KubeWaitingContainer: cfg.KubeWaitingContainers,
Notifications: cfg.Notifications,
AccessMonitoringRules: cfg.AccessMonitoringRules,
CrownJewels: cfg.CrownJewels,
BotInstance: cfg.BotInstance,
SPIFFEFederations: cfg.SPIFFEFederations,
StaticHostUser: cfg.StaticHostUsers,
ProvisioningStates: cfg.ProvisioningStates,
IdentityCenter: cfg.IdentityCenter,
Plugins: cfg.Plugins,
PluginStaticCredentials: cfg.PluginStaticCredentials,
GitServers: cfg.GitServers,
WorkloadIdentities: cfg.WorkloadIdentity,
StableUNIXUsersInternal: cfg.StableUNIXUsers,
WorkloadIdentityX509Revocations: cfg.WorkloadIdentityX509Revocations,
WorkloadIdentityX509Overrides: cfg.WorkloadIdentityX509Overrides,
SigstorePolicies: cfg.SigstorePolicies,
HealthCheckConfig: cfg.HealthCheckConfig,
BackendInfoService: cfg.BackendInfo,
VnetConfigService: cfg.VnetConfigService,
RecordingEncryptionManager: cfg.RecordingEncryption,
MultipartHandler: cfg.MultipartHandler,
Summarizer: cfg.Summarizer,
}
as = &Server{
bk: cfg.Backend,
clock: cfg.Clock,
limiter: limiter,
Authority: cfg.Authority,
AuthServiceName: cfg.AuthServiceName,
ServerID: cfg.HostUUID,
cancelFunc: cancelFunc,
closeCtx: closeCtx,
emitter: cfg.Emitter,
Streamer: cfg.Streamer,
Unstable: local.NewUnstableService(cfg.Backend, cfg.AssertionReplayService),
Services: services,
Cache: services,
scopedAccessBackend: cfg.ScopedAccess,
ScopedAccessCache: scopedAccessCache,
keyStore: cfg.KeyStore,
traceClient: cfg.TraceClient,
fips: cfg.FIPS,
loadAllCAs: cfg.LoadAllCAs,
httpClientForAWSSTS: cfg.HTTPClientForAWSSTS,
accessMonitoringEnabled: cfg.AccessMonitoringEnabled,
logger: cfg.Logger,
sessionSummarizerProvider: cfg.SessionSummarizerProvider,
}
as.inventory = inventory.NewController(as, services,
inventory.WithAuthServerID(cfg.HostUUID),
inventory.WithClock(cfg.Clock),
inventory.WithOnConnect(func(s string) {
if g, ok := connectedResourceGauges[s]; ok {
g.Inc()
} else {
as.logger.WarnContext(closeCtx, "missing connected resources gauge for keep alive (this is a bug)", "keep_alive_type", s)
}
}),
inventory.WithOnDisconnect(func(s string, c int) {
if g, ok := connectedResourceGauges[s]; ok {
g.Sub(float64(c))
} else {
as.logger.WarnContext(closeCtx, "missing connected resources gauge for keep alive (this is a bug)", "keep_alive_type", s)
}
}),
)
for _, o := range opts {
if err := o(as); err != nil {
return nil, trace.Wrap(err)
}
}
if as.clock == nil {
as.clock = clockwork.NewRealClock()
}
as.githubOrgSSOCache, err = utils.NewFnCache(utils.FnCacheConfig{
TTL: githubCacheTimeout,
})
if err != nil {
return nil, trace.Wrap(err)
}
as.ttlCache, err = utils.NewFnCache(utils.FnCacheConfig{
TTL: time.Second * 3,
})
if err != nil {
return nil, trace.Wrap(err)
}
_, cacheEnabled := as.getCache()
// cluster config ttl cache *must* be set up after `opts` has been applied to the server because
// the Cache field starts off as a pointer to the local backend services and is only switched
// over to being a proper cache during option processing.
as.ReadOnlyCache, err = readonly.NewCache(readonly.CacheConfig{
Upstream: as.Cache,
Disabled: !cacheEnabled,
ReloadOnErr: true,
})
if err != nil {
return nil, trace.Wrap(err)
}
if as.ghaIDTokenValidator == nil {
as.ghaIDTokenValidator = githubactions.NewIDTokenValidator(
githubactions.IDTokenValidatorConfig{},
)
}
if as.ghaIDTokenJWKSValidator == nil {
as.ghaIDTokenJWKSValidator = githubactions.ValidateTokenWithJWKS
}
if as.spaceliftIDTokenValidator == nil {
as.spaceliftIDTokenValidator = spacelift.NewIDTokenValidator(
spacelift.IDTokenValidatorConfig{},
)
}
if as.gitlabIDTokenValidator == nil {
as.gitlabIDTokenValidator, err = gitlab.NewIDTokenValidator(
gitlab.IDTokenValidatorConfig{
Clock: as.clock,
ClusterNameGetter: services,
},
)
if err != nil {
return nil, trace.Wrap(err)
}
}
if as.azureDevopsIDTokenValidator == nil {
as.azureDevopsIDTokenValidator = azuredevops.NewIDTokenValidator()
}
if as.circleCITokenValidate == nil {
as.circleCITokenValidate = func(
ctx context.Context, organizationID, token string,
) (*circleci.IDTokenClaims, error) {
return circleci.ValidateToken(
ctx, circleci.IssuerURLTemplate, organizationID, token,
)
}
}
if as.tpmValidator == nil {
as.tpmValidator = tpm.Validate
}
if as.k8sTokenReviewValidator == nil {
as.k8sTokenReviewValidator = &kubetoken.TokenReviewValidator{}
}
if as.k8sJWKSValidator == nil {
as.k8sJWKSValidator = kubetoken.ValidateTokenWithJWKS
}
if as.k8sOIDCValidator == nil {
validator, err := kubetoken.NewKubernetesOIDCTokenValidator()
if err != nil {
return nil, trace.Wrap(err)
}
as.k8sOIDCValidator = validator
}
if as.gcpIDTokenValidator == nil {
as.gcpIDTokenValidator = gcp.NewIDTokenValidator(
gcp.IDTokenValidatorConfig{},
)
}
if as.terraformIDTokenValidator == nil {
as.terraformIDTokenValidator = terraformcloud.NewIDTokenValidator(terraformcloud.IDTokenValidatorConfig{})
}
if as.bitbucketIDTokenValidator == nil {
as.bitbucketIDTokenValidator = bitbucket.NewIDTokenValidator()
}
if as.createBoundKeypairValidator == nil {
as.createBoundKeypairValidator = func(subject, clusterName string, publicKey crypto.PublicKey) (joinboundkeypair.BoundKeypairValidator, error) {
return boundkeypair.NewChallengeValidator(subject, clusterName, publicKey)
}
}
// Add in a login hook for generating state during user login.
as.ulsGenerator, err = userloginstate.NewGenerator(userloginstate.GeneratorConfig{
Log: as.logger,
AccessLists: as,
Access: as,
UsageEvents: as,
Clock: cfg.Clock,
Emitter: as.emitter,
})
if err != nil {
return nil, trace.Wrap(err)
}
as.RegisterLoginHook(as.ulsGenerator.LoginHook(services.UserLoginStates))
as.pdp, err = decision.NewService(decision.Config{
AccessPoint: as.Cache,
ULSGenerator: as.ulsGenerator,
})
if err != nil {
return nil, trace.Wrap(err)
}
as.botVersionReporter, err = machineidv1.NewAutoUpdateVersionReporter(machineidv1.AutoUpdateVersionReporterConfig{
Clock: cfg.Clock,
Logger: as.logger.With(
teleport.ComponentKey,
teleport.Component(teleport.ComponentAuth, "bot-version-reporter"),
),
Semaphores: as,
HostUUID: cfg.HostUUID,
Store: as,
Cache: as.Cache,
})
if err != nil {
return nil, trace.Wrap(err)
}
if err := as.botVersionReporter.Run(as.CloseContext()); err != nil {
return nil, trace.Wrap(err)
}
if _, ok := as.getCache(); !ok {
as.logger.WarnContext(closeCtx, "Auth server starting without cache (may have negative performance implications)")
}
return as, nil
}
// Services is a collection of services that are used by the auth server.
// Avoid using this type as a dependency and instead depend on the actual
// methods/services you need. It should really only be necessary to directly
// reference this type on auth.Server itself and on code that manages
// the lifecycle of the auth server.
type Services struct {
services.TrustInternal
services.PresenceInternal
services.Provisioner
services.Identity
services.Access
services.DynamicAccessExt
services.ClusterConfigurationInternal
services.Restrictions
services.Applications
services.Kubernetes
services.Databases
services.DatabaseServices
services.WindowsDesktops
services.DynamicWindowsDesktops
services.SAMLIdPServiceProviders
services.UserGroups
services.SessionTrackerService
services.ConnectionsDiagnostic
services.StatusInternal
services.Integrations
services.IntegrationsTokenGenerator
services.UserTasks
services.DiscoveryConfigs
services.Okta
services.AccessLists
services.DatabaseObjectImportRules
services.DatabaseObjects
services.UserLoginStates
services.UserPreferences
services.PluginData
services.SCIM
services.Notifications
usagereporter.UsageReporter
types.Events
events.AuditLogSessionStreamer
services.SecReports
services.KubeWaitingContainer
services.AccessMonitoringRules
services.CrownJewels
services.BotInstance
services.AccessGraphSecretsGetter
services.DevicesGetter
services.SPIFFEFederations
services.StaticHostUser
services.AutoUpdateService
services.ProvisioningStates
services.IdentityCenter
services.Plugins
services.PluginStaticCredentials
services.GitServers
services.WorkloadIdentities
services.StableUNIXUsersInternal
services.WorkloadIdentityX509Revocations
services.WorkloadIdentityX509Overrides
services.SigstorePolicies
services.HealthCheckConfig
services.BackendInfoService
services.VnetConfigService
RecordingEncryptionManager
events.MultipartHandler
services.Summarizer
}
// GetWebSession returns existing web session described by req.
// Implements ReadAccessPoint
func (r *Services) GetWebSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
return r.Identity.WebSessions().Get(ctx, req)
}
// GenerateAWSOIDCToken generates a token to be used to execute an AWS OIDC Integration action.
func (r *Services) GenerateAWSOIDCToken(ctx context.Context, integration string) (string, error) {
return r.IntegrationsTokenGenerator.GenerateAWSOIDCToken(ctx, integration)
}
// GenerateAzureOIDCToken generates a token to be used to execute an Azure OIDC Integration action.
func (r *Services) GenerateAzureOIDCToken(ctx context.Context, integration string) (string, error) {
return r.IntegrationsTokenGenerator.GenerateAzureOIDCToken(ctx, integration)
}
var (
generateRequestsCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricGenerateRequests,
Help: "Number of requests to generate new server keys",
},
)
generateThrottledRequestsCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricGenerateRequestsThrottled,
Help: "Number of throttled requests to generate new server keys",
},
)
generateRequestsCurrent = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: teleport.MetricGenerateRequestsCurrent,
Help: "Number of current generate requests for server keys",
},
)
generateRequestsLatencies = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: teleport.MetricGenerateRequestsHistogram,
Help: "Latency for generate requests for server keys",
// lowest bucket start of upper bound 0.001 sec (1 ms) with factor 2
// highest bucket start of 0.001 sec * 2^15 == 32.768 sec
Buckets: prometheus.ExponentialBuckets(0.001, 2, 16),
},
)
// UserLoginCount counts user logins
UserLoginCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricUserLoginCount,
Help: "Number of times there was a user login",
},
)
heartbeatsMissedByAuth = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: teleport.MetricHeartbeatsMissed,
Help: "Number of heartbeats missed by auth server",
},
)
roleCount = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: teleport.MetricNamespace,
Name: "roles_total",
Help: "Number of roles that exist in the cluster",
},
)
registeredAgents = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricRegisteredServers,
Help: "The number of Teleport services that are connected to an auth server.",
},
[]string{
teleport.TagOS,
teleport.TagVersion,
teleport.TagAutomaticUpdates,
},
)
registeredAgentsInstallMethod = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricRegisteredServersByInstallMethods,
Help: "The number of Teleport services that are connected to an auth server by install method.",
},
[]string{teleport.TagInstallMethods},
)
migrations = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricMigrations,
Help: "Migrations tracks for each migration if it is active (1) or not (0).",
},
[]string{teleport.TagMigration},
)
totalInstancesMetric = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricTotalInstances,
Help: "Total teleport instances",
},
)
enrolledInUpgradesMetric = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricEnrolledInUpgrades,
Help: "Number of instances enrolled in automatic upgrades",
},
)
upgraderCountsMetric = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricUpgraderCounts,
Help: "Tracks the number of instances advertising each upgrader",
},
[]string{
teleport.TagUpgrader,
teleport.TagVersion,
},
)
accessRequestsCreatedMetric = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricAccessRequestsCreated,
Help: "Tracks the number of created access requests",
},
[]string{teleport.TagRoles, teleport.TagResources},
)
userCertificatesGeneratedMetric = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: teleport.MetricNamespace,
Name: teleport.MetricUserCertificatesGenerated,
Help: "Tracks the number of user certificates generated",
},
[]string{teleport.TagPrivateKeyPolicy},
)
prometheusCollectors = []prometheus.Collector{
generateRequestsCount, generateThrottledRequestsCount,
generateRequestsCurrent, generateRequestsLatencies, UserLoginCount, heartbeatsMissedByAuth,
registeredAgents, migrations,
totalInstancesMetric, enrolledInUpgradesMetric, upgraderCountsMetric,
accessRequestsCreatedMetric,
registeredAgentsInstallMethod,
userCertificatesGeneratedMetric,
roleCount,
}
)
// LoginHook is a function that will be called on a successful login. This will likely be used
// for enterprise services that need to add in feature specific operations after a user has been
// successfully authenticated. An example would be creating objects based on the user.
type LoginHook func(context.Context, types.User) error
// CreateDeviceWebTokenFunc creates a new DeviceWebToken for the logged in user.
//
// Used during a successful Web login, after the user was verified and the
// WebSession created.
//
// May return `nil, nil` if device trust isn't supported (OSS), disabled, or if
// the user has no suitable trusted device.
type CreateDeviceWebTokenFunc func(context.Context, *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error)
// CreateDeviceAssertionFunc creates a new device assertion ceremony to authenticate
// a trusted device.
type CreateDeviceAssertionFunc func() (assertserver.Ceremony, error)
// ReadOnlyCache is a type alias used to assist with embedding [readonly.Cache] in places
// where it would have a naming conflict with other types named Cache.
type ReadOnlyCache = readonly.Cache
// Server keeps the cluster together. It acts as a certificate authority (CA) for
// a cluster and:
// - generates the keypair for the node it's running on
// - invites other SSH nodes to a cluster, by issuing invite tokens
// - adds other SSH nodes to a cluster, by checking their token and signing their keys
// - same for users and their sessions
// - checks public keys to see if they're signed by it (can be trusted or not)
type Server struct {
lock sync.RWMutex
clock clockwork.Clock
bk backend.Backend
closeCtx context.Context
cancelFunc context.CancelFunc
samlAuthService SAMLService
oidcAuthService OIDCService
releaseService release.Client
loginRuleEvaluator loginrule.Evaluator
sshca.Authority
upgradeWindowStartHourGetter func(context.Context) (int64, error)
// AuthServiceName is a human-readable name of this CA. If several Auth services are running
// (managing multiple teleport clusters) this field is used to tell them apart in UIs
// It usually defaults to the hostname of the machine the Auth service runs on.
AuthServiceName string
// ServerID is the server ID of this auth server.
ServerID string
// Unstable implements Unstable backend methods not suitable
// for inclusion in Services.
Unstable local.UnstableService
// Services encapsulate services - provisioner, trust, etc. used by the auth
// server in a separate structure. Reads through Services hit the backend.
*Services
// Cache should either be the same as Services, or a caching layer over it.
// As it's an interface (and thus directly implementing all of its methods)
// its embedding takes priority over Services (which only indirectly
// implements its methods), thus any implemented GetFoo method on both Cache
// and Services will call the one from Cache. To bypass the cache, call the
// method on Services instead.
authclient.Cache
// ScopedAccessCache is a specialized cache that provides read methods for select
// scoped access control resources.
ScopedAccessCache *scopedaccesscache.Cache
// scopedAccessBackend is the backend service for scoped access control resources.
scopedAccessBackend services.ScopedAccess
// ReadOnlyCache is a specialized cache that provides read-only shared references
// in certain performance-critical paths where deserialization/cloning may be too
// expensive at scale.
*ReadOnlyCache
// privateKey is used in tests to use pre-generated private keys
privateKey []byte
// cipherSuites is a list of ciphersuites that the auth server supports.
cipherSuites []uint16
// limiter limits the number of active connections per client IP.
limiter *limiter.ConnectionsLimiter
// Emitter is events emitter, used to submit discrete events
emitter apievents.Emitter
// Streamer is an events session streamer, used to create continuous
// session related streams
events.Streamer
// keyStore manages all CA private keys, which may or may not be backed by
// HSMs
keyStore *keystore.Manager
// lockWatcher is a lock watcher, used to verify cert generation requests.
lockWatcher *services.LockWatcher
// UnifiedResourceCache is a cache of multiple resource kinds to be presented
// in a unified manner in the web UI.
UnifiedResourceCache *services.UnifiedResourceCache
// AccessRequestCache is a cache of access requests that specifically provides
// custom sorting options not available via the standard backend.
AccessRequestCache *services.AccessRequestCache
// UserNotificationCache is a cache of user-specific notifications.
UserNotificationCache *services.UserNotificationCache
// GlobalNotificationCache is a cache of global notifications.
GlobalNotificationCache *services.GlobalNotificationCache
// workloadIdentityX509CAOverrideGetter is a getter for CA overrides for
// SPIFFE X.509 certificate issuance. Optional, set in enterprise code.
workloadIdentityX509CAOverrideGetter services.WorkloadIdentityX509CAOverrideGetter
inventory *inventory.Controller
pdp *decision.Service
// githubOrgSSOCache is used to cache whether Github organizations use
// external SSO or not.
githubOrgSSOCache *utils.FnCache
// ttlCache is a generic ttl cache. typed keys must be used.
ttlCache *utils.FnCache
// traceClient is used to forward spans to the upstream collector for components
// within the cluster that don't have a direct connection to said collector
traceClient otlptrace.Client
// fips means FedRAMP/FIPS 140-2 compliant configuration was requested.
fips bool
// ghaIDTokenValidator allows ID tokens from GitHub Actions to be validated
// by the auth server. It can be overridden for the purpose of tests.
ghaIDTokenValidator ghaIDTokenValidator
// ghaIDTokenJWKSValidator allows ID tokens from GitHub Actions to be
// validated by the auth server using a known JWKS. It can be overridden for
// the purpose of tests.
ghaIDTokenJWKSValidator ghaIDTokenJWKSValidator
// spaceliftIDTokenValidator allows ID tokens from Spacelift to be validated
// by the auth server. It can be overridden for the purpose of tests.
spaceliftIDTokenValidator spaceliftIDTokenValidator
// gitlabIDTokenValidator allows ID tokens from GitLab CI to be validated by
// the auth server. It can be overridden for the purpose of tests.
gitlabIDTokenValidator gitlabIDTokenValidator
// azureDevopsIDTokenValidator allows ID tokens from Azure DevOps to be
// validated by the auth server. It can be overridden for the purpose of
// tests.
azureDevopsIDTokenValidator azureDevopsIDTokenValidator
// tpmValidator allows TPMs to be validated by the auth server. It can be
// overridden for the purpose of tests.
tpmValidator func(
ctx context.Context, log *slog.Logger, params tpm.ValidateParams,
) (*tpm.ValidatedTPM, error)
// circleCITokenValidate allows ID tokens from CircleCI to be validated by
// the auth server. It can be overridden for the purpose of tests.
circleCITokenValidate func(ctx context.Context, organizationID, token string) (*circleci.IDTokenClaims, error)
// k8sTokenReviewValidator allows tokens from Kubernetes to be validated
// by the auth server using k8s Token Review API. It can be overridden for
// the purpose of tests.
k8sTokenReviewValidator k8sTokenReviewValidator
// k8sJWKSValidator allows tokens from Kubernetes to be validated
// by the auth server using a known JWKS. It can be overridden for the
// purpose of tests.
k8sJWKSValidator k8sJWKSValidator
// k8sOIDCValidator allows tokens from Kubernetes to be validated by the
// auth server using a known OIDC endpoint. It can be overridden in tests.
k8sOIDCValidator *kubetoken.KubernetesOIDCTokenValidator
// gcpIDTokenValidator allows ID tokens from GCP to be validated by the auth
// server. It can be overridden for the purpose of tests.
gcpIDTokenValidator gcpIDTokenValidator
// terraformIDTokenValidator allows JWTs from Terraform Cloud to be
// validated by the auth server using a known JWKS. It can be overridden for
// the purpose of tests.
terraformIDTokenValidator terraformCloudIDTokenValidator
bitbucketIDTokenValidator bitbucketIDTokenValidator
// createBoundKeypairValidator is a helper to create new bound keypair
// challenge validators. Used to override the implementation used in tests.
createBoundKeypairValidator joinboundkeypair.CreateBoundKeypairValidator
// loadAllCAs tells tsh to load the host CAs for all clusters when trying to ssh into a node.
loadAllCAs bool
// license is the Teleport Enterprise license used to start the auth server
license *liblicense.License
// headlessAuthenticationWatcher is a headless authentication watcher,
// used to catch and propagate headless authentication request changes.
headlessAuthenticationWatcher *local.HeadlessAuthenticationWatcher
loginHooksMu sync.RWMutex
// loginHooks are a list of hooks that will be called on login.
loginHooks []LoginHook
// httpClientForAWSSTS overwrites the default HTTP client used for making
// STS requests.
httpClientForAWSSTS utils.HTTPDoClient
// accessMonitoringEnabled is a flag that indicates whether access monitoring is enabled.
accessMonitoringEnabled bool
// ulsGenerator is the user login state generator.
ulsGenerator *userloginstate.Generator
// createDeviceWebTokenFunc is the CreateDeviceWebToken implementation.
// Is nil on OSS clusters.
createDeviceWebTokenFunc CreateDeviceWebTokenFunc
// deviceAssertionServer holds the server-side implementation of device assertions.
//
// It is used to authenticate devices previously enrolled in the cluster. The goal
// is to provide an API for devices to authenticate with the cluster without the need
// for valid user credentials, e.g. when running `tsh scan keys`.
//
// The value is nil on OSS clusters.
deviceAssertionServer CreateDeviceAssertionFunc
// bcryptCostOverride overrides the bcrypt cost for operations executed
// directly by [Server].
// Used for testing.
bcryptCostOverride *int
// GithubUserAndTeamsOverride overrides the user and teams that would
// normally be fetched from the GitHub API. Used for testing.
GithubUserAndTeamsOverride func() (*GithubUserResponse, []GithubTeamResponse, error)
// AWSRolesAnywhereCreateSessionOverride overrides the AWS Roles Anywhere Create Session API wrapper with a mocked one.
// Used for testing.
AWSRolesAnywhereCreateSessionOverride func(ctx context.Context, req createsession.CreateSessionRequest) (*createsession.CreateSessionResponse, error)
// sigstorePolicyEvaluator checks workload signatures and attestations
// against Sigstore policies.
sigstorePolicyEvaluator workloadidentityv1.SigstorePolicyEvaluator
// logger is the logger used by the auth server.
logger *slog.Logger
// sessionSummarizerProvider is a provider of the session summarizer service.
// It allows for late initialization of the summarizer in the enterprise
// plugin. The summarizer itself summarizes session recordings.
sessionSummarizerProvider *summarizer.SessionSummarizerProvider
// botVersionReporter is called periodically to generate a report of the
// number of bot instances by version and update group.
botVersionReporter *machineidv1.AutoUpdateVersionReporter
}
// SetSAMLService registers svc as the SAMLService that provides the SAML
// connector implementation. If a SAMLService has already been registered, this
// will override the previous registration.
func (a *Server) SetSAMLService(svc SAMLService) {
a.samlAuthService = svc
}
// SetOIDCService registers svc as the OIDCService that provides the OIDC
// connector implementation. If a OIDCService has already been registered, this
// will override the previous registration.
func (a *Server) SetOIDCService(svc OIDCService) {
a.oidcAuthService = svc
}
// SetLicense sets the license
func (a *Server) SetLicense(license *liblicense.License) {
a.license = license
}
// SetReleaseService sets the release service
func (a *Server) SetReleaseService(svc release.Client) {
a.releaseService = svc
}
// ScopedAccess returns an implementation of services.ScopedAccess with all read methods routed
// to the scoped access cache.
func (a *Server) ScopedAccess() services.ScopedAccess {
return struct {
services.ScopedAccessReader
services.ScopedAccessWriter
}{
ScopedAccessReader: a.ScopedAccessCache,
ScopedAccessWriter: a.scopedAccessBackend,
}
}
// SetUpgradeWindowStartHourGetter sets the getter used to sync the ClusterMaintenanceConfig resource
// with the cloud UpgradeWindowStartHour value.
func (a *Server) SetUpgradeWindowStartHourGetter(fn func(context.Context) (int64, error)) {
a.lock.Lock()
defer a.lock.Unlock()
a.upgradeWindowStartHourGetter = fn
}
func (a *Server) getUpgradeWindowStartHourGetter() func(context.Context) (int64, error) {
a.lock.Lock()
defer a.lock.Unlock()
return a.upgradeWindowStartHourGetter
}
// SetLoginRuleEvaluator sets the login rule evaluator.
func (a *Server) SetLoginRuleEvaluator(l loginrule.Evaluator) {
a.loginRuleEvaluator = l
}
// GetLoginRuleEvaluator returns the login rule evaluator. It is guaranteed not
// to return nil, if no evaluator has been installed it will return
// [loginrule.NullEvaluator].
func (a *Server) GetLoginRuleEvaluator() loginrule.Evaluator {
if a.loginRuleEvaluator == nil {
return loginrule.NullEvaluator{}
}
return a.loginRuleEvaluator
}
// RegisterLoginHook will register a login hook with the auth server.
func (a *Server) RegisterLoginHook(hook LoginHook) {
a.loginHooksMu.Lock()
defer a.loginHooksMu.Unlock()
a.loginHooks = append(a.loginHooks, hook)
}
// CallLoginHooks will call the registered login hooks.
func (a *Server) CallLoginHooks(ctx context.Context, user types.User) error {
// Make a copy of the login hooks to operate on.
a.loginHooksMu.RLock()
loginHooks := make([]LoginHook, len(a.loginHooks))
copy(loginHooks, a.loginHooks)
a.loginHooksMu.RUnlock()
if len(loginHooks) == 0 {
return nil
}
// Clone the input user so that hooks never mutate the original object.
//
// Currently, login hook share state via UserLoginState resources.
// The login hook calls GetUserLoginState to read the state, updates it,
// and then saves the changes when a next looking hood loads the state from storage.
//
// Note: We intentionally do not write access-listderived roles/traits back
// to the user record. Doing so could create inconsistencies with user objects
// provisioned by Entra ID / Okta Sync or via SCIM. If we choose to persist
// these attributes in the future, we should first define a single source of
// truth and a clear reconciliation strategy.
user = user.Clone()
var errs []error
for _, hook := range loginHooks {
errs = append(errs, hook(ctx, user))
}
return trace.NewAggregate(errs...)
}
// ResetLoginHooks will clear out the login hooks.
func (a *Server) ResetLoginHooks() {
a.loginHooksMu.Lock()
a.loginHooks = nil
a.loginHooksMu.Unlock()
}
// SetSummarizerService sets an implementation of the summarizer service used
// by this server and its underlying services.
func (a *Server) SetSummarizerService(s summarizer.SessionSummarizer) {
a.sessionSummarizerProvider.SetSummarizer(s)
}
// CloseContext returns the close context
func (a *Server) CloseContext() context.Context {
return a.closeCtx
}
// SetUnifiedResourcesCache sets the unified resource cache.
func (a *Server) SetUnifiedResourcesCache(unifiedResourcesCache *services.UnifiedResourceCache) {
a.lock.Lock()
defer a.lock.Unlock()
a.UnifiedResourceCache = unifiedResourcesCache
}
// SetAccessRequestCache sets the access request cache.
func (a *Server) SetAccessRequestCache(accessRequestCache *services.AccessRequestCache) {
a.lock.Lock()
defer a.lock.Unlock()
a.AccessRequestCache = accessRequestCache
}
// SetUserNotificationsCache sets the user notification cache.
func (a *Server) SetUserNotificationCache(userNotificationCache *services.UserNotificationCache) {
a.lock.Lock()
defer a.lock.Unlock()
a.UserNotificationCache = userNotificationCache
}
// SetGlobalNotificationsCache sets the global notification cache.
func (a *Server) SetGlobalNotificationCache(globalNotificationCache *services.GlobalNotificationCache) {
a.lock.Lock()
defer a.lock.Unlock()
a.GlobalNotificationCache = globalNotificationCache
}
func (a *Server) SetLockWatcher(lockWatcher *services.LockWatcher) {
a.lock.Lock()
defer a.lock.Unlock()
a.lockWatcher = lockWatcher
}
// CheckLockInForce returns an AccessDenied error if there is a lock in force
// matching at least one of the targets.
func (a *Server) CheckLockInForce(mode constants.LockingMode, targets []types.LockTarget) error {
return a.checkLockInForce(mode, targets)
}
func (a *Server) checkLockInForce(mode constants.LockingMode, targets []types.LockTarget) error {
a.lock.RLock()
defer a.lock.RUnlock()
if a.lockWatcher == nil {
return trace.BadParameter("lockWatcher is not set")
}
return a.lockWatcher.CheckLockInForce(mode, targets...)
}
func (a *Server) SetHeadlessAuthenticationWatcher(headlessAuthenticationWatcher *local.HeadlessAuthenticationWatcher) {
a.lock.Lock()
defer a.lock.Unlock()
a.headlessAuthenticationWatcher = headlessAuthenticationWatcher
}
// SetDeviceAssertionServer sets the device assertion implementation.
func (a *Server) SetDeviceAssertionServer(f CreateDeviceAssertionFunc) {
a.lock.Lock()
a.deviceAssertionServer = f
a.lock.Unlock()
}
// GetDeviceAssertionServer returns the device assertion implementation.
// On OSS clusters, this will return a non nil function that returns an error.
func (a *Server) GetDeviceAssertionServer() CreateDeviceAssertionFunc {
a.lock.RLock()
defer a.lock.RUnlock()
if a.deviceAssertionServer == nil {
return func() (assertserver.Ceremony, error) {
return nil, trace.NotImplemented("device assertions are not supported on OSS clusters")
}
}
return a.deviceAssertionServer
}
func (a *Server) SetCreateDeviceWebTokenFunc(f CreateDeviceWebTokenFunc) {
a.lock.Lock()
a.createDeviceWebTokenFunc = f
a.lock.Unlock()
}
// createDeviceWebToken safely calls the underlying [CreateDeviceWebTokenFunc].
func (a *Server) createDeviceWebToken(ctx context.Context, webToken *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
a.lock.RLock()
defer a.lock.RUnlock()
if a.createDeviceWebTokenFunc == nil {
return nil, nil
}
token, err := a.createDeviceWebTokenFunc(ctx, webToken)
return token, trace.Wrap(err)
}
func (a *Server) bcryptCost() int {
if cost := a.bcryptCostOverride; cost != nil {
return *cost
}
return bcrypt.DefaultCost
}
// syncUpgradeWindowStartHour attempts to load the cloud UpgradeWindowStartHour value and set
// the ClusterMaintenanceConfig resource's AgentUpgrade.UTCStartHour field to match it.
func (a *Server) syncUpgradeWindowStartHour(ctx context.Context) error {
getter := a.getUpgradeWindowStartHourGetter()
if getter == nil {
return trace.Errorf("getter has not been registered")
}
startHour, err := getter(ctx)
if err != nil {
return trace.Wrap(err)
}
cmc, err := a.GetClusterMaintenanceConfig(ctx)
if err != nil {
if !trace.IsNotFound(err) {
return trace.Wrap(err)
}
// create an empty maintenance config resource on NotFound
cmc = types.NewClusterMaintenanceConfig()
}
agentWindow, _ := cmc.GetAgentUpgradeWindow()
agentWindow.UTCStartHour = uint32(startHour)
agentWindow.Weekdays = []string{"Mon", "Tue", "Wed", "Thu"}
cmc.SetAgentUpgradeWindow(agentWindow)
if err := a.UpdateClusterMaintenanceConfig(ctx, cmc); err != nil {
return trace.Wrap(err)
}
return nil
}
// periodicIntervalKey is used to uniquely identify the subintervals registered with
// the interval.MultiInterval instance that we use for managing periodics operations.
type periodicIntervalKey int
const (
heartbeatCheckKey periodicIntervalKey = 1 + iota
rotationCheckKey
metricsKey
releaseCheckKey
localReleaseCheckKey
instancePeriodicsKey
dynamicLabelsCheckKey
notificationsCleanupKey
desktopCheckKey
upgradeWindowCheckKey
roleCountKey
accessListReminderNotificationsKey
autoUpdateAgentReportKey
autoUpdateBotInstanceReportKey
)
// runPeriodicOperations runs some periodic bookkeeping operations
// performed by auth server
func (a *Server) runPeriodicOperations() {
firstReleaseCheck := retryutils.FullJitter(time.Hour * 6)
// this environment variable is "unstable" since it will be deprecated
// by an upcoming tctl command. currently exists for testing purposes only.
if os.Getenv("TELEPORT_UNSTABLE_VC_SYNC_ON_START") == "yes" {
firstReleaseCheck = retryutils.HalfJitter(time.Second * 10)
}
// run periodic functions with a semi-random period
// to avoid contention on the database in case if there are multiple
// auth servers running - so they don't compete trying
// to update the same resources.
period := retryutils.HalfJitter(2 * defaults.HighResPollingPeriod)
ticker := interval.NewMulti(
a.GetClock(),
interval.SubInterval[periodicIntervalKey]{
Key: rotationCheckKey,
Duration: period,
},
interval.SubInterval[periodicIntervalKey]{
Key: metricsKey,
Duration: defaults.PrometheusScrapeInterval,
FirstDuration: 5 * time.Second,
Jitter: retryutils.SeventhJitter,
},
interval.SubInterval[periodicIntervalKey]{
Key: instancePeriodicsKey,
Duration: 9 * time.Minute,
FirstDuration: retryutils.HalfJitter(time.Minute),
Jitter: retryutils.SeventhJitter,
},
interval.SubInterval[periodicIntervalKey]{
Key: notificationsCleanupKey,
Duration: 48 * time.Hour,
FirstDuration: retryutils.FullJitter(time.Hour),
Jitter: retryutils.SeventhJitter,
},
interval.SubInterval[periodicIntervalKey]{
Key: roleCountKey,
Duration: 12 * time.Hour,
FirstDuration: retryutils.FullJitter(time.Minute),
Jitter: retryutils.SeventhJitter,
},
interval.SubInterval[periodicIntervalKey]{
Key: accessListReminderNotificationsKey,
Duration: 8 * time.Hour,
FirstDuration: retryutils.FullJitter(time.Hour),
Jitter: retryutils.SeventhJitter,
},
)
defer ticker.Stop()
// Prevent some periodic operations from running for dashboard tenants.
if !services.IsDashboard(*modules.GetModules().Features().ToProto()) {
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: dynamicLabelsCheckKey,
Duration: dynamicLabelCheckPeriod,
FirstDuration: retryutils.HalfJitter(10 * time.Second),
Jitter: retryutils.SeventhJitter,
})
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: heartbeatCheckKey,
Duration: apidefaults.ServerKeepAliveTTL() * 2,
Jitter: retryutils.SeventhJitter,
})
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: releaseCheckKey,
Duration: 24 * time.Hour,
FirstDuration: firstReleaseCheck,
// note the use of FullJitter for the releases check interval. this lets us ensure
// that frequent restarts don't prevent checks from happening despite the infrequent
// effective check rate.
Jitter: retryutils.FullJitter,
})
// more frequent release check that just re-calculates alerts based on previously
// pulled versioning info.
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: localReleaseCheckKey,
Duration: 10 * time.Minute,
FirstDuration: retryutils.HalfJitter(10 * time.Second),
Jitter: retryutils.HalfJitter,
})
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: autoUpdateAgentReportKey,
Duration: constants.AutoUpdateAgentReportPeriod,
FirstDuration: retryutils.FullJitter(constants.AutoUpdateAgentReportPeriod),
// No jitter here, this is intentional and required for accurate tracking across auths.
})
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: autoUpdateBotInstanceReportKey,
Duration: constants.AutoUpdateAgentReportPeriod,
FirstDuration: retryutils.HalfJitter(10 * time.Second),
Jitter: retryutils.SeventhJitter,
})
}
if modules.GetModules().IsOSSBuild() {
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: desktopCheckKey,
Duration: OSSDesktopsCheckPeriod,
FirstDuration: retryutils.HalfJitter(10 * time.Second),
Jitter: retryutils.HalfJitter,
})
} else if err := a.DeleteClusterAlert(a.closeCtx, OSSDesktopsAlertID); err != nil && !trace.IsNotFound(err) {
a.logger.WarnContext(a.closeCtx, "Can't delete OSS non-AD desktops limit alert", "error", err)
}
// isolate the schedule of potentially long-running refreshRemoteClusters() from other tasks
go func() {
// reasonably small interval to ensure that users observe clusters as online within 1 minute of adding them.
remoteClustersRefresh := interval.New(interval.Config{
Duration: time.Second * 40,
Jitter: retryutils.SeventhJitter,
})
defer remoteClustersRefresh.Stop()
for {
select {
case <-a.closeCtx.Done():
return
case <-remoteClustersRefresh.Next():
a.refreshRemoteClusters(a.closeCtx)
}
}
}()
// cloud auth servers need to periodically sync the upgrade window
// from the cloud db.
if modules.GetModules().Features().Cloud {
ticker.Push(interval.SubInterval[periodicIntervalKey]{
Key: upgradeWindowCheckKey,
Duration: 3 * time.Minute,
FirstDuration: retryutils.FullJitter(30 * time.Second),
Jitter: retryutils.SeventhJitter,
})
}
for {
select {
case <-a.closeCtx.Done():
return
case tick := <-ticker.Next():
switch tick.Key {
case rotationCheckKey:
go func() {
if err := a.AutoRotateCertAuthorities(a.closeCtx); err != nil {
if trace.IsCompareFailed(err) {
a.logger.DebugContext(a.closeCtx, "Cert authority has been updated concurrently", "error", err)
} else {
a.logger.ErrorContext(a.closeCtx, "Failed to perform cert rotation check", "error", err)
}
}
}()
case heartbeatCheckKey:
go func() {
for srv, err := range a.UnifiedResourceCache.Nodes(a.closeCtx, services.UnifiedResourcesIterateParams{}) {
if err != nil {
a.logger.ErrorContext(a.closeCtx, "Failed to load nodes for heartbeat metric calculation", "error", err)
return
}
if services.NodeHasMissedKeepAlives(srv) {
heartbeatsMissedByAuth.Inc()
}
if srv.GetSubKind() != types.SubKindOpenSSHNode {
continue
}
// TODO(tross) DELETE in v20.0.0 - all invalid hostnames should have been sanitized by then.
if !validServerHostname(srv.GetHostname()) {
logger := a.logger.With("server", srv.GetName(), "hostname", srv.GetHostname())
logger.DebugContext(a.closeCtx, "sanitizing invalid static SSH server hostname")
// Any existing static hosts will not have their
// hostname sanitized since they don't heartbeat.
if err := sanitizeHostname(srv); err != nil {
logger.WarnContext(a.closeCtx, "failed to sanitize static SSH server hostname", "error", err)
continue
}
if _, err := a.Services.UpdateNode(a.closeCtx, srv); err != nil && !trace.IsCompareFailed(err) {
logger.WarnContext(a.closeCtx, "failed to update SSH server hostname", "error", err)
}
} else if oldHostname, ok := srv.GetLabel(replacedHostnameLabel); ok && validServerHostname(oldHostname) {
// If the hostname has been replaced by a sanitized version, revert it back to the original
// if the original is valid under the most recent rules.
logger := a.logger.With("server", srv.GetName(), "old_hostname", oldHostname, "sanitized_hostname", srv.GetHostname())
if err := restoreSanitizedHostname(srv); err != nil {
logger.WarnContext(a.closeCtx, "failed to restore sanitized static SSH server hostname", "error", err)
continue
}
if _, err := a.Services.UpdateNode(a.closeCtx, srv); err != nil && !trace.IsCompareFailed(err) {
logger.WarnContext(a.closeCtx, "Failed to update node hostname", "error", err)
}
}
}
}()
case metricsKey:
go a.updateAgentMetrics()
case releaseCheckKey:
go a.syncReleaseAlerts(a.closeCtx, true)
case localReleaseCheckKey:
go a.syncReleaseAlerts(a.closeCtx, false)
case instancePeriodicsKey:
go a.doInstancePeriodics(a.closeCtx)
case desktopCheckKey:
go a.syncDesktopsLimitAlert(a.closeCtx)
case dynamicLabelsCheckKey:
go a.syncDynamicLabelsAlert(a.closeCtx)
case notificationsCleanupKey:
go a.CleanupNotifications(a.closeCtx)
case upgradeWindowCheckKey:
go a.syncUpgradeWindowStartHour(a.closeCtx)
case roleCountKey:
go a.tallyRoles(a.closeCtx)
case accessListReminderNotificationsKey:
go a.CreateAccessListReminderNotifications(a.closeCtx)
case autoUpdateAgentReportKey:
go a.reportAgentVersions(a.closeCtx)
case autoUpdateBotInstanceReportKey:
go a.botVersionReporter.Report(a.closeCtx)
}
}
}
}
func (a *Server) tallyRoles(ctx context.Context) {
count := 0
a.logger.DebugContext(ctx, "tallying roles")
defer func() {
a.logger.DebugContext(ctx, "tallying roles completed", "role_count", count)
}()
req := &proto.ListRolesRequest{Limit: 20}
readLimiter := time.NewTicker(20 * time.Millisecond)
defer readLimiter.Stop()
for {
resp, err := a.Cache.ListRoles(ctx, req)
if err != nil {
return
}
count += len(resp.Roles)
req.StartKey = resp.NextKey
if req.StartKey == "" {
break
}
select {
case <-readLimiter.C:
case <-ctx.Done():
return
}
}
roleCount.Set(float64(count))
}
func (a *Server) doInstancePeriodics(ctx context.Context) {
const slowRate = time.Millisecond * 200 // 5 reads per second
const fastRate = time.Millisecond * 5 // 200 reads per second
const dynamicPeriod = time.Minute * 3
instances := a.GetInstances(ctx, types.InstanceFilter{})
// dynamically scale the rate-limiting we apply to reading instances
// s.t. we read at a progressively faster rate as we observe larger
// connected instance counts. this isn't a perfect metric, but it errs
// on the side of slowness, which is preferable for this kind of periodic.
instanceRate := slowRate
if ci := a.inventory.ConnectedInstances(); ci > 0 {
localDynamicRate := max(dynamicPeriod/time.Duration(ci), fastRate)
if localDynamicRate < instanceRate {
instanceRate = localDynamicRate
}
}
limiter := rate.NewLimiter(rate.Every(instanceRate), 100)
instances = stream.RateLimit(instances, func() error {
return limiter.Wait(ctx)
})
// cloud deployments shouldn't include control-plane elements in
// metrics since information about them is not actionable and may
// produce misleading/confusing results.
skipControlPlane := modules.GetModules().Features().Cloud
// set up aggregators for our periodics
uep := newUpgradeEnrollPeriodic()
// stream all instances to all aggregators
for instances.Next() {
if skipControlPlane {
for _, service := range instances.Item().GetServices() {
if service.IsControlPlane() {
continue
}
}
}
uep.VisitInstance(instances.Item())
}
if err := instances.Done(); err != nil {
a.logger.WarnContext(ctx, "Failed stream instances for periodics", "error", err)
return
}
// create/delete upgrade enroll prompt as appropriate
enrollMsg, shouldPrompt := uep.GenerateEnrollPrompt()
a.handleUpgradeEnrollPrompt(ctx, enrollMsg, shouldPrompt)
}
const (
upgradeEnrollAlertID = "auto-upgrade-enroll"
)
func (a *Server) handleUpgradeEnrollPrompt(ctx context.Context, msg string, shouldPrompt bool) {
const alertTTL = time.Minute * 30
if !shouldPrompt {
if err := a.DeleteClusterAlert(ctx, upgradeEnrollAlertID); err != nil && !trace.IsNotFound(err) {
a.logger.WarnContext(ctx, "Failed to delete auto-upgrade-enroll alert", "error", err)
}
return
}
alert, err := types.NewClusterAlert(
upgradeEnrollAlertID,
msg,
// Defaulting to "low" severity level. We may want to make this dynamic
// in the future depending on the distance from up-to-date.
types.WithAlertSeverity(types.AlertSeverity_LOW),
types.WithAlertLabel(types.AlertVerbPermit, fmt.Sprintf("%s:%s", types.KindInstance, types.VerbRead)),
// hide the normal upgrade alert for users who can see this alert as it is
// generally more actionable/specific.
types.WithAlertLabel(types.AlertSupersedes, releaseAlertID),
types.WithAlertLabel(types.AlertOnLogin, "yes"),
types.WithAlertExpires(a.clock.Now().Add(alertTTL)),
)
if err != nil {
a.logger.WarnContext(ctx, "Failed to build auto-upgrade-enroll alert (this is a bug)", "error", err)
return
}
if err := a.UpsertClusterAlert(ctx, alert); err != nil {
a.logger.WarnContext(ctx, "Failed to set auto-upgrade-enroll alert", "error", err)
return
}
}
const (
releaseAlertID = "upgrade-suggestion"
secAlertID = "security-patch-available"
verInUseLabel = "teleport.internal/ver-in-use"
)
// syncReleaseAlerts calculates alerts related to new teleport releases. When checkRemote
// is true it pulls the latest release info from GitHub. Otherwise, it loads the versions used
// for the most recent alerts and re-syncs with latest cluster state.
func (a *Server) syncReleaseAlerts(ctx context.Context, checkRemote bool) {
a.logger.DebugContext(ctx, "Checking for new teleport releases via github api")
// NOTE: essentially everything in this function is going to be
// scrapped/replaced once the inventory and version-control systems
// are a bit further along.
current := vc.NewTarget(vc.Normalize(teleport.Version))
// this environment variable is "unstable" since it will be deprecated
// by an upcoming tctl command. currently exists for testing purposes only.
if t := vc.NewTarget(os.Getenv("TELEPORT_UNSTABLE_VC_VERSION")); t.Ok() {
current = t
}
visitor := vc.Visitor{
Current: current,
}
// users cannot upgrade their own auth instances in cloud, so it isn't helpful
// to generate alerts for releases newer than the current auth server version.
if modules.GetModules().Features().Cloud {
visitor.NotNewerThan = current
}
var loadFailed bool
if checkRemote {
// scrape the github releases API with our visitor
if err := github.Visit(&visitor); err != nil {
a.logger.WarnContext(ctx, "Failed to load github releases (this will not impact teleport functionality)", "error", err)
loadFailed = true
}
} else {
if err := a.visitCachedAlertVersions(ctx, &visitor); err != nil {
a.logger.WarnContext(ctx, "Failed to load release alert info (this will not impact teleport functionality)", "error", err)
loadFailed = true
}
}
a.doReleaseAlertSync(ctx, current, visitor, !loadFailed)
}
// visitCachedAlertVersions updates the visitor with targets reconstructed from the metadata
// of existing alerts. This lets us "reevaluate" the alerts based on newer cluster state without
// re-pulling the releases page. Future version of teleport will cache actual full release
// descriptions, rending this unnecessary.
func (a *Server) visitCachedAlertVersions(ctx context.Context, visitor *vc.Visitor) error {
// reconstruct the target for the "latest stable" alert if it exists.
alert, err := a.getClusterAlert(ctx, releaseAlertID)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
if err == nil {
if t := vc.NewTarget(alert.Metadata.Labels[verInUseLabel]); t.Ok() {
visitor.Visit(t)
}
}
// reconstruct the target for the "latest sec patch" alert if it exists.
alert, err = a.getClusterAlert(ctx, secAlertID)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
if err == nil {
if t := vc.NewTarget(alert.Metadata.Labels[verInUseLabel], vc.SecurityPatch(true)); t.Ok() {
visitor.Visit(t)
}
}
return nil
}
func (a *Server) getClusterAlert(ctx context.Context, id string) (types.ClusterAlert, error) {
alerts, err := a.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
AlertID: id,
})
if err != nil {
return types.ClusterAlert{}, trace.Wrap(err)
}
if len(alerts) == 0 {
return types.ClusterAlert{}, trace.NotFound("cluster alert %q not found", id)
}
return alerts[0], nil
}
func (a *Server) doReleaseAlertSync(ctx context.Context, current vc.Target, visitor vc.Visitor, cleanup bool) {
const alertTTL = time.Minute * 30
// use visitor to find the oldest version among connected instances.
// TODO(fspmarshall): replace this check as soon as we have a backend inventory repr. using
// connected instances is a poor approximation and may lead to missed notifications if auth
// server is up to date, but instances not connected to this auth need update.
var instanceVisitor vc.Visitor
a.inventory.UniqueHandles(func(handle inventory.UpstreamHandle) {
v := vc.Normalize(handle.Hello().Version)
instanceVisitor.Visit(vc.NewTarget(v))
})
if sp := visitor.NewestSecurityPatch(); sp.Ok() && sp.NewerThan(current) && !sp.SecurityPatchAltOf(current) {
// explicit security patch alerts have a more limited audience, so we generate
// them as their own separate alert.
a.logger.WarnContext(ctx, "A newer security patch has been detected",
"current_version", current.Version(),
"patch_version", sp.Version(),
)
secMsg := fmt.Sprintf("A security patch is available for Teleport. Please upgrade your Cluster to %s or newer.", sp.Version())
alert, err := types.NewClusterAlert(
secAlertID,
secMsg,
types.WithAlertLabel(types.AlertOnLogin, "yes"),
// TODO(fspmarshall): permit alert to be shown to those with inventory management
// permissions once we have RBAC around that. For now, token:write is a decent
// approximation and will ensure that alerts are shown to the editor role.
types.WithAlertLabel(types.AlertVerbPermit, fmt.Sprintf("%s:%s", types.KindToken, types.VerbCreate)),
// hide the normal upgrade alert for users who can see this alert in order to
// improve its visibility and reduce clutter.
types.WithAlertLabel(types.AlertSupersedes, releaseAlertID),
types.WithAlertSeverity(types.AlertSeverity_HIGH),
types.WithAlertLabel(verInUseLabel, sp.Version()),
types.WithAlertExpires(a.clock.Now().Add(alertTTL)),
)
if err != nil {
a.logger.WarnContext(ctx, "Failed to build security-patch-available alert (this is a bug)", "error", err)
return
}
if err := a.UpsertClusterAlert(ctx, alert); err != nil {
a.logger.WarnContext(ctx, "Failed to set security-patch-available alert", "error", err)
return
}
} else if cleanup {
err := a.DeleteClusterAlert(ctx, secAlertID)
if err != nil && !trace.IsNotFound(err) {
a.logger.WarnContext(ctx, "Failed to delete security-patch-available alert", "error", err)
}
}
}
func (a *Server) updateAgentMetrics() {
imp := newInstanceMetricsPeriodic()
a.inventory.UniqueHandles(func(handle inventory.UpstreamHandle) {
imp.VisitInstance(handle.Hello(), handle.AgentMetadata())
})
totalInstancesMetric.Set(float64(imp.TotalInstances()))
enrolledInUpgradesMetric.Set(float64(imp.TotalEnrolledInUpgrades()))
// reset the gauges so that any versions that fall off are removed from exported metrics
registeredAgents.Reset()
for agent, count := range imp.RegisteredAgentsCount() {
registeredAgents.With(prometheus.Labels{
teleport.TagOS: agent.os,
teleport.TagVersion: agent.version,
teleport.TagAutomaticUpdates: agent.automaticUpdates,
}).Set(float64(count))
}
// reset the gauges so that any versions that fall off are removed from exported metrics
registeredAgentsInstallMethod.Reset()
for installMethod, count := range imp.InstallMethodCounts() {
registeredAgentsInstallMethod.WithLabelValues(installMethod).Set(float64(count))
}
// reset the gauges so that any type+version that fall off are removed from exported metrics
upgraderCountsMetric.Reset()
for metadata, count := range imp.UpgraderCounts() {
upgraderCountsMetric.With(prometheus.Labels{
teleport.TagUpgrader: metadata.upgraderType,
teleport.TagVersion: metadata.version,
}).Set(float64(count))
}
}
var (
// remoteClusterRefreshLimit is the maximum number of backend updates that will be performed
// during periodic remote cluster connection status refresh.
remoteClusterRefreshLimit = 50
// remoteClusterRefreshBuckets is the maximum number of refresh cycles that should guarantee the status update
// of all remote clusters if their number exceeds remoteClusterRefreshLimit × remoteClusterRefreshBuckets.
remoteClusterRefreshBuckets = 12
)
// refreshRemoteClusters updates connection status of all remote clusters.
func (a *Server) refreshRemoteClusters(ctx context.Context) {
remoteClusters, err := a.Services.GetRemoteClusters(ctx)
if err != nil {
a.logger.ErrorContext(ctx, "Failed to load remote clusters for status refresh", "error", err)
return
}
netConfig, err := a.GetClusterNetworkingConfig(ctx)
if err != nil {
a.logger.ErrorContext(ctx, "Failed to load networking config for remote cluster status refresh", "error", err)
return
}
// we want to limit the number of backend updates performed on each refresh to avoid overwhelming the backend.
updateLimit := remoteClusterRefreshLimit
if dynamicLimit := (len(remoteClusters) / remoteClusterRefreshBuckets) + 1; dynamicLimit > updateLimit {
// if the number of remote clusters is larger than remoteClusterRefreshLimit × remoteClusterRefreshBuckets,
// bump the limit to make sure all remote clusters will be updated within reasonable time.
updateLimit = dynamicLimit
}
var updateCount int
// randomize the order to optimize for multiple auth servers running in parallel
for _, remoteCluster := range utils.ShuffleVisit(remoteClusters) {
if updated, err := a.updateRemoteClusterStatus(ctx, netConfig, remoteCluster); err != nil {
a.logger.ErrorContext(ctx, "Failed to perform remote cluster status refresh", "error", err)
} else if updated {
updateCount++
}
if updateCount >= updateLimit {
break
}
}
}
func (a *Server) Close() error {
a.cancelFunc()
var errs []error
if err := a.inventory.Close(); err != nil {
errs = append(errs, err)
}
if a.Services.AuditLogSessionStreamer != nil {
if err := a.Services.AuditLogSessionStreamer.Close(); err != nil {
errs = append(errs, err)
}
}
if a.bk != nil {
if err := a.bk.Close(); err != nil {
errs = append(errs, err)
}
}
if a.ScopedAccessCache != nil {
if err := a.ScopedAccessCache.Close(); err != nil {
errs = append(errs, err)
}
}
if a.AccessRequestCache != nil {
if err := a.AccessRequestCache.Close(); err != nil {
errs = append(errs, err)
}
}
if a.UserNotificationCache != nil {
if err := a.UserNotificationCache.Close(); err != nil {
errs = append(errs, err)
}
}
if a.GlobalNotificationCache != nil {
if err := a.GlobalNotificationCache.Close(); err != nil {
errs = append(errs, err)
}
}
return trace.NewAggregate(errs...)
}
func (a *Server) GetClock() clockwork.Clock {
a.lock.RLock()
defer a.lock.RUnlock()
return a.clock
}
// SetClock sets clock, used in tests
func (a *Server) SetClock(clock clockwork.Clock) {
a.lock.Lock()
defer a.lock.Unlock()
a.clock = clock
}
// SetBcryptCost sets bcryptCostOverride, used in tests
func (a *Server) SetBcryptCost(cost int) {
a.lock.Lock()
defer a.lock.Unlock()
a.bcryptCostOverride = &cost
}
func (a *Server) SetSCIMService(scim services.SCIM) {
a.Services.SCIM = scim
}
// SetAccessGraphSecretService sets the server's access graph secret service
func (a *Server) SetAccessGraphSecretService(s services.AccessGraphSecretsGetter) {
a.Services.AccessGraphSecretsGetter = s
}
// SetDevicesGetter sets the server's device service
func (a *Server) SetDevicesGetter(s services.DevicesGetter) {
a.Services.DevicesGetter = s
}
// SetAuditLog sets the server's audit log
func (a *Server) SetAuditLog(auditLog events.AuditLogSessionStreamer) {
a.Services.AuditLogSessionStreamer = auditLog
}
// GetEmitter fetches the current audit log emitter implementation.
func (a *Server) GetEmitter() apievents.Emitter {
return a.emitter
}
// SetEmitter sets the current audit log emitter. Note that this is only safe to
// use before main server start.
func (a *Server) SetEmitter(emitter apievents.Emitter) {
a.emitter = emitter
}
// EmitAuditEvent implements [apievents.Emitter] by delegating to its dedicated
// emitter rather than falling back to the implementation from [Services] (using
// the audit log directly, which is almost never what you want).
func (a *Server) EmitAuditEvent(ctx context.Context, e apievents.AuditEvent) error {
return trace.Wrap(a.emitter.EmitAuditEvent(context.WithoutCancel(ctx), e))
}
// SetUsageReporter sets the server's usage reporter. Note that this is only
// safe to use before server start.
func (a *Server) SetUsageReporter(reporter usagereporter.UsageReporter) {
a.Services.UsageReporter = reporter
}
// GetClusterID returns the cluster ID.
func (a *Server) GetClusterID(ctx context.Context) (string, error) {
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return "", trace.Wrap(err)
}
return clusterName.GetClusterID(), nil
}
// GetAnonymizationKey returns the anonymization key that identifies this client.
// The anonymization key may be any of the following, in order of precedence:
// - (Teleport Cloud) a key provided by the Teleport Cloud API
// - a key embedded in the license file
// - the cluster's UUID
func (a *Server) GetAnonymizationKey(ctx context.Context) (string, error) {
if key := modules.GetModules().Features().CloudAnonymizationKey; len(key) > 0 {
return string(key), nil
}
if a.license != nil && len(a.license.AnonymizationKey) > 0 {
return string(a.license.AnonymizationKey), nil
}
id, err := a.GetClusterID(ctx)
return id, trace.Wrap(err)
}
// GetDomainName returns the domain name that identifies this authority server.
// Also known as "cluster name"
func (a *Server) GetDomainName() (string, error) {
clusterName, err := a.GetClusterName(context.TODO())
if err != nil {
return "", trace.Wrap(err)
}
return clusterName.GetClusterName(), nil
}
// GetClusterCACert returns the PEM-encoded TLS certs for the local cluster. If
// the cluster has multiple TLS certs, they will all be concatenated.
func (a *Server) GetClusterCACert(ctx context.Context) (*proto.GetClusterCACertResponse, error) {
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// Extract the TLS CA for this cluster.
hostCA, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: clusterName.GetClusterName(),
}, false)
if err != nil {
return nil, trace.Wrap(err)
}
certs := services.GetTLSCerts(hostCA)
if len(certs) < 1 {
return nil, trace.NotFound("no tls certs found in host CA")
}
allCerts := bytes.Join(certs, []byte("\n"))
return &proto.GetClusterCACertResponse{
TLSCA: allCerts,
}, nil
}
// GenerateHostCert uses the private key of the CA to sign the public key of the host
// (along with meta data like host ID, node name, roles, and ttl) to generate a host certificate.
func (a *Server) GenerateHostCert(ctx context.Context, hostPublicKey []byte, hostID, nodeName string, principals []string, clusterName string, role types.SystemRole, ttl time.Duration) ([]byte, error) {
domainName, err := a.GetDomainName()
if err != nil {
return nil, trace.Wrap(err)
}
// get the certificate authority that will be signing the public key of the host
ca, err := a.Services.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: domainName,
}, true)
if err != nil {
return nil, trace.BadParameter("failed to load host CA for %q: %v", domainName, err)
}
caSigner, err := a.keyStore.GetSSHSigner(ctx, ca)
if err != nil {
return nil, trace.Wrap(err)
}
// create and sign!
return a.generateHostCert(ctx, sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: hostPublicKey,
HostID: hostID,
NodeName: nodeName,
TTL: ttl,
Identity: sshca.Identity{
Principals: principals,
ClusterName: clusterName,
SystemRole: role,
},
})
}
func (a *Server) generateHostCert(
ctx context.Context, req sshca.HostCertificateRequest,
) ([]byte, error) {
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
var locks []types.LockTarget
switch req.Identity.SystemRole {
case types.RoleNode:
// Node role is a special case because it was previously suported as a
// lock target that only locked the `ssh_service`. If the same Teleport server
// had multiple roles, Node lock would only lock the `ssh_service` while
// other roles would be able to generate certificates without a problem.
// To remove the ambiguity, we now lock the entire Teleport server for
// all roles using the LockTarget.ServerID field and `Node` field is
// deprecated.
// In order to support legacy behavior, we need fill in both `ServerID`
// and `Node` fields if the role is `Node` so that the previous behavior
// is preserved.
// This is a legacy behavior that we need to support for backwards compatibility.
locks = []types.LockTarget{{ServerID: req.HostID}, {ServerID: utils.HostFQDN(req.HostID, req.Identity.ClusterName)}}
default:
locks = []types.LockTarget{{ServerID: req.HostID}, {ServerID: utils.HostFQDN(req.HostID, req.Identity.ClusterName)}}
}
if lockErr := a.checkLockInForce(readOnlyAuthPref.GetLockingMode(),
locks,
); lockErr != nil {
return nil, trace.Wrap(lockErr)
}
return a.Authority.GenerateHostCert(req)
}
// GetKeyStore returns the KeyStore used by the auth server
func (a *Server) GetKeyStore() *keystore.Manager {
return a.keyStore
}
type certRequest struct {
// sshPublicKey is a public key in SSH authorized_keys format. If set it
// will be used as the subject public key for the returned SSH certificate.
sshPublicKey []byte
// tlsPublicKey is a PEM-encoded public key in PKCS#1 or PKIX ASN.1 DER
// form. If set it will be used as the subject public key for the returned
// TLS certificate.
tlsPublicKey []byte
// sshPublicKeyAttestationStatement is an attestation statement associated with sshPublicKey.
sshPublicKeyAttestationStatement *hardwarekey.AttestationStatement
// tlsPublicKeyAttestationStatement is an attestation statement associated with tlsPublicKey.
tlsPublicKeyAttestationStatement *hardwarekey.AttestationStatement
// user is a user to generate certificate for
user services.UserState
// impersonator is a user who generates the certificate,
// is set when different from the user in the certificate
impersonator string
// checker is an access checker that may either be scoped or unscoped. used to generate various
// certificate parameters, some of which differ depending on whether the cert being generated
// is scoped or not.
checker *services.SplitAccessChecker
// ttl is Duration of the certificate
ttl time.Duration
// compatibility is compatibility mode
compatibility string
// overrideRoleTTL is used for requests when the requested TTL should not be
// adjusted based off the role of the user. This is used by tctl to allow
// creating long lived user certs.
overrideRoleTTL bool
// usage is a list of acceptable usages to be encoded in X509 certificate,
// is used to limit ways the certificate can be used, for example
// the cert can be only used against kubernetes endpoint, and not auth endpoint,
// no usage means unrestricted (to keep backwards compatibility)
usage []string
// routeToCluster is an optional teleport cluster name to route the
// certificate requests to, this teleport cluster name will be used to
// route the requests to in case of kubernetes
routeToCluster string
// kubernetesCluster specifies the target kubernetes cluster for TLS
// identities. This can be empty on older Teleport clients.
kubernetesCluster string
// traits hold claim data used to populate a role at runtime.
traits wrappers.Traits
// activeRequests tracks privilege escalation requests applied
// during the construction of the certificate.
activeRequests []string
// appSessionID is the session ID of the application session.
appSessionID string
// appPublicAddr is the public address of the application.
appPublicAddr string
// appClusterName is the name of the cluster this application is in.
appClusterName string
// appName is the name of the application to generate cert for.
appName string
// appURI is the URI of the app. This is the internal endpoint where the application is running and isn't user-facing.
appURI string
// appTargetPort signifies that the cert should grant access to a specific port in a multi-port
// TCP app, as long as the port is defined in the app spec. Used only for routing, should not be
// used in other contexts (e.g., access requests).
appTargetPort int
// awsRoleARN is the role ARN to generate certificate for.
awsRoleARN string
// azureIdentity is the Azure identity to generate certificate for.
azureIdentity string
// gcpServiceAccount is the GCP service account to generate certificate for.
gcpServiceAccount string
// dbService identifies the name of the database service requests will
// be routed to.
dbService string
// dbProtocol specifies the protocol of the database a certificate will
// be issued for.
dbProtocol string
// dbUser is the optional database user which, if provided, will be used
// as a default username.
dbUser string
// dbName is the optional database name which, if provided, will be used
// as a default database.
dbName string
// dbRoles is the optional list of database roles which, if provided, will
// be used instead of all database roles granted for the target database.
dbRoles []string
// mfaVerified is the UUID of an MFA device when this certRequest was
// created immediately after an MFA check.
mfaVerified string
// previousIdentityExpires is the expiry time of the identity/cert that this
// identity/cert was derived from. It is used to determine a session's hard
// deadline in cases where both require_session_mfa and disconnect_expired_cert
// are enabled. See https://github.com/gravitational/teleport/issues/18544.
previousIdentityExpires time.Time
// loginIP is an IP of the client requesting the certificate.
loginIP string
// pinIP flags that client's login IP should be pinned in the certificate
pinIP bool
// disallowReissue flags that a cert should not be allowed to issue future
// certificates.
disallowReissue bool
// renewable indicates that the certificate can be renewed,
// having its TTL increased
renewable bool
// includeHostCA indicates that host CA certs should be included in the
// returned certs
includeHostCA bool
// generation indicates the number of times this certificate has been
// renewed.
generation uint64
// connectionDiagnosticID contains the ID of the ConnectionDiagnostic.
// The Node/Agent will append connection traces to this instance.
connectionDiagnosticID string
// deviceExtensions holds device-aware user certificate extensions.
deviceExtensions DeviceExtensions
// botName is the name of the bot requesting this cert, if any
botName string
// botInstanceID is the unique identifier of the bot instance associated
// with this cert, if any
botInstanceID string
// joinToken is the name of the join token used to join, set only for bot
// identities. It is unset for token-joined bots, whose token names are
// secret values.
joinToken string
// joinAttributes holds attributes derived from attested metadata from the
// join process, should any exist.
joinAttributes *workloadidentityv1pb.JoinAttrs
}
// check verifies the cert request is valid.
func (r *certRequest) check() error {
if r.user == nil {
return trace.BadParameter("missing parameter user")
}
if r.checker == nil {
return trace.BadParameter("missing parameter checker")
}
// When generating certificate for MongoDB access, database username must
// be encoded into it. This is required to be able to tell which database
// user to authenticate the connection as.
if r.dbProtocol == defaults.ProtocolMongoDB {
if r.dbUser == "" {
return trace.BadParameter("must provide database user name to generate certificate for database %q", r.dbService)
}
}
if r.sshPublicKey == nil && r.tlsPublicKey == nil {
return trace.BadParameter("must provide a public key")
}
return nil
}
type certRequestOption func(*certRequest)
func certRequestPreviousIdentityExpires(previousIdentityExpires time.Time) certRequestOption {
return func(r *certRequest) { r.previousIdentityExpires = previousIdentityExpires }
}
func certRequestLoginIP(ip string) certRequestOption {
return func(r *certRequest) { r.loginIP = ip }
}
func certRequestDeviceExtensions(ext tlsca.DeviceExtensions) certRequestOption {
return func(r *certRequest) {
r.deviceExtensions = DeviceExtensions(ext)
}
}
// GetUserOrLoginState will return the given user or the login state associated with the user.
func (a *Server) GetUserOrLoginState(ctx context.Context, username string) (services.UserState, error) {
return services.GetUserOrLoginState(ctx, a, username)
}
func (a *Server) GenerateOpenSSHCert(ctx context.Context, req *proto.OpenSSHCertRequest) (*proto.OpenSSHCert, error) {
if req.User == nil {
return nil, trace.BadParameter("user is empty")
}
if len(req.PublicKey) == 0 {
return nil, trace.BadParameter("public key is empty")
}
if req.TTL == 0 {
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.BadParameter("cert request does not specify a TTL and the cluster_auth_preference is not available: %v", err)
}
req.TTL = proto.Duration(readOnlyAuthPref.GetDefaultSessionTTL())
}
if req.TTL < 0 {
return nil, trace.BadParameter("TTL must be positive")
}
if req.Cluster == "" {
return nil, trace.BadParameter("cluster is empty")
}
// add implicit roles to the set and build a checker
accessInfo := services.AccessInfoFromUserState(req.User)
roles := make([]types.Role, len(req.Roles))
for i := range req.Roles {
var err error
roles[i], err = services.ApplyTraits(req.Roles[i], req.User.GetTraits())
if err != nil {
return nil, trace.Wrap(err)
}
}
roleSet := services.NewRoleSet(roles...)
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
checker := services.NewAccessCheckerWithRoleSet(accessInfo, clusterName.GetClusterName(), roleSet)
sessionTTL := time.Duration(req.TTL)
// OpenSSH certs and their corresponding keys are held strictly by the proxy,
// so we can attest them as "web_session" to bypass Hardware Key support
// requirements that are unattainable from the Proxy.
sshPublicKey, _, _, _, err := ssh.ParseAuthorizedKey(req.PublicKey)
if err != nil {
return nil, trace.Wrap(err)
}
cryptoPublicKey, ok := sshPublicKey.(ssh.CryptoPublicKey)
if !ok {
return nil, trace.BadParameter("unsupported SSH public key type %q", sshPublicKey.Type())
}
webAttData, err := services.NewWebSessionAttestationData(cryptoPublicKey.CryptoPublicKey())
if err != nil {
return nil, trace.Wrap(err)
}
if err = a.UpsertKeyAttestationData(ctx, webAttData, sessionTTL); err != nil {
return nil, trace.Wrap(err)
}
certs, err := a.generateOpenSSHCert(ctx, certRequest{
user: req.User,
sshPublicKey: req.PublicKey,
compatibility: constants.CertificateFormatStandard,
checker: services.NewUnscopedSplitAccessChecker(checker), // TODO(fspmarshall/scopes): add scoping support to OpenSSH certs.
ttl: sessionTTL,
traits: req.User.GetTraits(),
routeToCluster: req.Cluster,
disallowReissue: true,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.OpenSSHCert{
Cert: certs.SSH,
}, nil
}
// GenerateUserTestCertsRequest is a request to generate test certificates.
// TODO(tross): Figure out how to move this into test only code.
type GenerateUserTestCertsRequest struct {
SSHPubKey []byte
TLSPubKey []byte
Username string
TTL time.Duration
Compatibility string
RouteToCluster string
PinnedIP string
MFAVerified string
SSHAttestationStatement *hardwarekey.AttestationStatement
TLSAttestationStatement *hardwarekey.AttestationStatement
AppName string
AppSessionID string
DeviceExtensions DeviceExtensions
Renewable bool
Generation uint64
ActiveRequests []string
KubernetesCluster string
Usage []string
}
// GenerateUserTestCerts is used to generate user certificate, used internally for tests
// TODO(tross): Figure out how to move this into test only code.
func (a *Server) GenerateUserTestCerts(req GenerateUserTestCertsRequest) ([]byte, []byte, error) {
return a.GenerateUserTestCertsWithContext(context.TODO(), req)
}
// GenerateUserTestCertsWithContext is used to generate user certificate, used internally for tests
// TODO(tross): Figure out how to move this into test only code.
func (a *Server) GenerateUserTestCertsWithContext(ctx context.Context, req GenerateUserTestCertsRequest) ([]byte, []byte, error) {
userState, err := a.GetUserOrLoginState(ctx, req.Username)
if err != nil {
return nil, nil, trace.Wrap(err)
}
accessInfo := services.AccessInfoFromUserState(userState)
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, nil, trace.Wrap(err)
}
checker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), a)
if err != nil {
return nil, nil, trace.Wrap(err)
}
certReq := certRequest{
user: userState,
ttl: req.TTL,
compatibility: req.Compatibility,
sshPublicKey: req.SSHPubKey,
tlsPublicKey: req.TLSPubKey,
routeToCluster: req.RouteToCluster,
checker: services.NewUnscopedSplitAccessChecker(checker),
traits: userState.GetTraits(),
loginIP: req.PinnedIP,
pinIP: req.PinnedIP != "",
mfaVerified: req.MFAVerified,
sshPublicKeyAttestationStatement: req.SSHAttestationStatement,
tlsPublicKeyAttestationStatement: req.TLSAttestationStatement,
appName: req.AppName,
appSessionID: req.AppSessionID,
deviceExtensions: req.DeviceExtensions,
generation: req.Generation,
renewable: req.Renewable,
activeRequests: req.ActiveRequests,
kubernetesCluster: req.KubernetesCluster,
usage: req.Usage,
}
if botName, isBot := userState.GetLabel(types.BotLabel); isBot {
certReq.botName = botName
certReq.botInstanceID = uuid.NewString()
}
certs, err := a.generateUserCert(ctx, certReq)
if err != nil {
return nil, nil, trace.Wrap(err)
}
return certs.SSH, certs.TLS, nil
}
// AppTestCertRequest combines parameters for generating a test app access cert.
type AppTestCertRequest struct {
// PublicKey is the public key to sign, in PEM-encoded PKCS#1 or PKIX DER format.
PublicKey []byte
// Username is the Teleport user name to sign certificate for.
Username string
// TTL is the test certificate validity period.
TTL time.Duration
// PublicAddr is the application public address. Used for routing.
PublicAddr string
// TargetPort is the port to which connections to multi-port TCP apps should be routed to.
TargetPort int
// ClusterName is the name of the cluster application resides in. Used for routing.
ClusterName string
// SessionID is the optional session ID to encode. Used for routing.
SessionID string
// AWSRoleARN is optional AWS role ARN a user wants to assume to encode.
AWSRoleARN string
// AzureIdentity is the optional Azure identity a user wants to assume to encode.
AzureIdentity string
// GCPServiceAccount is optional GCP service account a user wants to assume to encode.
GCPServiceAccount string
// PinnedIP is optional IP to pin certificate to.
PinnedIP string
// LoginTrait is the login to include in the cert
LoginTrait string
}
// GenerateUserAppTestCert generates an application specific certificate, used
// internally for tests.
func (a *Server) GenerateUserAppTestCert(req AppTestCertRequest) ([]byte, error) {
ctx := context.Background()
userState, err := a.GetUserOrLoginState(ctx, req.Username)
if err != nil {
return nil, trace.Wrap(err)
}
accessInfo := services.AccessInfoFromUserState(userState)
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
checker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), a)
if err != nil {
return nil, trace.Wrap(err)
}
sessionID := req.SessionID
if sessionID == "" {
sessionID = uuid.New().String()
}
login := req.LoginTrait
if login == "" {
login = uuid.New().String()
}
certs, err := a.generateUserCert(ctx, certRequest{
user: userState,
tlsPublicKey: req.PublicKey,
checker: services.NewUnscopedSplitAccessChecker(checker),
ttl: req.TTL,
// Set the login to be a random string. Application certificates are never
// used to log into servers but SSH certificate generation code requires a
// principal be in the certificate.
traits: wrappers.Traits(map[string][]string{
constants.TraitLogins: {login},
}),
// Only allow this certificate to be used for applications.
usage: []string{teleport.UsageAppsOnly},
// Add in the application routing information.
appSessionID: sessionID,
appPublicAddr: req.PublicAddr,
appTargetPort: req.TargetPort,
appClusterName: req.ClusterName,
awsRoleARN: req.AWSRoleARN,
azureIdentity: req.AzureIdentity,
gcpServiceAccount: req.GCPServiceAccount,
pinIP: req.PinnedIP != "",
loginIP: req.PinnedIP,
})
if err != nil {
return nil, trace.Wrap(err)
}
return certs.TLS, nil
}
// DatabaseTestCertRequest combines parameters for generating test database
// access certificate.
type DatabaseTestCertRequest struct {
// PublicKey is the public key to sign, in PEM-encoded PKCS#1 or PKIX format.
PublicKey []byte
// Cluster is the Teleport cluster name.
Cluster string
// Username is the Teleport username.
Username string
// RouteToDatabase contains database routing information.
RouteToDatabase tlsca.RouteToDatabase
// PinnedIP is an IP new certificate should be pinned to.
PinnedIP string
}
// GenerateDatabaseTestCert generates a database access certificate for the
// provided parameters. Used only internally in tests.
func (a *Server) GenerateDatabaseTestCert(req DatabaseTestCertRequest) ([]byte, error) {
ctx := context.Background()
userState, err := a.GetUserOrLoginState(ctx, req.Username)
if err != nil {
return nil, trace.Wrap(err)
}
accessInfo := services.AccessInfoFromUserState(userState)
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
checker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), a)
if err != nil {
return nil, trace.Wrap(err)
}
certs, err := a.generateUserCert(ctx, certRequest{
user: userState,
tlsPublicKey: req.PublicKey,
loginIP: req.PinnedIP,
pinIP: req.PinnedIP != "",
checker: services.NewUnscopedSplitAccessChecker(checker),
ttl: time.Hour,
traits: map[string][]string{
constants.TraitLogins: {req.Username},
},
routeToCluster: req.Cluster,
dbService: req.RouteToDatabase.ServiceName,
dbProtocol: req.RouteToDatabase.Protocol,
dbUser: req.RouteToDatabase.Username,
dbName: req.RouteToDatabase.Database,
dbRoles: req.RouteToDatabase.Roles,
})
if err != nil {
return nil, trace.Wrap(err)
}
return certs.TLS, nil
}
// DeviceExtensions hold device-aware user certificate extensions.
// Device extensions are a part of Device Trust, a feature exclusive to Teleport
// Enterprise.
type DeviceExtensions tlsca.DeviceExtensions
// AugmentUserCertificateOpts aggregates options for extending user
// certificates.
// See [AugmentContextUserCertificates].
type AugmentUserCertificateOpts struct {
// SSHAuthorizedKey is an SSH certificate, in the authorized key format, to
// augment with opts.
// The SSH certificate must be issued for the current authenticated user,
// and either:
// - the public key must match their TLS certificate, or
// - SSHKeySatisfiedChallenge must be true.
SSHAuthorizedKey []byte
// SSHKeySatisfiedChallenge will be true if the user has already
// proven that they own the private key associated with SSHAuthorizedKey by
// satisfying a signature challenge.
SSHKeySatisfiedChallenge bool
// DeviceExtensions are the device-aware extensions to add to the certificates
// being augmented.
DeviceExtensions *DeviceExtensions
}
// AugmentContextUserCertificates augments the context user certificates with
// the given extensions. It requires the user's TLS certificate to be present
// in the [ctx], in addition to the [authCtx] itself.
//
// Any additional certificates to augment, such as the SSH certificate, must be
// valid and fully match the certificate used to authenticate (likely the user's
// mTLS cert).
//
// Used by Device Trust to add device extensions to the user certificate.
func (a *Server) AugmentContextUserCertificates(
ctx context.Context,
authCtx *authz.Context,
opts *AugmentUserCertificateOpts,
) (*proto.Certs, error) {
switch {
case authCtx == nil:
return nil, trace.BadParameter("authCtx required")
case opts == nil:
return nil, trace.BadParameter("opts required")
}
// Fetch user TLS certificate.
x509Cert, err := authz.UserCertificateFromContext(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
identity := authCtx.Identity.GetIdentity()
return a.augmentUserCertificates(ctx, augmentUserCertificatesOpts{
checker: authCtx.Checker,
x509Cert: x509Cert,
x509Identity: &identity,
sshAuthorizedKey: opts.SSHAuthorizedKey,
sshKeyVerified: opts.SSHKeySatisfiedChallenge,
deviceExtensions: opts.DeviceExtensions,
})
}
// AugmentWebSessionCertificatesOpts aggregates arguments for
// [AugmentWebSessionCertificates].
type AugmentWebSessionCertificatesOpts struct {
// WebSessionID is the identifier for the WebSession.
WebSessionID string
// User is the owner of the WebSession.
User string
// DeviceExtensions are the device-aware extensions to add to the certificates
// being augmented.
DeviceExtensions *DeviceExtensions
}
// AugmentWebSessionCertificates is a variant of
// [AugmentContextUserCertificates] that operates directly in the certificates
// stored in a WebSession.
//
// On success the WebSession is updated with device extension certificates.
func (a *Server) AugmentWebSessionCertificates(ctx context.Context, opts *AugmentWebSessionCertificatesOpts) error {
switch {
case opts == nil:
return trace.BadParameter("opts required")
case opts.WebSessionID == "":
return trace.BadParameter("opts.WebSessionID required")
case opts.User == "":
return trace.BadParameter("opts.User required")
}
// Get and validate session.
sessions := a.WebSessions()
session, err := sessions.Get(ctx, types.GetWebSessionRequest{
User: opts.User,
SessionID: opts.WebSessionID,
})
if err != nil {
return trace.Wrap(err)
}
// Coerce session before doing more expensive operations.
sessionV2, ok := session.(*types.WebSessionV2)
if !ok {
return trace.BadParameter("unexpected WebSession type: %T", session)
}
// Parse X.509 certificate.
block, _ := pem.Decode(session.GetTLSCert())
if block == nil {
return trace.BadParameter("cannot decode session TLS certificate")
}
x509Cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return trace.Wrap(err)
}
x509Identity, err := tlsca.FromSubject(x509Cert.Subject, x509Cert.NotAfter)
if err != nil {
return trace.Wrap(err)
}
// Prepare the AccessChecker for the WebSession identity.
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return trace.Wrap(err)
}
accessInfo, err := services.AccessInfoFromLocalTLSIdentity(*x509Identity)
if err != nil {
return trace.Wrap(err)
}
checker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), a)
if err != nil {
return trace.Wrap(err)
}
// We consider this SSH key to be verified because we take it directly from
// the web session. The user doesn't need to verify they own it because the
// don't: we own it.
const sshKeyVerified = true
// Augment certificates.
newCerts, err := a.augmentUserCertificates(ctx, augmentUserCertificatesOpts{
checker: checker,
x509Cert: x509Cert,
x509Identity: x509Identity,
sshAuthorizedKey: session.GetPub(),
sshKeyVerified: sshKeyVerified,
deviceExtensions: opts.DeviceExtensions,
})
if err != nil {
return trace.Wrap(err)
}
// Update WebSession.
sessionV2.Spec.Pub = newCerts.SSH
sessionV2.Spec.TLSCert = newCerts.TLS
sessionV2.Spec.HasDeviceExtensions = true
return trace.Wrap(sessions.Upsert(ctx, sessionV2))
}
type augmentUserCertificatesOpts struct {
checker services.AccessChecker
x509Cert *x509.Certificate
x509Identity *tlsca.Identity
sshAuthorizedKey []byte
// sshKeyVerified means that either the user has proven that they control
// the private key associated with sshAuthorizedKey (by signing a
// challenge), or it comes from a web session where we know that the cluster
// controls the key.
sshKeyVerified bool
deviceExtensions *DeviceExtensions
}
func (a *Server) augmentUserCertificates(
ctx context.Context,
opts augmentUserCertificatesOpts,
) (*proto.Certs, error) {
// Is at least one extension present?
// Are the extensions valid?
dev := opts.deviceExtensions
switch {
case dev == nil: // Only extension that currently exists.
return nil, trace.BadParameter("at least one opts extension must be present")
case dev.DeviceID == "":
return nil, trace.BadParameter("opts.DeviceExtensions.DeviceID required")
case dev.AssetTag == "":
return nil, trace.BadParameter("opts.DeviceExtensions.AssetTag required")
case dev.CredentialID == "":
return nil, trace.BadParameter("opts.DeviceExtensions.CredentialID required")
}
x509Cert := opts.x509Cert
x509Identity := opts.x509Identity
// Sanity check: x509Cert identity matches x509Identity.
if x509Cert.Subject.CommonName != x509Identity.Username {
return nil, trace.BadParameter("identity and x509 user mismatch")
}
// Do not reissue if device extensions are already present.
// Note that the certIdentity extensions could differ from the "current"
// identity extensions if this was not the cert used to authenticate.
if x509Identity.DeviceExtensions.DeviceID != "" ||
x509Identity.DeviceExtensions.AssetTag != "" ||
x509Identity.DeviceExtensions.CredentialID != "" {
return nil, trace.BadParameter("device extensions already present")
}
// Parse and verify SSH certificate.
sshAuthorizedKey := opts.sshAuthorizedKey
var sshCert *ssh.Certificate
if len(sshAuthorizedKey) > 0 {
var err error
sshCert, err = apisshutils.ParseCertificate(sshAuthorizedKey)
if err != nil {
return nil, trace.Wrap(err)
}
xPubKey, err := ssh.NewPublicKey(x509Cert.PublicKey)
if err != nil {
return nil, trace.Wrap(err)
}
// filter and sort TLS and SSH principals for comparison.
// Order does not matter and "-teleport-*" principals are filtered out.
filterAndSortPrincipals := func(s []string) []string {
res := make([]string, 0, len(s))
for _, principal := range s {
// Ignore -teleport- internal principals.
if strings.HasPrefix(principal, "-teleport-") {
continue
}
res = append(res, principal)
}
sort.Strings(res)
return res
}
// Verify SSH certificate against identity.
// The SSH certificate isn't used to establish the connection that
// eventually reaches this method, so we check it more thoroughly.
// In the end it still has to be signed by the Teleport CA and share the
// TLS public key, but we verify most fields to be safe.
switch {
case sshCert.CertType != ssh.UserCert:
return nil, trace.BadParameter("ssh cert type mismatch")
case sshCert.KeyId != x509Identity.Username:
return nil, trace.BadParameter("identity and SSH user mismatch")
case !slices.Equal(filterAndSortPrincipals(sshCert.ValidPrincipals), filterAndSortPrincipals(x509Identity.Principals)):
return nil, trace.BadParameter("identity and SSH principals mismatch")
case !opts.sshKeyVerified && !apisshutils.KeysEqual(sshCert.Key, xPubKey):
return nil, trace.BadParameter("x509 and SSH public key mismatch and SSH challenge unsatisfied")
// Do not reissue if device extensions are already present.
case sshCert.Extensions[teleport.CertExtensionDeviceID] != "",
sshCert.Extensions[teleport.CertExtensionDeviceAssetTag] != "",
sshCert.Extensions[teleport.CertExtensionDeviceCredentialID] != "":
return nil, trace.BadParameter("device extensions already present")
}
}
// Fetch TLS CA and SSH signer.
domainName, err := a.GetDomainName()
if err != nil {
return nil, trace.Wrap(err)
}
tlsCA, sshSigner, _, err := a.getSigningCAs(ctx, domainName, types.UserCA)
if err != nil {
return nil, trace.Wrap(err)
}
// Verify TLS certificate against CA.
now := a.clock.Now()
roots := x509.NewCertPool()
roots.AddCert(tlsCA.Cert)
if _, err := x509Cert.Verify(x509.VerifyOptions{
Roots: roots,
CurrentTime: now,
KeyUsages: []x509.ExtKeyUsage{
// Extensions added by tlsca.
// See https://github.com/gravitational/teleport/blob/master/lib/tlsca/ca.go#L963.
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
}); err != nil {
return nil, trace.Wrap(err)
}
// Verify SSH certificate against CA.
if sshCert != nil {
// ValidPrincipals are checked against identity above.
// Pick the first one from the cert here.
var principal string
if len(sshCert.ValidPrincipals) > 0 {
principal = sshCert.ValidPrincipals[0]
}
certChecker := &ssh.CertChecker{
Clock: a.clock.Now,
}
if err := certChecker.CheckCert(principal, sshCert); err != nil {
return nil, trace.Wrap(err)
}
// CheckCert verifies the signature but not the CA.
// Do that here.
if !apisshutils.KeysEqual(sshCert.SignatureKey, sshSigner.PublicKey()) {
return nil, trace.BadParameter("ssh certificate signed by unknown authority")
}
}
// Verify locks right before we re-issue any certificates.
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{
checker: services.NewUnscopedSplitAccessChecker(opts.checker), // TODO(fspmarshall/scopes): add scoping support to AugmentUserCertificates.
defaultMode: readOnlyAuthPref.GetLockingMode(),
username: x509Identity.Username,
mfaVerified: x509Identity.MFAVerified,
activeAccessRequests: x509Identity.ActiveRequests,
deviceID: dev.DeviceID, // Check lock against requested device.
}); err != nil {
return nil, trace.Wrap(err)
}
// Augment TLS certificate.
newIdentity := x509Identity
newIdentity.DeviceExtensions.DeviceID = dev.DeviceID
newIdentity.DeviceExtensions.AssetTag = dev.AssetTag
newIdentity.DeviceExtensions.CredentialID = dev.CredentialID
subj, err := newIdentity.Subject()
if err != nil {
return nil, trace.Wrap(err)
}
notAfter := x509Cert.NotAfter
newTLSCert, err := tlsCA.GenerateCertificate(tlsca.CertificateRequest{
Clock: a.clock,
PublicKey: x509Cert.PublicKey,
Subject: subj,
// Use the same expiration as the original cert.
NotAfter: notAfter,
})
if err != nil {
return nil, trace.Wrap(err)
}
// Augment SSH certificate.
var newAuthorizedKey []byte
if sshCert != nil {
// Add some leeway to validAfter to avoid time skew errors.
validAfter := a.clock.Now().UTC().Add(-1 * time.Minute)
newSSHCert := &ssh.Certificate{
Key: sshCert.Key,
CertType: ssh.UserCert,
KeyId: sshCert.KeyId,
ValidPrincipals: sshCert.ValidPrincipals,
ValidAfter: uint64(validAfter.Unix()),
// Use the same expiration as the x509 cert.
ValidBefore: uint64(notAfter.Unix()),
Permissions: sshCert.Permissions,
}
newSSHCert.Extensions[teleport.CertExtensionDeviceID] = dev.DeviceID
newSSHCert.Extensions[teleport.CertExtensionDeviceAssetTag] = dev.AssetTag
newSSHCert.Extensions[teleport.CertExtensionDeviceCredentialID] = dev.CredentialID
if err := newSSHCert.SignCert(rand.Reader, sshSigner); err != nil {
return nil, trace.Wrap(err)
}
newAuthorizedKey = ssh.MarshalAuthorizedKey(newSSHCert)
}
// Issue audit event on success, same as [Server.generateCert].
a.emitCertCreateEvent(ctx, tlsCA, newIdentity, notAfter)
return &proto.Certs{
SSH: newAuthorizedKey,
TLS: newTLSCert,
}, nil
}
// submitCertificateIssuedEvent submits a certificate issued usage event to the
// usage reporting service.
func (a *Server) submitCertificateIssuedEvent(req *certRequest, attestedKeyPolicy keys.PrivateKeyPolicy) {
var database, app, kubernetes, desktop bool
if req.dbService != "" {
database = true
}
if req.appName != "" {
app = true
}
if req.kubernetesCluster != "" {
kubernetes = true
}
// Bot users are regular Teleport users, but have a special internal label.
bot := req.user.IsBot()
// Unfortunately the only clue we have about Windows certs is the usage
// restriction: `RouteToWindowsDesktop` isn't actually passed along to the
// certRequest.
for _, usage := range req.usage {
switch usage {
case teleport.UsageWindowsDesktopOnly:
desktop = true
}
}
// For usage reporting, we care about the impersonator rather than the user
// being impersonated (if any).
user := req.user.GetName()
if req.impersonator != "" {
user = req.impersonator
}
a.AnonymizeAndSubmit(&usagereporter.UserCertificateIssuedEvent{
UserName: user,
Ttl: durationpb.New(req.ttl),
IsBot: bot,
UsageDatabase: database,
UsageApp: app,
UsageKubernetes: kubernetes,
UsageDesktop: desktop,
PrivateKeyPolicy: string(attestedKeyPolicy),
BotInstanceId: req.botInstanceID,
})
}
// generateUserCert generates certificates signed with User CA
func (a *Server) generateUserCert(ctx context.Context, req certRequest) (*proto.Certs, error) {
return generateCert(ctx, a, req, types.UserCA)
}
// generateOpenSSHCert generates certificates signed with OpenSSH CA
func (a *Server) generateOpenSSHCert(ctx context.Context, req certRequest) (*proto.Certs, error) {
return generateCert(ctx, a, req, types.OpenSSHCA)
}
func generateCert(ctx context.Context, a *Server, req certRequest, caType types.CertAuthType) (*proto.Certs, error) {
err := req.check()
if err != nil {
return nil, trace.Wrap(err)
}
if _, ok := req.checker.Scoped(); ok {
// require that the scope feature is enabled for scoped certificate creation
if err := scopes.AssertFeatureEnabled(); err != nil {
return nil, trace.Wrap(err)
}
}
if unscopedChecker, ok := req.checker.Unscoped(); ok {
if len(unscopedChecker.GetAllowedResourceIDs()) > 0 && modules.GetModules().BuildType() != modules.BuildEnterprise {
return nil, trace.Errorf("resource access requests: %w", ErrRequiresEnterprise)
}
}
// Reject the cert request if there is a matching lock in force.
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{
checker: req.checker,
defaultMode: readOnlyAuthPref.GetLockingMode(),
username: req.user.GetName(),
mfaVerified: req.mfaVerified,
activeAccessRequests: req.activeRequests,
deviceID: req.deviceExtensions.DeviceID,
botInstanceID: req.botInstanceID,
joinToken: req.joinToken,
}); err != nil {
return nil, trace.Wrap(err)
}
// extract the passed in certificate format. if nothing was passed in, fetch
// the certificate format from the role.
certificateFormat, err := utils.CheckCertificateFormatFlag(req.compatibility)
if err != nil {
return nil, trace.Wrap(err)
}
// scoped identities must use the standard certificate format, unscoped identities may have their
// certificate format customized by request parameters and/or role settings.
if unscopedChecker, ok := req.checker.Unscoped(); ok {
if certificateFormat == teleport.CertificateFormatUnspecified {
certificateFormat = unscopedChecker.CertificateFormat()
}
} else {
switch certificateFormat {
case constants.CertificateFormatStandard:
case teleport.CertificateFormatUnspecified:
certificateFormat = constants.CertificateFormatStandard
default:
return nil, trace.BadParameter("certificate format %q is not supported for scoped access", certificateFormat)
}
}
var sessionTTL time.Duration
var allowedLogins []string
if req.ttl == 0 {
req.ttl = time.Duration(readOnlyAuthPref.GetDefaultSessionTTL())
}
// If the role TTL is ignored, do not restrict session TTL and allowed logins.
// The only caller setting this parameter should be "tctl auth sign".
// Otherwise, set the session TTL to the smallest of all roles and
// then only grant access to allowed logins based on that.
if req.overrideRoleTTL {
// Take whatever was passed in. Pass in 0 to CheckLoginDuration so all
// logins are returned for the role set.
sessionTTL = req.ttl
allowedLogins, err = req.checker.Common().CheckLoginDuration(0)
if err != nil {
return nil, trace.Wrap(err)
}
} else {
// Adjust session TTL to the smaller of two values: the session TTL requested
// in tsh (possibly using default_session_ttl) or the session TTL for the
// role.
sessionTTL = req.checker.Common().AdjustSessionTTL(req.ttl)
// Return a list of logins that meet the session TTL limit. This means if
// the requested session TTL is larger than the max session TTL for a login,
// that login will not be included in the list of allowed logins.
allowedLogins, err = req.checker.Common().CheckLoginDuration(sessionTTL)
if err != nil {
return nil, trace.Wrap(err)
}
}
notAfter := a.clock.Now().UTC().Add(sessionTTL)
attestedKeyPolicy := keys.PrivateKeyPolicyNone
requiredKeyPolicy, err := req.checker.Common().PrivateKeyPolicy(readOnlyAuthPref.GetPrivateKeyPolicy())
if err != nil {
return nil, trace.Wrap(err)
}
if requiredKeyPolicy != keys.PrivateKeyPolicyNone {
var (
sshAttestedKeyPolicy keys.PrivateKeyPolicy
tlsAttestedKeyPolicy keys.PrivateKeyPolicy
)
if req.sshPublicKey != nil {
sshCryptoPubKey, err := sshutils.CryptoPublicKey(req.sshPublicKey)
if err != nil {
return nil, trace.Wrap(err)
}
sshAttestedKeyPolicy, err = a.attestHardwareKey(ctx, &attestHardwareKeyParams{
requiredKeyPolicy: requiredKeyPolicy,
pubKey: sshCryptoPubKey,
attestationStatement: req.sshPublicKeyAttestationStatement,
sessionTTL: sessionTTL,
readOnlyAuthPref: readOnlyAuthPref,
userName: req.user.GetName(),
userTraits: req.checker.Common().Traits(),
})
if err != nil {
return nil, trace.Wrap(err, "attesting SSH key")
}
}
if req.tlsPublicKey != nil {
tlsCryptoPubKey, err := keys.ParsePublicKey(req.tlsPublicKey)
if err != nil {
return nil, trace.Wrap(err)
}
tlsAttestedKeyPolicy, err = a.attestHardwareKey(ctx, &attestHardwareKeyParams{
requiredKeyPolicy: requiredKeyPolicy,
pubKey: tlsCryptoPubKey,
attestationStatement: req.tlsPublicKeyAttestationStatement,
sessionTTL: sessionTTL,
readOnlyAuthPref: readOnlyAuthPref,
userName: req.user.GetName(),
userTraits: req.checker.Common().Traits(),
})
if err != nil {
return nil, trace.Wrap(err, "attesting TLS key")
}
}
if req.sshPublicKey != nil && req.tlsPublicKey != nil && sshAttestedKeyPolicy != tlsAttestedKeyPolicy {
return nil, trace.BadParameter("SSH attested key policy %q does not match TLS attested key policy %q, this not supported",
sshAttestedKeyPolicy, tlsAttestedKeyPolicy)
}
attestedKeyPolicy = cmp.Or(sshAttestedKeyPolicy, tlsAttestedKeyPolicy)
}
clusterName, err := a.GetDomainName()
if err != nil {
return nil, trace.Wrap(err)
}
if req.routeToCluster == "" {
req.routeToCluster = clusterName
}
if req.routeToCluster != clusterName {
unscopedChecker, ok := req.checker.Unscoped()
if !ok {
return nil, trace.BadParameter("cannot generate certs for remote cluster %q, remote cluster access is only supported for unscoped certs", req.routeToCluster)
}
// Authorize access to a remote cluster.
rc, err := a.GetRemoteCluster(ctx, req.routeToCluster)
if err != nil {
return nil, trace.Wrap(err)
}
if err := unscopedChecker.CheckAccessToRemoteCluster(rc); err != nil {
if trace.IsAccessDenied(err) {
return nil, trace.NotFound("remote cluster %q not found", req.routeToCluster)
}
return nil, trace.Wrap(err)
}
}
// Add the special join-only principal used for joining sessions.
// All users have access to this and join RBAC rules are checked after the connection is established.
allowedLogins = append(allowedLogins, teleport.SSHSessionJoinPrincipal)
pinnedIP := ""
if caType == types.UserCA && (req.checker.Common().PinSourceIP() || req.pinIP) {
if req.loginIP == "" {
return nil, trace.BadParameter("IP pinning is enabled for user %q but there is no client IP information", req.user.GetName())
}
pinnedIP = req.loginIP
}
ca, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: caType,
DomainName: clusterName,
}, true)
if err != nil {
return nil, trace.Wrap(err)
}
// At most one GitHub identity expected.
var githubUserID, githubUsername string
if githubIdentities := req.user.GetGithubIdentities(); len(githubIdentities) > 0 {
githubUserID = githubIdentities[0].UserID
githubUsername = githubIdentities[0].Username
}
// collect fields which are mutually exclusive to scoped or unscoped certs
var (
scopePin *scopesv1.Pin
roleNames []string
allowedResourceIDs []types.ResourceID
)
if scopedChecker, ok := req.checker.Scoped(); ok {
scopePin = scopedChecker.ScopePin()
}
if unscopedChecker, ok := req.checker.Unscoped(); ok {
roleNames = unscopedChecker.RoleNames()
allowedResourceIDs = unscopedChecker.GetAllowedResourceIDs()
}
var signedSSHCert []byte
if req.sshPublicKey != nil {
sshSigner, err := a.keyStore.GetSSHSigner(ctx, ca)
if err != nil {
return nil, trace.Wrap(err)
}
// certificate extensions are only supported for unscoped ssh certs
var certificateExtensions []*types.CertExtension
if unscopedChecker, ok := req.checker.Unscoped(); ok {
certificateExtensions = unscopedChecker.CertificateExtensions()
}
params := sshca.UserCertificateRequest{
CASigner: sshSigner,
PublicUserKey: req.sshPublicKey,
TTL: sessionTTL,
CertificateFormat: certificateFormat,
Identity: sshca.Identity{
Username: req.user.GetName(),
Impersonator: req.impersonator,
Principals: allowedLogins,
ScopePin: scopePin,
Roles: roleNames,
PermitPortForwarding: req.checker.Common().CanPortForward(),
PermitAgentForwarding: req.checker.Common().CanForwardAgents(),
PermitX11Forwarding: req.checker.Common().PermitX11Forwarding(),
RouteToCluster: req.routeToCluster,
Traits: req.traits,
ActiveRequests: req.activeRequests,
MFAVerified: req.mfaVerified,
PreviousIdentityExpires: req.previousIdentityExpires,
LoginIP: req.loginIP,
PinnedIP: pinnedIP,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
BotName: req.botName,
BotInstanceID: req.botInstanceID,
JoinToken: req.joinToken,
CertificateExtensions: certificateExtensions,
AllowedResourceIDs: allowedResourceIDs,
ConnectionDiagnosticID: req.connectionDiagnosticID,
PrivateKeyPolicy: attestedKeyPolicy,
DeviceID: req.deviceExtensions.DeviceID,
DeviceAssetTag: req.deviceExtensions.AssetTag,
DeviceCredentialID: req.deviceExtensions.CredentialID,
GitHubUserID: githubUserID,
GitHubUsername: githubUsername,
},
}
signedSSHCert, err = a.GenerateUserCert(params)
if err != nil {
return nil, trace.Wrap(err)
}
}
// Ensure that the Kubernetes cluster name specified in the request exists
// when the certificate is intended for a local Kubernetes cluster.
// If the certificate is targeting a trusted Teleport cluster, it is the
// responsibility of the cluster to ensure its existence.
if req.routeToCluster == clusterName && req.kubernetesCluster != "" {
var found bool
for ks, err := range a.UnifiedResourceCache.KubernetesServers(a.closeCtx, services.UnifiedResourcesIterateParams{}) {
if err != nil {
return nil, trace.Wrap(err)
}
if ks.GetCluster().GetName() == req.kubernetesCluster {
found = true
break
}
}
if !found {
return nil, trace.BadParameter("Kubernetes cluster %q is not registered in this Teleport cluster; you can list registered Kubernetes clusters using 'tsh kube ls'", req.kubernetesCluster)
}
}
var (
kubeGroups, kubeUsers []string
dbNames, dbUsers []string
roleARNs []string
azureIdentities []string
gcpAccounts []string
)
// only unscoped identities currently support kube groups/users.
if unscopedChecker, ok := req.checker.Unscoped(); ok {
kubeGroups, kubeUsers, err = unscopedChecker.CheckKubeGroupsAndUsers(sessionTTL, req.overrideRoleTTL)
// NotFound errors are acceptable - this user may have no k8s access
// granted and that shouldn't prevent us from issuing a TLS cert.
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
// See which database names and users this user is allowed to use.
dbNames, dbUsers, err = unscopedChecker.CheckDatabaseNamesAndUsers(sessionTTL, req.overrideRoleTTL)
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
// See which AWS role ARNs this user is allowed to assume.
roleARNs, err = unscopedChecker.CheckAWSRoleARNs(sessionTTL, req.overrideRoleTTL)
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
// See which Azure identities this user is allowed to assume.
azureIdentities, err = unscopedChecker.CheckAzureIdentities(sessionTTL, req.overrideRoleTTL)
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
// Enumerate allowed GCP service accounts.
gcpAccounts, err = unscopedChecker.CheckGCPServiceAccounts(sessionTTL, req.overrideRoleTTL)
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
}
// Generate AWS client side credentials if the user is trying to access an AWS App with an AWS Roles Anywhere Integration.
awsCredentialProcessCredentials, err := generateAWSClientSideCredentials(ctx, a, req, notAfter)
switch {
case errors.Is(err, errAppWithoutAWSClientSideCredentials):
case err != nil:
return nil, trace.Wrap(err)
}
identity := tlsca.Identity{
Username: req.user.GetName(),
Impersonator: req.impersonator,
ScopePin: scopePin,
Groups: roleNames,
Principals: allowedLogins,
Usage: req.usage,
RouteToCluster: req.routeToCluster,
KubernetesCluster: req.kubernetesCluster,
Traits: req.traits,
KubernetesGroups: kubeGroups,
KubernetesUsers: kubeUsers,
RouteToApp: tlsca.RouteToApp{
SessionID: req.appSessionID,
URI: req.appURI,
TargetPort: req.appTargetPort,
PublicAddr: req.appPublicAddr,
ClusterName: req.appClusterName,
Name: req.appName,
AWSRoleARN: req.awsRoleARN,
AWSCredentialProcessCredentials: awsCredentialProcessCredentials,
AzureIdentity: req.azureIdentity,
GCPServiceAccount: req.gcpServiceAccount,
},
TeleportCluster: clusterName,
OriginClusterName: clusterName,
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: req.dbService,
Protocol: req.dbProtocol,
Username: req.dbUser,
Database: req.dbName,
Roles: req.dbRoles,
},
DatabaseNames: dbNames,
DatabaseUsers: dbUsers,
MFAVerified: req.mfaVerified,
PreviousIdentityExpires: req.previousIdentityExpires,
LoginIP: req.loginIP,
PinnedIP: pinnedIP,
AWSRoleARNs: roleARNs,
AzureIdentities: azureIdentities,
GCPServiceAccounts: gcpAccounts,
ActiveRequests: req.activeRequests,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
BotName: req.botName,
BotInstanceID: req.botInstanceID,
JoinToken: req.joinToken,
AllowedResourceIDs: allowedResourceIDs,
PrivateKeyPolicy: attestedKeyPolicy,
ConnectionDiagnosticID: req.connectionDiagnosticID,
DeviceExtensions: tlsca.DeviceExtensions{
DeviceID: req.deviceExtensions.DeviceID,
AssetTag: req.deviceExtensions.AssetTag,
CredentialID: req.deviceExtensions.CredentialID,
},
UserType: req.user.GetUserType(),
JoinAttributes: req.joinAttributes,
}
var signedTLSCert []byte
var tlsIssuer *tlsca.CertAuthority
if req.tlsPublicKey != nil {
tlsCryptoPubKey, err := keys.ParsePublicKey(req.tlsPublicKey)
if err != nil {
return nil, trace.Wrap(err)
}
tlsCert, tlsSigner, err := a.keyStore.GetTLSCertAndSigner(ctx, ca)
if err != nil {
return nil, trace.Wrap(err)
}
tlsCA, err := tlsca.FromCertAndSigner(tlsCert, tlsSigner)
if err != nil {
return nil, trace.Wrap(err)
}
subject, err := identity.Subject()
if err != nil {
return nil, trace.Wrap(err)
}
certRequest := tlsca.CertificateRequest{
Clock: a.clock,
PublicKey: tlsCryptoPubKey,
Subject: subject,
NotAfter: notAfter,
}
signedTLSCert, err = tlsCA.GenerateCertificate(certRequest)
if err != nil {
return nil, trace.Wrap(err)
}
tlsIssuer = tlsCA
}
a.emitCertCreateEvent(ctx, tlsIssuer, &identity, notAfter)
// create certs struct to return to user
certs := &proto.Certs{
SSH: signedSSHCert,
TLS: signedTLSCert,
}
// always include specified CA
cas := []types.CertAuthority{ca}
// also include host CA certs if requested
if req.includeHostCA {
hostCA, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: clusterName,
}, false)
if err != nil {
return nil, trace.Wrap(err)
}
cas = append(cas, hostCA)
}
for _, ca := range cas {
certs.TLSCACerts = append(certs.TLSCACerts, services.GetTLSCerts(ca)...)
certs.SSHCACerts = append(certs.SSHCACerts, services.GetSSHCheckingKeys(ca)...)
}
a.submitCertificateIssuedEvent(&req, attestedKeyPolicy)
userCertificatesGeneratedMetric.WithLabelValues(string(attestedKeyPolicy)).Inc()
return certs, nil
}
type attestHardwareKeyParams struct {
requiredKeyPolicy keys.PrivateKeyPolicy
pubKey crypto.PublicKey
attestationStatement *hardwarekey.AttestationStatement
sessionTTL time.Duration
readOnlyAuthPref readonly.AuthPreference
userName string
userTraits map[string][]string
}
func (a *Server) attestHardwareKey(ctx context.Context, params *attestHardwareKeyParams) (attestedKeyPolicy keys.PrivateKeyPolicy, err error) {
// Try to attest the given hardware key using the given attestation statement.
attestationData, err := modules.GetModules().AttestHardwareKey(ctx, a, params.attestationStatement, params.pubKey, params.sessionTTL)
if trace.IsNotFound(err) {
return attestedKeyPolicy, keys.NewPrivateKeyPolicyError(params.requiredKeyPolicy)
} else if err != nil {
return attestedKeyPolicy, trace.Wrap(err)
}
// verify that the required private key policy for the requested identity
// is met by the provided attestation statement.
attestedKeyPolicy = attestationData.PrivateKeyPolicy
if !params.requiredKeyPolicy.IsSatisfiedBy(attestedKeyPolicy) {
return attestedKeyPolicy, keys.NewPrivateKeyPolicyError(params.requiredKeyPolicy)
}
var validateSerialNumber bool
hksnv, err := params.readOnlyAuthPref.GetHardwareKeySerialNumberValidation()
if err == nil {
validateSerialNumber = hksnv.Enabled
}
// Validate the serial number if enabled, unless this is a web session.
if validateSerialNumber && attestedKeyPolicy != keys.PrivateKeyPolicyWebSession {
const defaultSerialNumberTraitName = "hardware_key_serial_numbers"
// Note: currently only yubikeys are supported as hardware keys. If we extend
// support to more hardware keys, we can add prefixes to serial numbers.
// Ex: solokey_12345678 or s_12345678.
// When prefixes are added, we can default to assuming that serial numbers
// without prefixes are for yubikeys, meaning there will be no backwards
// compatibility issues.
serialNumberTraitName := hksnv.SerialNumberTraitName
if serialNumberTraitName == "" {
serialNumberTraitName = defaultSerialNumberTraitName
}
// Check that the attested hardware key serial number matches
// a serial number in the user's traits, if any are set.
registeredSerialNumbers, ok := params.userTraits[serialNumberTraitName]
if !ok || len(registeredSerialNumbers) == 0 {
a.logger.DebugContext(ctx, "A user tried to sign in with hardware key support, but has no known hardware keys set in their traits",
"user", params.userName,
"expected_trait", "user.traits."+serialNumberTraitName)
return attestedKeyPolicy, trace.BadParameter("cannot generate certs for user with no known hardware keys")
}
attestedSerialNumber := strconv.Itoa(int(attestationData.SerialNumber))
// serial number traits can be a comma separated list, or a list of comma separated lists.
// e.g. [["12345678,87654321"], ["13572468"]].
if !slices.ContainsFunc(registeredSerialNumbers, func(s string) bool {
return slices.Contains(strings.Split(s, ","), attestedSerialNumber)
}) {
a.logger.DebugContext(ctx, "A user tried to sign in with hardware key support with an unknown hardware key and was denied",
"user", params.userName,
"yubikey_serial_number", attestedSerialNumber,
)
return attestedKeyPolicy, trace.BadParameter("cannot generate certs for user with unknown hardware key: YubiKey serial number %q", attestedSerialNumber)
}
}
return attestedKeyPolicy, nil
}
type verifyLocksForUserCertsReq struct {
// checker is a split access checker which may be scoped or unscoped.
checker *services.SplitAccessChecker
// defaultMode is the default locking mode, as recorded in the cluster
// Auth Preferences.
defaultMode constants.LockingMode
// username is the Teleport username.
// Eg: tlsca.Identity.Username.
username string
// mfaVerified is the UUID of the MFA device used to authenticate the user.
// Eg: tlsca.Identity.MFAVerified.
mfaVerified string
// activeAccessRequests are the UUIDs of active access requests for the user.
// Eg: tlsca.Identity.ActiveRequests.
activeAccessRequests []string
// deviceID is the trusted device ID.
// Eg: tlsca.Identity.DeviceExtensions.DeviceID
deviceID string
// botInstanceID is the bot instance UUID, set only for bots.
botInstanceID string
// joinMethod is the join token name, set only for non-token bots.
joinToken string
}
// verifyLocksForUserCerts verifies if any locks are in place before issuing new
// user certificates.
func (a *Server) verifyLocksForUserCerts(req verifyLocksForUserCertsReq) error {
lockingMode := req.checker.Common().LockingMode(req.defaultMode)
lockTargets := []types.LockTarget{
{User: req.username},
{MFADevice: req.mfaVerified},
{Device: req.deviceID},
}
if unscopedChecker, ok := req.checker.Unscoped(); ok {
lockTargets = append(lockTargets,
services.RolesToLockTargets(unscopedChecker.RoleNames())...,
)
}
// TODO(fspmarshall/scopes): implement scoped role locking.
lockTargets = append(lockTargets,
services.AccessRequestsToLockTargets(req.activeAccessRequests)...,
)
if req.botInstanceID != "" {
lockTargets = append(lockTargets, types.LockTarget{BotInstanceID: req.botInstanceID})
}
if req.joinToken != "" {
lockTargets = append(lockTargets, types.LockTarget{JoinToken: req.joinToken})
}
return trace.Wrap(a.checkLockInForce(lockingMode, lockTargets))
}
// getSigningCAs returns the necessary resources to issue/sign new certificates.
func (a *Server) getSigningCAs(ctx context.Context, domainName string, caType types.CertAuthType) (*tlsca.CertAuthority, ssh.Signer, types.CertAuthority, error) {
const loadKeys = true
ca, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: caType,
DomainName: domainName,
}, loadKeys)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
tlsCert, tlsSigner, err := a.keyStore.GetTLSCertAndSigner(ctx, ca)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
tlsCA, err := tlsca.FromCertAndSigner(tlsCert, tlsSigner)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
sshSigner, err := a.keyStore.GetSSHSigner(ctx, ca)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
return tlsCA, sshSigner, ca, nil
}
func (a *Server) emitCertCreateEvent(ctx context.Context, issuer *tlsca.CertAuthority, identity *tlsca.Identity, notAfter time.Time) {
eventIdentity := identity.GetEventIdentity()
eventIdentity.Expires = notAfter
var certAuthority *apievents.CertificateAuthority
if issuer != nil {
certAuthority = &apievents.CertificateAuthority{
Type: string(types.UserCA),
Domain: issuer.Cert.Issuer.CommonName,
SubjectKeyID: base32.HexEncoding.EncodeToString(issuer.Cert.SubjectKeyId),
}
}
if err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.CertificateCreate{
Metadata: apievents.Metadata{
Type: events.CertificateCreateEvent,
Code: events.CertificateCreateCode,
},
CertificateType: events.CertificateTypeUser,
Identity: &eventIdentity,
ClientMetadata: apievents.ClientMetadata{
// TODO(greedy52) currently only user-agent from GRPC clients are
// fetched. Need to propagate user-agent from HTTP calls.
UserAgent: trimUserAgent(metadata.UserAgentFromContext(ctx)),
},
CertificateAuthority: certAuthority,
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit certificate create event", "error", err)
}
}
// WithUserLock executes function authenticateFn that performs user authentication
// if authenticateFn returns non nil error, the login attempt will be logged in as failed.
// The only exception to this rule is ConnectionProblemError, in case if it occurs
// access will be denied, but login attempt will not be recorded
// this is done to avoid potential user lockouts due to backend failures
// In case if user exceeds defaults.MaxLoginAttempts
// the user account will be locked for defaults.AccountLockInterval
func (a *Server) WithUserLock(ctx context.Context, username string, authenticateFn func() error) error {
user, err := a.Services.GetUser(ctx, username, false)
if err != nil {
if trace.IsNotFound(err) {
// If user is not found, still call authenticateFn. It should
// always return an error. This prevents username oracles and
// timing attacks.
return authenticateFn()
}
return trace.Wrap(err)
}
status := user.GetStatus()
if status.IsLocked {
if status.LockExpires.After(a.clock.Now().UTC()) {
a.logger.DebugContext(ctx, "Locking user that exceeded the failed login attempt limit",
"user", user.GetName(),
"failed_attempt_limit", defaults.MaxLoginAttempts,
"locked_until", apiutils.HumanTimeFormat(status.LockExpires),
)
err := trace.AccessDenied("%s", MaxFailedAttemptsErrMsg)
return trace.WithField(err, ErrFieldKeyUserMaxedAttempts, true)
}
}
fnErr := authenticateFn()
if fnErr == nil {
// upon successful login, reset the failed attempt counter
err = a.DeleteUserLoginAttempts(username)
if !trace.IsNotFound(err) {
return trace.Wrap(err)
}
return nil
}
// do not lock user in case if DB is flaky or down
if trace.IsConnectionProblem(err) {
return trace.Wrap(fnErr)
}
// log failed attempt and possibly lock user
attempt := services.LoginAttempt{Time: a.clock.Now().UTC(), Success: false}
err = a.AddUserLoginAttempt(username, attempt, defaults.AttemptTTL)
if err != nil {
a.logger.ErrorContext(ctx, "unable to persist failed login attempt", "error", err)
return trace.Wrap(fnErr)
}
loginAttempts, err := a.GetUserLoginAttempts(username)
if err != nil {
a.logger.ErrorContext(ctx, "unable to retrieve user login attempts", "error", err)
return trace.Wrap(fnErr)
}
if !services.LastFailed(defaults.MaxLoginAttempts, loginAttempts) {
a.logger.DebugContext(ctx, "user has less than the failed login attempt limit",
"user", username,
"failed_attempt_limit", defaults.MaxLoginAttempts,
)
return trace.Wrap(fnErr)
}
lockUntil := a.clock.Now().UTC().Add(defaults.AccountLockInterval)
a.logger.DebugContext(ctx, "Locking user that exceeded the failed login attempt limit",
"user", username,
"failed_attempt_limit", defaults.MaxLoginAttempts,
"locked_until", apiutils.HumanTimeFormat(lockUntil),
)
user.SetLocked(lockUntil, "user has exceeded maximum failed login attempts")
_, err = a.UpsertUser(ctx, user)
if err != nil {
a.logger.ErrorContext(ctx, "failed to persist user record", "error", err)
return trace.Wrap(fnErr)
}
retErr := trace.AccessDenied("%s", MaxFailedAttemptsErrMsg)
return trace.WithField(retErr, ErrFieldKeyUserMaxedAttempts, true)
}
// CreateAuthPreference creates a new auth preference if one does not exist. This
// is an internal API and is not exposed via [clusterconfigv1.ClusterConfigServiceServer] or
// [proto.AuthServiceServer]. It is only meant to be called directly from within auth
// initialization to seed the [types.AuthPreference] for brand new clusters.
func (a *Server) CreateAuthPreference(ctx context.Context, p types.AuthPreference) (types.AuthPreference, error) {
if err := services.ValidateAuthPreference(p); err != nil {
return nil, trace.Wrap(err)
}
// check that the given RequireMFAType is supported in this build.
if p.GetPrivateKeyPolicy().IsHardwareKeyPolicy() && modules.GetModules().BuildType() != modules.BuildEnterprise {
return nil, trace.AccessDenied("Hardware Key support is only available with an enterprise license")
}
if err := dtconfig.ValidateConfigAgainstModules(p.GetDeviceTrust()); err != nil {
return nil, trace.Wrap(err)
}
if err := p.CheckSignatureAlgorithmSuite(types.SignatureAlgorithmSuiteParams{
FIPS: a.fips,
UsingHSMOrKMS: a.keyStore.UsingHSMOrKMS(),
Cloud: modules.GetModules().Features().Cloud,
}); err != nil {
return nil, trace.Wrap(err)
}
created, err := a.Services.CreateAuthPreference(ctx, p)
if err != nil {
return nil, trace.Wrap(err)
}
authPrefV2, ok := created.(*types.AuthPreferenceV2)
if !ok {
return nil, trace.Wrap(trace.BadParameter("unexpected auth preference type %T (expected %T)", created, authPrefV2))
}
return authPrefV2, nil
}
// CreateAuthenticateChallenge implements AuthService.CreateAuthenticateChallenge.
func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) {
var username string
challengeExtensions := &mfav1.ChallengeExtensions{}
if req.ChallengeExtensions != nil {
challengeExtensions = req.ChallengeExtensions
}
validateAndSetScope := func(challengeExtensions *mfav1.ChallengeExtensions, expectedScope mfav1.ChallengeScope) error {
if challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_UNSPECIFIED {
challengeExtensions.Scope = expectedScope
} else if challengeExtensions.Scope != expectedScope {
// scope doesn't need to be specified when the challenge request type is
// tied to a specific scope, but we validate it anyways as a sanity check.
return trace.BadParameter("invalid scope %q, expected %q", challengeExtensions.Scope, expectedScope)
}
return nil
}
switch req.GetRequest().(type) {
case *proto.CreateAuthenticateChallengeRequest_UserCredentials:
username = req.GetUserCredentials().GetUsername()
if err := a.WithUserLock(ctx, username, func() error {
return a.checkPasswordWOToken(ctx, username, req.GetUserCredentials().GetPassword())
}); err != nil {
// This is only ever used as a means to acquire a login challenge, so
// let's issue an authentication failure event.
if err := a.emitAuthAuditEvent(ctx, authAuditProps{
username: username,
authErr: err,
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit login event", "error", err)
// err swallowed on purpose.
}
return nil, trace.Wrap(err)
}
if err := validateAndSetScope(challengeExtensions, mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN); err != nil {
return nil, trace.Wrap(ErrDone)
}
case *proto.CreateAuthenticateChallengeRequest_RecoveryStartTokenID:
token, err := a.GetUserToken(ctx, req.GetRecoveryStartTokenID())
if err != nil {
a.logger.ErrorContext(ctx, "failed to get user token", "error", err)
return nil, trace.AccessDenied("invalid token")
}
if err := a.verifyUserToken(ctx, token, authclient.UserTokenTypeRecoveryStart); err != nil {
return nil, trace.Wrap(err)
}
username = token.GetUser()
if err := validateAndSetScope(challengeExtensions, mfav1.ChallengeScope_CHALLENGE_SCOPE_ACCOUNT_RECOVERY); err != nil {
return nil, trace.Wrap(ErrDone)
}
case *proto.CreateAuthenticateChallengeRequest_Passwordless:
if err := validateAndSetScope(challengeExtensions, mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN); err != nil {
return nil, trace.Wrap(ErrDone)
}
default: // unset or CreateAuthenticateChallengeRequest_ContextUser.
// Require that a scope was provided.
if challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_UNSPECIFIED {
return nil, trace.BadParameter("scope not present in request")
}
var err error
username, err = authz.GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}
challenges, err := a.mfaAuthChallenge(ctx, username, req.SSOClientRedirectURL, req.ProxyAddress, challengeExtensions)
if err != nil {
// Do not obfuscate config-related errors.
if errors.Is(err, types.ErrPasswordlessRequiresWebauthn) || errors.Is(err, types.ErrPasswordlessDisabledBySettings) {
return nil, trace.Wrap(err)
}
a.logger.ErrorContext(ctx, "failed to create MFA challenge", "error", err)
return nil, trace.AccessDenied("unable to create MFA challenges")
}
return challenges, nil
}
// CreateRegisterChallenge implements AuthService.CreateRegisterChallenge.
func (a *Server) CreateRegisterChallenge(ctx context.Context, req *proto.CreateRegisterChallengeRequest) (*proto.MFARegisterChallenge, error) {
var token types.UserToken
var username string
switch {
case req.TokenID != "": // Web UI or account recovery flows.
var err error
token, err = a.GetUserToken(ctx, req.GetTokenID())
if err != nil {
a.logger.ErrorContext(ctx, "failed to retrieve user token", "error", err)
return nil, trace.AccessDenied("invalid token")
}
allowedTokenTypes := []string{
authclient.UserTokenTypePrivilege,
authclient.UserTokenTypePrivilegeException,
authclient.UserTokenTypeResetPassword,
authclient.UserTokenTypeResetPasswordInvite,
authclient.UserTokenTypeRecoveryApproved,
}
if err := a.verifyUserToken(ctx, token, allowedTokenTypes...); err != nil {
return nil, trace.AccessDenied("invalid token")
}
username = token.GetUser()
default: // Authenticated user without token, tsh.
var err error
username, err = authz.GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES}
if _, err := a.validateMFAAuthResponseForRegister(ctx, req.ExistingMFAResponse, username, requiredExt); err != nil {
return nil, trace.Wrap(err)
}
// Create a special token for OTP registrations. The token doubles as
// temporary storage for the OTP secret, like in the branch above.
// This is OK because the user just did an MFA check.
if req.GetDeviceType() != proto.DeviceType_DEVICE_TYPE_TOTP {
break // break from switch
}
token, err = a.createTOTPPrivilegeToken(ctx, username)
if err != nil {
return nil, trace.Wrap(err)
}
}
regChal, err := a.createRegisterChallenge(ctx, &newRegisterChallengeRequest{
username: username,
token: token,
deviceType: req.GetDeviceType(),
deviceUsage: req.GetDeviceUsage(),
})
return regChal, trace.Wrap(err)
}
func (a *Server) createTOTPPrivilegeToken(ctx context.Context, username string) (types.UserToken, error) {
tokenReq := authclient.CreateUserTokenRequest{
Name: username,
Type: userTokenTypePrivilegeOTP,
}
if err := tokenReq.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
token, err := a.newUserToken(tokenReq)
if err != nil {
return nil, trace.Wrap(err)
}
token, err = a.CreateUserToken(ctx, token)
return token, trace.Wrap(err)
}
type newRegisterChallengeRequest struct {
username string
deviceType proto.DeviceType
deviceUsage proto.DeviceUsage
// token is a user token resource.
// It is used as following:
// - TOTP:
// - create a UserTokenSecrets resource
// - store by token's ID using Server's IdentityService.
// - MFA:
// - store challenge by the token's ID
// - store by token's ID using Server's IdentityService.
// This field can be empty to use storage overrides.
token types.UserToken
// webIdentityOverride is an optional RegistrationIdentity override to be used
// to store webauthn challenge. A common override is decorating the regular
// Identity with an in-memory SessionData storage.
// Defaults to the Server's IdentityService.
webIdentityOverride wanlib.RegistrationIdentity
}
func (a *Server) createRegisterChallenge(ctx context.Context, req *newRegisterChallengeRequest) (*proto.MFARegisterChallenge, error) {
switch req.deviceType {
case proto.DeviceType_DEVICE_TYPE_TOTP:
if req.token == nil {
return nil, trace.BadParameter("all TOTP registrations require a privilege token")
}
otpKey, otpOpts, err := a.newTOTPKey(req.username)
if err != nil {
return nil, trace.Wrap(err)
}
token := req.token
secrets, err := a.createTOTPUserTokenSecrets(ctx, token, otpKey)
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.MFARegisterChallenge{
Request: &proto.MFARegisterChallenge_TOTP{
TOTP: &proto.TOTPRegisterChallenge{
Secret: otpKey.Secret(),
Issuer: otpKey.Issuer(),
PeriodSeconds: uint32(otpOpts.Period),
Algorithm: otpOpts.Algorithm.String(),
Digits: uint32(otpOpts.Digits.Length()),
Account: otpKey.AccountName(),
QRCode: secrets.GetQRCode(),
ID: token.GetName(),
},
},
}, nil
case proto.DeviceType_DEVICE_TYPE_WEBAUTHN:
cap, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
webConfig, err := cap.GetWebauthn()
if err != nil {
return nil, trace.Wrap(err)
}
identity := req.webIdentityOverride
if identity == nil {
identity = a.Services
}
webRegistration := &wanlib.RegistrationFlow{
Webauthn: webConfig,
Identity: identity,
}
passwordless := req.deviceUsage == proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS
credentialCreation, err := webRegistration.Begin(ctx, req.username, passwordless)
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.MFARegisterChallenge{Request: &proto.MFARegisterChallenge_Webauthn{
Webauthn: wantypes.CredentialCreationToProto(credentialCreation),
}}, nil
default:
return nil, trace.BadParameter("MFA device type %q unsupported", req.deviceType.String())
}
}
// GetMFADevices returns all mfa devices for the user defined in the token or the user defined in context.
func (a *Server) GetMFADevices(ctx context.Context, req *proto.GetMFADevicesRequest) (*proto.GetMFADevicesResponse, error) {
var username string
if req.GetTokenID() != "" {
token, err := a.GetUserToken(ctx, req.GetTokenID())
if err != nil {
a.logger.ErrorContext(ctx, "failed to retrieve user token", "error", err)
return nil, trace.AccessDenied("invalid token")
}
if err := a.verifyUserToken(ctx, token, authclient.UserTokenTypeRecoveryApproved); err != nil {
return nil, trace.Wrap(err)
}
username = token.GetUser()
}
if username == "" {
var err error
username, err = authz.GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}
devs, err := a.Services.GetMFADevices(ctx, username, false)
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.GetMFADevicesResponse{
Devices: devs,
}, nil
}
// DeleteMFADeviceSync implements AuthService.DeleteMFADeviceSync.
func (a *Server) DeleteMFADeviceSync(ctx context.Context, req *proto.DeleteMFADeviceSyncRequest) error {
var user string
switch {
case req.TokenID != "":
token, err := a.GetUserToken(ctx, req.TokenID)
if err != nil {
a.logger.ErrorContext(ctx, "failed to retrieve user token", "error", err)
return trace.AccessDenied("invalid token")
}
user = token.GetUser()
if err := a.verifyUserToken(ctx, token, authclient.UserTokenTypeRecoveryApproved, authclient.UserTokenTypePrivilege); err != nil {
return trace.Wrap(err)
}
case req.ExistingMFAResponse != nil:
var err error
user, err = authz.GetClientUsername(ctx)
if err != nil {
return trace.Wrap(err)
}
requiredExt := &mfav1.ChallengeExtensions{Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES}
if _, err := a.ValidateMFAAuthResponse(ctx, req.ExistingMFAResponse, user, requiredExt); err != nil {
return trace.Wrap(err)
}
default:
return trace.BadParameter(
"deleting an MFA device requires either a privilege token or a solved authentication challenge")
}
_, err := a.deleteMFADeviceSafely(ctx, user, req.DeviceName)
return trace.Wrap(err)
}
// deleteMFADeviceSafely deletes the user's mfa device while preventing users
// from locking themselves out of their account.
//
// Deletes are not allowed in the following situations:
// - Last MFA device when the cluster requires MFA
// - Last resident key credential in a passwordless-capable cluster (avoids
// passwordless users from locking themselves out).
func (a *Server) deleteMFADeviceSafely(ctx context.Context, user, deviceName string) (*types.MFADevice, error) {
mfaDevices, err := a.Services.GetMFADevices(ctx, user, true)
if err != nil {
return nil, trace.Wrap(err)
}
readOnlyAuthPref, err := a.GetReadOnlyAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
isPasskey := func(d *types.MFADevice) bool {
return d.GetWebauthn() != nil && d.GetWebauthn().ResidentKey
}
var deviceToDelete *types.MFADevice
remainingDevices := make(map[types.SecondFactorType]int)
var remainingPasskeys int
// Find the device to delete and count devices.
for _, d := range mfaDevices {
// Match device by name or ID.
if d.GetName() == deviceName || d.Id == deviceName {
deviceToDelete = d
switch d.Device.(type) {
case *types.MFADevice_Totp, *types.MFADevice_U2F, *types.MFADevice_Webauthn:
case *types.MFADevice_Sso:
return nil, trace.BadParameter("cannot delete ephemeral SSO MFA device")
default:
return nil, trace.NotImplemented("cannot delete device of type %T", d.Device)
}
continue
}
switch d.Device.(type) {
case *types.MFADevice_Totp:
remainingDevices[types.SecondFactorType_SECOND_FACTOR_TYPE_OTP]++
case *types.MFADevice_U2F, *types.MFADevice_Webauthn:
remainingDevices[types.SecondFactorType_SECOND_FACTOR_TYPE_WEBAUTHN]++
case *types.MFADevice_Sso:
remainingDevices[types.SecondFactorType_SECOND_FACTOR_TYPE_SSO]++
default:
a.logger.WarnContext(ctx, "Ignoring unknown device type in deletion", "device_type", logutils.TypeAttr(d.Device))
continue
}
if isPasskey(d) {
remainingPasskeys++
}
}
if deviceToDelete == nil {
return nil, trace.NotFound("MFA device %q does not exist", deviceName)
}
var remainingAllowedDevices int
for _, sf := range readOnlyAuthPref.GetSecondFactors() {
remainingAllowedDevices += remainingDevices[sf]
}
// Prevent users from deleting their last allowed device for clusters that require second factors.
if readOnlyAuthPref.IsSecondFactorEnforced() && remainingAllowedDevices == 0 {
return nil, trace.BadParameter("cannot delete the last MFA device for this user; add a replacement device first to avoid getting locked out")
}
// Check whether the device to delete is the last passwordless device,
// and whether deleting it would lockout the user from login.
//
// Note: the user may already be locked out from login if a password
// is not set and passwordless is disabled. Prevent them from deleting
// their last passkey to prevent them from being locked out further,
// in the case of passwordless being re-enabled.
if isPasskey(deviceToDelete) && remainingPasskeys == 0 {
u, err := a.Services.GetUser(ctx, user, false /* withSecrets */)
if err != nil {
return nil, trace.Wrap(err)
}
if u.GetUserType() != types.UserTypeSSO && u.GetPasswordState() != types.PasswordState_PASSWORD_STATE_SET {
return nil, trace.BadParameter("cannot delete last passwordless credential for user")
}
}
if err := a.DeleteMFADevice(ctx, user, deviceToDelete.Id); err != nil {
return nil, trace.Wrap(err)
}
// Emit deleted event.
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.MFADeviceDelete{
Metadata: apievents.Metadata{
Type: events.MFADeviceDeleteEvent,
Code: events.MFADeviceDeleteEventCode,
ClusterName: clusterName.GetClusterName(),
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, user),
MFADeviceMetadata: mfaDeviceEventMetadata(deviceToDelete),
ConnectionMetadata: authz.ConnectionMetadata(ctx),
}); err != nil {
return nil, trace.Wrap(err)
}
return deviceToDelete, nil
}
// AddMFADeviceSync implements AuthService.AddMFADeviceSync.
func (a *Server) AddMFADeviceSync(ctx context.Context, req *proto.AddMFADeviceSyncRequest) (*proto.AddMFADeviceSyncResponse, error) {
// Use either the explicitly provided token or the TOTP token created by
// CreateRegisterChallenge.
token := req.GetTokenID()
if token == "" {
token = req.GetNewMFAResponse().GetTOTP().GetID()
}
var username string
switch {
case token != "":
privilegeToken, err := a.GetUserToken(ctx, token)
if err != nil {
a.logger.ErrorContext(ctx, "failed to retrieve user token", "error", err)
return nil, trace.AccessDenied("invalid token")
}
if err := a.verifyUserToken(
ctx,
privilegeToken,
authclient.UserTokenTypePrivilege,
authclient.UserTokenTypePrivilegeException,
userTokenTypePrivilegeOTP,
); err != nil {
return nil, trace.Wrap(err)
}
username = privilegeToken.GetUser()
default: // ContextUser
var err error
username, err = authz.GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}
dev, err := a.verifyMFARespAndAddDevice(ctx, &newMFADeviceFields{
username: username,
newDeviceName: req.GetNewDeviceName(),
tokenID: token,
deviceResp: req.GetNewMFAResponse(),
deviceUsage: req.DeviceUsage,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.AddMFADeviceSyncResponse{Device: dev}, nil
}
type newMFADeviceFields struct {
username string
newDeviceName string
// tokenID is the ID of a reset/invite/recovery/privilege token.
// It is generally used to recover the TOTP secret stored in the token.
tokenID string
// webIdentityOverride is an optional RegistrationIdentity override to be used
// for device registration. A common override is decorating the regular
// Identity with an in-memory SessionData storage.
// Defaults to the Server's IdentityService.
webIdentityOverride wanlib.RegistrationIdentity
// deviceResp is the register response from the new device.
deviceResp *proto.MFARegisterResponse
// deviceUsage describes the intended usage of the new device.
deviceUsage proto.DeviceUsage
}
// verifyMFARespAndAddDevice validates MFA register response and on success adds the new MFA device.
func (a *Server) verifyMFARespAndAddDevice(ctx context.Context, req *newMFADeviceFields) (*types.MFADevice, error) {
if len(req.newDeviceName) > mfaDeviceNameMaxLen {
return nil, trace.BadParameter("device name must be %v characters or less", mfaDeviceNameMaxLen)
}
cap, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if !cap.IsSecondFactorEnabled() {
return nil, trace.BadParameter("second factor disabled by cluster configuration")
}
var dev *types.MFADevice
switch req.deviceResp.GetResponse().(type) {
case *proto.MFARegisterResponse_TOTP:
dev, err = a.registerTOTPDevice(ctx, req.deviceResp, req)
if err != nil {
return nil, trace.Wrap(err)
}
case *proto.MFARegisterResponse_Webauthn:
dev, err = a.registerWebauthnDevice(ctx, req.deviceResp, req)
if err != nil {
return nil, trace.Wrap(err)
}
default:
return nil, trace.BadParameter("MFARegisterResponse is an unknown response type %T", req.deviceResp.Response)
}
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.MFADeviceAdd{
Metadata: apievents.Metadata{
Type: events.MFADeviceAddEvent,
Code: events.MFADeviceAddEventCode,
ClusterName: clusterName.GetClusterName(),
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, req.username),
MFADeviceMetadata: mfaDeviceEventMetadata(dev),
ConnectionMetadata: authz.ConnectionMetadata(ctx),
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit add mfa device event", "error", err)
}
return dev, nil
}
func (a *Server) registerTOTPDevice(ctx context.Context, regResp *proto.MFARegisterResponse, req *newMFADeviceFields) (*types.MFADevice, error) {
cap, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if !cap.IsSecondFactorTOTPAllowed() {
return nil, trace.BadParameter("second factor TOTP not allowed by cluster")
}
if req.tokenID == "" {
return nil, trace.BadParameter("missing TOTP secret")
}
secrets, err := a.GetUserTokenSecrets(ctx, req.tokenID)
if err != nil {
return nil, trace.Wrap(err)
}
secret := secrets.GetOTPKey()
dev, err := services.NewTOTPDevice(req.newDeviceName, secret, a.clock.Now())
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.checkTOTP(ctx, req.username, regResp.GetTOTP().GetCode(), dev); err != nil {
return nil, trace.Wrap(err)
}
if err := a.UpsertMFADevice(ctx, req.username, dev); err != nil {
return nil, trace.Wrap(err)
}
return dev, nil
}
func (a *Server) registerWebauthnDevice(ctx context.Context, regResp *proto.MFARegisterResponse, req *newMFADeviceFields) (*types.MFADevice, error) {
cap, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if !cap.IsSecondFactorWebauthnAllowed() {
return nil, trace.BadParameter("second factor webauthn not allowed by cluster")
}
webConfig, err := cap.GetWebauthn()
if err != nil {
return nil, trace.Wrap(err)
}
identity := req.webIdentityOverride // Override Identity, if supplied.
if identity == nil {
identity = a.Services
}
webRegistration := &wanlib.RegistrationFlow{
Webauthn: webConfig,
Identity: identity,
}
// Finish upserts the device on success.
dev, err := webRegistration.Finish(ctx, wanlib.RegisterResponse{
User: req.username,
DeviceName: req.newDeviceName,
CreationResponse: wantypes.CredentialCreationResponseFromProto(regResp.GetWebauthn()),
Passwordless: req.deviceUsage == proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS,
})
return dev, trace.Wrap(err)
}
// GetWebSession returns existing web session described by req. Explicitly
// delegating to Services as it's directly implemented by Cache as well.
func (a *Server) GetWebSession(ctx context.Context, req types.GetWebSessionRequest) (types.WebSession, error) {
return a.Services.GetWebSession(ctx, req)
}
// GetWebToken returns existing web token described by req. Explicitly
// delegating to Services as it's directly implemented by Cache as well.
func (a *Server) GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
return a.Services.GetWebToken(ctx, req)
}
// ExtendWebSession creates a new web session for a user based on a valid previous (current) session.
//
// If there is an approved access request, additional roles are appended to the roles that were
// extracted from identity. The new session expiration time will not exceed the expiration time
// of the previous session.
//
// If there is a switchback request, the roles will switchback to user's default roles and
// the expiration time is derived from users recently logged in time.
func (a *Server) ExtendWebSession(ctx context.Context, req authclient.WebSessionReq, identity tlsca.Identity) (types.WebSession, error) {
prevSession, err := a.GetWebSession(ctx, types.GetWebSessionRequest{
User: req.User,
SessionID: req.PrevSessionID,
})
if err != nil {
return nil, trace.Wrap(err)
}
// consider absolute expiry time that may be set for this session
// by some external identity service, so we can not renew this session
// anymore without extra logic for renewal with external OIDC provider
expiresAt := prevSession.GetExpiryTime()
if !expiresAt.IsZero() && expiresAt.Before(a.clock.Now().UTC()) {
return nil, trace.NotFound("web session has expired")
}
accessInfo, err := services.AccessInfoFromLocalTLSIdentity(identity)
if err != nil {
return nil, trace.Wrap(err)
}
roles := accessInfo.Roles
traits := accessInfo.Traits
allowedResourceIDs := accessInfo.AllowedResourceIDs
accessRequests := identity.ActiveRequests
if req.ReloadUser {
// We don't call from the cache layer because we want to
// retrieve the recently updated user. Otherwise, the cache
// returns stale data.
user, err := a.Identity.GetUser(ctx, req.User, false)
if err != nil {
return nil, trace.Wrap(err)
}
// Make sure to refresh the user login state.
userState, err := a.ulsGenerator.Refresh(ctx, user, a.UserLoginStates)
if err != nil {
return nil, trace.Wrap(err)
}
// Updating traits is needed for guided SSH flow in Discover.
traits = userState.GetTraits()
// Updating roles is needed for guided Connect My Computer flow in Discover.
roles = userState.GetRoles()
} else if req.AccessRequestID != "" {
accessRequest, err := a.getValidatedAccessRequest(ctx, identity, req.User, req.AccessRequestID)
if err != nil {
return nil, trace.Wrap(err)
}
roles = append(roles, accessRequest.GetRoles()...)
roles = apiutils.Deduplicate(roles)
accessRequests = apiutils.Deduplicate(append(accessRequests, req.AccessRequestID))
if len(accessRequest.GetRequestedResourceIDs()) > 0 {
// There's not a consistent way to merge multiple resource access
// requests, a user may be able to request access to different resources
// with different roles which should not overlap.
if len(allowedResourceIDs) > 0 {
return nil, trace.BadParameter("user is already logged in with a resource access request, cannot assume another")
}
allowedResourceIDs = accessRequest.GetRequestedResourceIDs()
}
webSessionTTL := a.getWebSessionTTL(accessRequest)
// Let the session expire with the shortest expiry time.
if expiresAt.After(webSessionTTL) {
expiresAt = webSessionTTL
}
} else if req.Switchback {
if prevSession.GetLoginTime().IsZero() {
return nil, trace.BadParameter("Unable to switchback, log in time was not recorded.")
}
// Get default/static roles.
userState, err := a.GetUserOrLoginState(ctx, req.User)
if err != nil {
return nil, trace.Wrap(err, "failed to switchback")
}
// Reset any search-based access requests
allowedResourceIDs = nil
// Calculate expiry time.
roleSet, err := services.FetchRoles(userState.GetRoles(), a, userState.GetTraits())
if err != nil {
return nil, trace.Wrap(err)
}
sessionTTL := roleSet.AdjustSessionTTL(apidefaults.CertDuration)
// Set default roles and expiration.
expiresAt = prevSession.GetLoginTime().UTC().Add(sessionTTL)
roles = userState.GetRoles()
accessRequests = nil
}
// Create a new web session with the same private key. This way, if the
// original session was an attested web session, the extended session will
// also be an attested web session.
prevSSHKey, err := keys.ParsePrivateKey(prevSession.GetSSHPriv())
if err != nil {
return nil, trace.Wrap(err)
}
prevTLSKey, err := keys.ParsePrivateKey(prevSession.GetTLSPriv())
if err != nil {
return nil, trace.Wrap(err)
}
// Keep existing device extensions in the new session.
opts := &newWebSessionOpts{}
if prevSession.GetHasDeviceExtensions() {
var err error
opts.deviceExtensions, err = decodeDeviceExtensionsFromSession(prevSession)
if err != nil {
return nil, trace.Wrap(err)
}
}
sessionTTL := utils.ToTTL(a.clock, expiresAt)
sess, _, err := a.newWebSession(ctx, NewWebSessionRequest{
User: req.User,
LoginIP: identity.LoginIP,
Roles: roles,
Traits: traits,
SessionTTL: sessionTTL,
AccessRequests: accessRequests,
RequestedResourceIDs: allowedResourceIDs,
SSHPrivateKey: prevSSHKey,
TLSPrivateKey: prevTLSKey,
}, opts)
if err != nil {
return nil, trace.Wrap(err)
}
// Keep preserving the login time.
sess.SetLoginTime(prevSession.GetLoginTime())
sess.SetConsumedAccessRequestID(req.AccessRequestID)
if err := a.upsertWebSession(ctx, sess); err != nil {
return nil, trace.Wrap(err)
}
return sess, nil
}
func decodeDeviceExtensionsFromSession(webSession types.WebSession) (*tlsca.DeviceExtensions, error) {
// Reading the extensions from the session itself means we are always taking
// them for a legitimate source (ie, certificates issued by Auth).
// We don't re-validate the certificates when decoding the extensions.
block, _ := pem.Decode(webSession.GetTLSCert())
if block == nil {
return nil, trace.BadParameter("failed to decode session TLS certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, trace.Wrap(err)
}
certIdentity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
if err != nil {
return nil, trace.Wrap(err)
}
return &certIdentity.DeviceExtensions, nil
}
// getWebSessionTTL returns the earliest expiration time of allowed in the access request.
func (a *Server) getWebSessionTTL(accessRequest types.AccessRequest) time.Time {
webSessionTTL := accessRequest.GetAccessExpiry()
sessionTTL := accessRequest.GetSessionTLL()
if sessionTTL.IsZero() {
return webSessionTTL
}
// Session TTL contains the time when the session should end.
// We need to subtract it from the creation time to get the
// session duration.
sessionDuration := sessionTTL.Sub(accessRequest.GetCreationTime())
// Calculate the adjusted session TTL.
adjustedSessionTTL := a.clock.Now().UTC().Add(sessionDuration)
// Adjusted TTL can't exceed webSessionTTL.
if adjustedSessionTTL.Before(webSessionTTL) {
return adjustedSessionTTL
}
return webSessionTTL
}
func (a *Server) getValidatedAccessRequest(ctx context.Context, identity tlsca.Identity, user string, accessRequestID string) (types.AccessRequest, error) {
reqFilter := types.AccessRequestFilter{
User: user,
ID: accessRequestID,
}
reqs, err := a.GetAccessRequests(ctx, reqFilter)
if err != nil {
return nil, trace.Wrap(err)
}
if len(reqs) < 1 {
return nil, trace.NotFound("access request %q not found", accessRequestID)
}
req := reqs[0]
if !req.GetState().IsApproved() {
if req.GetState().IsDenied() {
return nil, trace.AccessDenied("access request %q has been denied", accessRequestID)
}
if req.GetState().IsPromoted() {
return nil, trace.AccessDenied("access request %q has been promoted. Use access list to access resources.", accessRequestID)
}
return nil, trace.AccessDenied("access request %q is awaiting approval", accessRequestID)
}
if err := services.ValidateAccessRequestForUser(ctx, a.clock, a, req, identity); err != nil {
return nil, trace.Wrap(err)
}
accessExpiry := req.GetAccessExpiry()
if accessExpiry.Before(a.GetClock().Now()) {
return nil, trace.BadParameter("access request %q has expired", accessRequestID)
}
if req.GetAssumeStartTime() != nil && req.GetAssumeStartTime().After(a.GetClock().Now()) {
return nil, trace.BadParameter("access request %q can not be assumed until %v", accessRequestID, req.GetAssumeStartTime())
}
return req, nil
}
// CreateWebSession creates a new web session for user without any
// checks, is used by admins
func (a *Server) CreateWebSession(ctx context.Context, user string) (types.WebSession, error) {
u, err := a.GetUserOrLoginState(ctx, user)
if err != nil {
return nil, trace.Wrap(err)
}
session, err := a.CreateWebSessionFromReq(ctx, NewWebSessionRequest{
User: user,
Roles: u.GetRoles(),
Traits: u.GetTraits(),
LoginTime: a.clock.Now().UTC(),
})
return session, trace.Wrap(err)
}
// ExtractHostID returns host id based on the hostname
func ExtractHostID(hostName string, clusterName string) (string, error) {
suffix := "." + clusterName
if !strings.HasSuffix(hostName, suffix) {
return "", trace.BadParameter("expected suffix %q in %q", suffix, hostName)
}
return strings.TrimSuffix(hostName, suffix), nil
}
// GenerateHostCerts generates new host certificates (signed
// by the host certificate authority) for a node.
func (a *Server) GenerateHostCerts(ctx context.Context, req *proto.HostCertsRequest) (*proto.Certs, error) {
if err := req.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
if err := req.Role.Check(); err != nil {
return nil, err
}
if err := a.limiter.AcquireConnection(req.Role.String()); err != nil {
generateThrottledRequestsCount.Inc()
a.logger.DebugContext(ctx, "Rejecting request to generate host certs because host is rate limited",
"name", req.NodeName,
"host_id", req.HostID,
"role", req.Role,
)
return nil, trace.Wrap(err)
}
defer a.limiter.ReleaseConnection(req.Role.String())
// only observe latencies for non-throttled requests
start := a.clock.Now()
defer func() { generateRequestsLatencies.Observe(time.Since(start).Seconds()) }()
generateRequestsCount.Inc()
generateRequestsCurrent.Inc()
defer generateRequestsCurrent.Dec()
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// If the request contains 0.0.0.0, this implies an advertise IP was not
// specified on the node. Try and guess what the address by replacing 0.0.0.0
// with the RemoteAddr as known to the Auth Server.
if slices.Contains(req.AdditionalPrincipals, defaults.AnyAddress) {
remoteHost, err := utils.Host(req.RemoteAddr)
if err != nil {
return nil, trace.Wrap(err)
}
req.AdditionalPrincipals = utils.ReplaceInSlice(
req.AdditionalPrincipals,
defaults.AnyAddress,
remoteHost)
}
if _, _, _, _, err := ssh.ParseAuthorizedKey(req.PublicSSHKey); err != nil {
return nil, trace.BadParameter("failed to parse SSH public key")
}
cryptoPubKey, err := keys.ParsePublicKey(req.PublicTLSKey)
if err != nil {
return nil, trace.Wrap(err)
}
// get the certificate authority that will be signing the public key of the host,
client := a.Cache
if req.NoCache {
client = a.Services
}
ca, err := client.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: clusterName.GetClusterName(),
}, true)
if err != nil {
return nil, trace.BadParameter("failed to load host CA for %q: %v", clusterName.GetClusterName(), err)
}
// could be a couple of scenarios, either client data is out of sync,
// or auth server is out of sync, either way, for now check that
// cache is out of sync, this will result in higher read rate
// to the backend, which is a fine tradeoff
if !req.NoCache && !req.Rotation.IsZero() && !req.Rotation.Matches(ca.GetRotation()) {
a.logger.DebugContext(ctx, "Client sent rotation state and cache state mismatch, retrieving state from the DB",
"client_state", req.Rotation,
"cache_state", ca.GetRotation(),
)
ca, err = a.Services.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: clusterName.GetClusterName(),
}, true)
if err != nil {
return nil, trace.BadParameter("failed to load host CA for %q: %v", clusterName.GetClusterName(), err)
}
if !req.Rotation.Matches(ca.GetRotation()) {
return nil, trace.BadParameter(""+
"the client expected state is out of sync, server rotation state: %v, "+
"client rotation state: %v, re-register the client from scratch to fix the issue.",
ca.GetRotation(), req.Rotation)
}
}
isAdminRole := req.Role == types.RoleAdmin
cert, signer, err := a.keyStore.GetTLSCertAndSigner(ctx, ca)
if trace.IsNotFound(err) && isAdminRole {
// If there is no local TLS signer found in the host CA ActiveKeys, this
// auth server may have a newly configured HSM and has only populated
// local keys in the AdditionalTrustedKeys until the next CA rotation.
// This is the only case where we should be able to get a signer from
// AdditionalTrustedKeys but not ActiveKeys.
cert, signer, err = a.keyStore.GetAdditionalTrustedTLSCertAndSigner(ctx, ca)
}
if err != nil {
return nil, trace.Wrap(err)
}
tlsAuthority, err := tlsca.FromCertAndSigner(cert, signer)
if err != nil {
return nil, trace.Wrap(err)
}
caSigner, err := a.keyStore.GetSSHSigner(ctx, ca)
if trace.IsNotFound(err) && isAdminRole {
// If there is no local SSH signer found in the host CA ActiveKeys, this
// auth server may have a newly configured HSM and has only populated
// local keys in the AdditionalTrustedKeys until the next CA rotation.
// This is the only case where we should be able to get a signer from
// AdditionalTrustedKeys but not ActiveKeys.
caSigner, err = a.keyStore.GetAdditionalTrustedSSHSigner(ctx, ca)
}
if err != nil {
return nil, trace.Wrap(err)
}
// generate host SSH certificate
hostSSHCert, err := a.generateHostCert(ctx, sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: req.PublicSSHKey,
HostID: req.HostID,
NodeName: req.NodeName,
Identity: sshca.Identity{
ClusterName: clusterName.GetClusterName(),
SystemRole: req.Role,
Principals: req.AdditionalPrincipals,
},
})
if err != nil {
return nil, trace.Wrap(err)
}
if req.Role == types.RoleInstance && len(req.SystemRoles) == 0 {
return nil, trace.BadParameter("cannot generate instance cert with no system roles")
}
systemRoles := make([]string, 0, len(req.SystemRoles))
for _, r := range req.SystemRoles {
systemRoles = append(systemRoles, string(r))
}
// generate host TLS certificate
identity := tlsca.Identity{
Username: utils.HostFQDN(req.HostID, clusterName.GetClusterName()),
Groups: []string{req.Role.String()},
TeleportCluster: clusterName.GetClusterName(),
SystemRoles: systemRoles,
}
subject, err := identity.Subject()
if err != nil {
return nil, trace.Wrap(err)
}
certRequest := tlsca.CertificateRequest{
Clock: a.clock,
PublicKey: cryptoPubKey,
Subject: subject,
NotAfter: a.clock.Now().UTC().Add(defaults.CATTL),
DNSNames: slices.Clone(req.AdditionalPrincipals),
}
// API requests need to specify a DNS name, which must be present in the certificate's DNS Names.
// The target DNS is not always known in advance, so we add a default one to all certificates.
certRequest.DNSNames = append(certRequest.DNSNames, DefaultDNSNamesForRole(req.Role)...)
// Unlike additional principals, DNS Names is x509 specific and is limited
// to services with TLS endpoints (e.g. auth, proxies, kubernetes)
if (types.SystemRoles{req.Role}).IncludeAny(
types.RoleAuth,
types.RoleAdmin,
types.RoleProxy,
types.RoleRelay,
types.RoleKube,
types.RoleWindowsDesktop,
) {
certRequest.DNSNames = append(certRequest.DNSNames, req.DNSNames...)
}
hostTLSCert, err := tlsAuthority.GenerateCertificate(certRequest)
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.Certs{
SSH: hostSSHCert,
TLS: hostTLSCert,
TLSCACerts: services.GetTLSCerts(ca),
SSHCACerts: services.GetSSHCheckingKeys(ca),
}, nil
}
// AssertSystemRole is used by agents to prove that they have a given system role when their credentials
// originate from multiple separate join tokens so that they can be issued an instance certificate that
// encompasses all of their capabilities. This method will be deprecated once we have a more comprehensive
// model for join token joining/replacement.
func (a *Server) AssertSystemRole(ctx context.Context, req proto.SystemRoleAssertion) error {
return trace.Wrap(a.Unstable.AssertSystemRole(ctx, req))
}
// GetSystemRoleAssertions is used in validated claims made by older instances to prove that they hold a given
// system role. This method will be deprecated once we have a more comprehensive model for join token
// joining/replacement.
func (a *Server) GetSystemRoleAssertions(ctx context.Context, serverID string, assertionID string) (proto.SystemRoleAssertionSet, error) {
set, err := a.Unstable.GetSystemRoleAssertions(ctx, serverID, assertionID)
return set, trace.Wrap(err)
}
func (a *Server) RegisterInventoryControlStream(ics client.UpstreamInventoryControlStream, hello *proto.UpstreamInventoryHello) error {
// upstream hello is pulled and checked at rbac layer. we wait to send the downstream hello until we get here
// in order to simplify creation of in-memory streams when dealing with local auth (note: in theory we could
// send hellos simultaneously to slightly improve perf, but there is a potential benefit to having the
// downstream hello serve double-duty as an indicator of having successfully transitioned the rbac layer).
downstreamHello := &proto.DownstreamInventoryHello{
Version: teleport.Version,
ServerID: a.ServerID,
Capabilities: &proto.DownstreamInventoryHello_SupportedCapabilities{
NodeHeartbeats: true,
AppHeartbeats: true,
AppCleanup: true,
DatabaseHeartbeats: true,
DatabaseHeartbeatGracefulStop: true,
DatabaseCleanup: true,
KubernetesHeartbeats: true,
KubernetesCleanup: true,
RelayServerHeartbeatsCleanup: true,
},
}
if err := ics.Send(a.CloseContext(), downstreamHello); err != nil {
return trace.Wrap(err)
}
a.inventory.RegisterControlStream(ics, hello)
return nil
}
// MakeLocalInventoryControlStream sets up an in-memory control stream which automatically registers with this auth
// server upon hello exchange.
func (a *Server) MakeLocalInventoryControlStream(opts ...client.ICSPipeOption) client.DownstreamInventoryControlStream {
upstream, downstream := client.InventoryControlStreamPipe(opts...)
go func() {
select {
case msg := <-upstream.Recv():
hello, ok := msg.(*proto.UpstreamInventoryHello)
if !ok {
upstream.CloseWithError(trace.BadParameter("expected upstream hello, got: %T", msg))
return
}
if err := a.RegisterInventoryControlStream(upstream, hello); err != nil {
upstream.CloseWithError(err)
return
}
case <-upstream.Done():
case <-a.CloseContext().Done():
upstream.Close()
}
}()
return downstream
}
func (a *Server) GetInventoryStatus(ctx context.Context, req *proto.InventoryStatusRequest) (*proto.InventoryStatusSummary, error) {
rsp := new(proto.InventoryStatusSummary)
if req.Connected {
a.inventory.UniqueHandles(func(handle inventory.UpstreamHandle) {
rsp.Connected = append(rsp.Connected, handle.Hello())
})
// connected instance list is a special case, don't bother aggregating heartbeats
return rsp, nil
}
rsp.VersionCounts = make(map[string]uint32)
rsp.UpgraderCounts = make(map[string]uint32)
rsp.ServiceCounts = make(map[string]uint32)
ins := a.GetInstances(ctx, types.InstanceFilter{})
for ins.Next() {
rsp.InstanceCount++
rsp.VersionCounts[vc.Normalize(ins.Item().GetTeleportVersion())]++
upgrader := ins.Item().GetExternalUpgrader()
if upgrader == "" {
upgrader = "none"
}
rsp.UpgraderCounts[upgrader]++
for _, service := range ins.Item().GetServices() {
rsp.ServiceCounts[string(service)]++
}
}
return rsp, ins.Done()
}
// GetInventoryConnectedServiceCounts returns the counts of each connected service seen in the inventory.
func (a *Server) GetInventoryConnectedServiceCounts() proto.InventoryConnectedServiceCounts {
return proto.InventoryConnectedServiceCounts{
ServiceCounts: a.inventory.ConnectedServiceCounts(),
}
}
// GetInventoryConnectedServiceCount returns the counts of a particular connected service seen in the inventory.
func (a *Server) GetInventoryConnectedServiceCount(service types.SystemRole) uint64 {
return a.inventory.ConnectedServiceCount(service)
}
func (a *Server) PingInventory(ctx context.Context, req proto.InventoryPingRequest) (proto.InventoryPingResponse, error) {
stream, ok := a.inventory.GetControlStream(req.ServerID)
if !ok {
return proto.InventoryPingResponse{}, trace.NotFound("no control stream found for server %q", req.ServerID)
}
id := mathrand.Uint64()
if req.ControlLog { //nolint:staticcheck // SA1019. Checking deprecated field that may be sent by older clients.
return proto.InventoryPingResponse{}, trace.BadParameter("ControlLog pings are not supported")
}
d, err := stream.Ping(ctx, id)
if err != nil {
return proto.InventoryPingResponse{}, trace.Wrap(err)
}
return proto.InventoryPingResponse{
Duration: d,
}, nil
}
// UpdateLabels updates the labels on an instance over the inventory control
// stream.
func (a *Server) UpdateLabels(ctx context.Context, req *proto.InventoryUpdateLabelsRequest) error {
stream, ok := a.inventory.GetControlStream(req.GetServerID())
if !ok {
return trace.NotFound("no control stream found for server %q", req.GetServerID())
}
return trace.Wrap(stream.UpdateLabels(ctx, req.GetKind(), req.GetLabels()))
}
// TokenExpiredOrNotFound is a special message returned by the auth server when provisioning
// tokens are either past their TTL, or could not be found.
const TokenExpiredOrNotFound = "token expired or not found"
// ValidateToken takes a provisioning token value and finds if it's valid. Returns
// a list of roles this token allows its owner to assume and token labels, or an error if the token
// cannot be found.
func (a *Server) ValidateToken(ctx context.Context, token string) (types.ProvisionToken, error) {
tkns, err := a.GetStaticTokens(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// First check if the token is a static token. If it is, return right away.
// Static tokens have no expiration.
for _, st := range tkns.GetStaticTokens() {
if subtle.ConstantTimeCompare([]byte(st.GetName()), []byte(token)) == 1 {
return st, nil
}
}
// If it's not a static token, check if it's a ephemeral token in the backend.
// If a ephemeral token is found, make sure it's still valid.
tok, err := a.GetToken(ctx, token)
if err != nil {
if trace.IsNotFound(err) {
return nil, trace.AccessDenied("%s", TokenExpiredOrNotFound)
}
return nil, trace.Wrap(err)
}
if !a.checkTokenTTL(tok) {
return nil, trace.AccessDenied("%s", TokenExpiredOrNotFound)
}
return tok, nil
}
// checkTokenTTL checks if the token is still valid. If it is not, the token
// is removed from the backend and returns false. Otherwise returns true.
func (a *Server) checkTokenTTL(tok types.ProvisionToken) bool {
// Always accept tokens without an expiry configured.
if tok.Expiry().IsZero() {
return true
}
now := a.clock.Now().UTC()
if tok.Expiry().Before(now) {
// Tidy up the expired token in background if it has expired.
go func() {
ctx, cancel := context.WithTimeout(a.CloseContext(), time.Second*30)
defer cancel()
if err := a.DeleteToken(ctx, tok.GetName()); err != nil {
if !trace.IsNotFound(err) {
a.logger.WarnContext(ctx, "Unable to delete token from backend", "error", err)
}
}
}()
return false
}
return true
}
func (a *Server) DeleteToken(ctx context.Context, token string) (err error) {
tkns, err := a.GetStaticTokens(ctx)
if err != nil {
return trace.Wrap(err)
}
// is this a static token?
for _, st := range tkns.GetStaticTokens() {
if subtle.ConstantTimeCompare([]byte(st.GetName()), []byte(token)) == 1 {
return trace.BadParameter("token %s is statically configured and cannot be removed", backend.MaskKeyName(token))
}
}
// Delete a user token.
if err = a.DeleteUserToken(ctx, token); err == nil {
return nil
}
// delete node token:
if err = a.Services.DeleteToken(ctx, token); err == nil {
return nil
}
return trace.Wrap(err)
}
// GetTokens returns all tokens (machine provisioning ones and user tokens). Machine
// tokens usually have "node roles", like auth,proxy,node and user invitation tokens have 'signup' role
// Deprecated: Use [ListProvisionTokens], [ListUserTokens], and [GetStaticTokens] instead.
// TODO(hugoShaka): DELETE IN 21.0.0
func (a *Server) GetTokens(ctx context.Context, opts ...services.MarshalOption) ([]types.ProvisionToken, error) {
// get node tokens:
tokens, err := iterstream.Collect(
clientutils.Resources(
ctx,
// ListProvisionTokens take too many arguments for [clientutils.Resources]
// so we wrap it to get the usual paginated signature.
func(ctx context.Context, pageSize int, pageKey string) ([]types.ProvisionToken, string, error) {
return a.Services.ListProvisionTokens(ctx, pageSize, pageKey, nil, "")
},
),
)
if err != nil {
return nil, trace.Wrap(err)
}
// get static tokens:
tkns, err := a.GetStaticTokens(ctx)
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
if err == nil {
tokens = append(tokens, tkns.GetStaticTokens()...)
}
// get user tokens:
userTokens, err := iterstream.Collect(clientutils.Resources(ctx, a.Services.ListUserTokens))
if err != nil {
return nil, trace.Wrap(err, "retrieving user tokens")
}
// convert user tokens to machine tokens:
for _, t := range userTokens {
roles := types.SystemRoles{types.RoleSignup}
tok, err := types.NewProvisionToken(t.GetName(), roles, t.Expiry())
if err != nil {
return nil, trace.Wrap(err)
}
tokens = append(tokens, tok)
}
return tokens, nil
}
// GetWebSessionInfo returns the web session specified with sessionID for the given user.
// The session is stripped of any authentication details.
// Implements auth.WebUIService
func (a *Server) GetWebSessionInfo(ctx context.Context, user, sessionID string) (types.WebSession, error) {
sess, err := a.GetWebSession(ctx, types.GetWebSessionRequest{User: user, SessionID: sessionID})
if err != nil {
return nil, trace.Wrap(err)
}
return sess.WithoutSecrets(), nil
}
// IterateRoles is a helper used to read a page of roles with a custom matcher, used by access-control logic to handle
// per-resource read permissions.
func (a *Server) IterateRoles(ctx context.Context, req *proto.ListRolesRequest, match func(*types.RoleV6) (bool, error)) ([]*types.RoleV6, string, error) {
const maxIterations = 100_000
if req.Limit == 0 {
req.Limit = apidefaults.DefaultChunkSize
}
req.Limit++
defer func() {
req.Limit--
}()
var filtered []*types.RoleV6
var iterations int
Outer:
for {
iterations++
if iterations > maxIterations {
return nil, "", trace.Errorf("too many role page iterations (%d), this is likely a bug", iterations)
}
rsp, err := a.Cache.ListRoles(ctx, req)
if err != nil {
return nil, "", trace.Wrap(err)
}
Inner:
for _, role := range rsp.Roles {
ok, err := match(role)
if err != nil {
return nil, "", trace.Wrap(err)
}
if !ok {
continue Inner
}
filtered = append(filtered, role)
if len(filtered) == int(req.Limit) {
break Outer
}
}
req.StartKey = rsp.NextKey
if req.StartKey == "" {
break Outer
}
}
var nextKey string
if len(filtered) == int(req.Limit) {
nextKey = filtered[req.Limit-1].GetName()
filtered = filtered[:req.Limit-1]
}
return filtered, nextKey, nil
}
// ListAccessRequests is an access request getter with pagination and sorting options.
func (a *Server) ListAccessRequests(ctx context.Context, req *proto.ListAccessRequestsRequest) (*proto.ListAccessRequestsResponse, error) {
// most access request methods target the backend directly since access requests are frequently read
// immediately after writing, but listing requires support for custom sort orders so we route it to
// a special cache. note that the access request cache will still end up forwarding single-request
// reads to the real backend due to the read after write issue.
return a.AccessRequestCache.ListAccessRequests(ctx, req)
}
// ListMatchingAccessRequests is equivalent to ListAccessRequests except that it adds the ability to provide an arbitrary matcher function. This method
// should be preferred when using custom filtering (e.g. access-controls), since the paginations keys used by the access request cache are non-standard.
func (a *Server) ListMatchingAccessRequests(ctx context.Context, req *proto.ListAccessRequestsRequest, match func(*types.AccessRequestV3) bool) (*proto.ListAccessRequestsResponse, error) {
// most access request methods target the backend directly since access requests are frequently read
// immediately after writing, but listing requires support for custom sort orders so we route it to
// a special cache. note that the access request cache will still end up forwarding single-request
// reads to the real backend due to the read after write issue.
return a.AccessRequestCache.ListMatchingAccessRequests(ctx, req, match)
}
func (a *Server) CreateAccessRequestV2(ctx context.Context, req types.AccessRequest, identity tlsca.Identity) (types.AccessRequest, error) {
now := a.clock.Now().UTC()
req.SetCreationTime(now)
// Always perform variable expansion on creation.
expandOpts := services.WithExpandVars(true)
if err := services.ValidateAccessRequestForUser(ctx, a.clock, a, req, identity, expandOpts); err != nil {
return nil, trace.Wrap(err)
}
// Look for user groups and associated applications to the request.
requestedResourceIDs, err := a.appendImplicitlyRequiredResources(ctx, req.GetRequestedResourceIDs())
if err != nil {
return nil, trace.Wrap(err, "adding additional implicitly required resources")
}
req.SetRequestedResourceIDs(requestedResourceIDs)
if err := a.checkResourcesRequestable(ctx, requestedResourceIDs); err != nil {
return nil, trace.Wrap(err)
}
// Fetch all Access Lists from the Cache, for use in generating Request promotions and validating long-term grouping
allAccessLists, err := a.Cache.GetAccessLists(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
var longTermResourceGrouping *types.LongTermResourceGrouping
if req.GetRequestKind().IsLongTerm() {
longTermResourceGrouping, err = a.generateLongTermResourceGrouping(ctx, req, allAccessLists)
if err != nil {
return nil, trace.Wrap(err)
}
if longTermResourceGrouping == nil {
return nil, trace.Errorf("Failed to find optimal resource grouping for long-term access")
}
if !longTermResourceGrouping.CanProceed && !req.GetDryRun() {
return nil, trace.BadParameter("%s", longTermResourceGrouping.ValidationMessage)
}
}
if req.GetDryRun() {
// NOTE: Some dry-run options are set in [services.ValidateAccessRequestForUser].
_, promotions := a.generateAccessRequestPromotions(ctx, req, allAccessLists)
// TODO(kiosion): if long-term, skip promotion generation, and instead, use info from LongTermResourceGrouping to add additional reviewers.
updateAccessRequestWithAdditionalReviewers(ctx, req, a.AccessLists, promotions)
if req.GetRequestKind().IsLongTerm() {
req.SetLongTermResourceGrouping(longTermResourceGrouping)
}
// Return before creating the request if this is a dry run.
return req, nil
}
if err := a.verifyAccessRequestMonthlyLimit(ctx); err != nil {
return nil, trace.Wrap(err)
}
a.logger.DebugContext(ctx, "Creating Access Request",
"request_name", req.GetName(),
"request_expiry", req.Expiry(),
)
if _, err := a.Services.CreateAccessRequestV2(ctx, req); err != nil {
return nil, trace.Wrap(err)
}
// We want the long-term grouping info present in the returned request,
// but don't want it persisted via setting before saving the request.
if req.GetRequestKind().IsLongTerm() {
req.SetLongTermResourceGrouping(longTermResourceGrouping)
}
var annotations *apievents.Struct
if sa := req.GetSystemAnnotations(); len(sa) > 0 {
var err error
annotations, err = apievents.EncodeMapStrings(sa)
if err != nil {
a.logger.DebugContext(ctx, "Failed to encode access request annotations", "error", err)
}
}
resourceNames, err := accessrequest.GetResourceNames(ctx, a.Services, req)
if err != nil {
return nil, trace.Wrap(err)
}
err = a.emitter.EmitAuditEvent(a.closeCtx, &apievents.AccessRequestCreate{
Metadata: apievents.Metadata{
Type: events.AccessRequestCreateEvent,
Code: events.AccessRequestCreateCode,
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, req.GetUser()),
ResourceMetadata: apievents.ResourceMetadata{
Expires: req.GetAccessExpiry(),
},
Roles: req.GetRoles(),
RequestedResourceIDs: apievents.ResourceIDs(req.GetRequestedResourceIDs()),
ResourceNames: resourceNames,
RequestID: req.GetName(),
RequestState: req.GetState().String(),
Reason: req.GetRequestReason(),
MaxDuration: req.GetMaxDuration(),
Annotations: annotations,
})
if err != nil {
a.logger.WarnContext(ctx, "Failed to emit access request create event", "error", err)
}
resources := []string{}
if len(req.GetRoles()) != 0 {
resources = append(resources, types.KindRole)
}
for _, resource := range req.GetRequestedResourceIDs() {
resources = append(resources, resource.Kind)
}
a.AnonymizeAndSubmit(&usagereporter.AccessRequestCreateEvent{
UserName: req.GetUser(),
ResourceKinds: apiutils.Deduplicate(resources),
})
// Create a notification.
var notificationText string
// If this is a resource request.
if len(req.GetRequestedResourceIDs()) > 0 {
notificationText = fmt.Sprintf("%s requested access to %d resources.", req.GetUser(), len(req.GetRequestedResourceIDs()))
if len(req.GetRequestedResourceIDs()) == 1 {
notificationText = fmt.Sprintf("%s requested access to a resource.", req.GetUser())
}
// If this is a role request.
} else {
notificationText = fmt.Sprintf("%s requested access to the '%s' role.", req.GetUser(), req.GetRoles()[0])
if len(req.GetRoles()) > 1 {
notificationText = fmt.Sprintf("%s requested access to %d roles.", req.GetUser(), len(req.GetRoles()))
}
}
_, err = a.Services.CreateGlobalNotification(ctx, &notificationsv1.GlobalNotification{
Spec: &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByPermissions{
ByPermissions: &notificationsv1.ByPermissions{
RoleConditions: []*types.RoleConditions{
{
ReviewRequests: &types.AccessReviewConditions{
Roles: req.GetOriginalRoles(),
},
},
},
},
},
// Prevent the requester from seeing the notification for their own access request.
ExcludeUsers: []string{req.GetUser()},
Notification: &notificationsv1.Notification{
Spec: &notificationsv1.NotificationSpec{},
SubKind: types.NotificationAccessRequestPendingSubKind,
Metadata: &headerv1.Metadata{
Labels: map[string]string{types.NotificationTitleLabel: notificationText, "request-id": req.GetName()},
Expires: timestamppb.New(req.Expiry()),
},
},
},
})
if err != nil {
a.logger.WarnContext(ctx, "Failed to create access request notification", "error", err)
}
// calculate the promotions
reqCopy, promotions := a.generateAccessRequestPromotions(ctx, req, allAccessLists)
if promotions != nil {
// Create the promotion entry even if the allowed promotion is empty. Otherwise, we won't
// be able to distinguish between an allowed empty set and generation failure.
if err := a.Services.CreateAccessRequestAllowedPromotions(ctx, reqCopy, promotions); err != nil {
a.logger.WarnContext(ctx, "Failed to update access request with promotions", "error", err)
}
}
accessRequestsCreatedMetric.WithLabelValues(
strconv.Itoa(len(req.GetRoles())),
strconv.Itoa(len(req.GetRequestedResourceIDs()))).Inc()
return req, nil
}
// appendImplicitlyRequiredResources examines the set of requested resources and adds
// any extra resources that are implicitly required by the request.
func (a *Server) appendImplicitlyRequiredResources(ctx context.Context, resources []types.ResourceID) ([]types.ResourceID, error) {
addedApps := set.New[string]()
var userGroups []types.ResourceID
var accountAssignments []types.ResourceID
for _, resource := range resources {
switch resource.Kind {
case types.KindApp:
addedApps.Add(resource.Name)
case types.KindUserGroup:
userGroups = append(userGroups, resource)
case types.KindIdentityCenterAccountAssignment:
accountAssignments = append(accountAssignments, resource)
}
}
for _, resource := range userGroups {
userGroup, err := a.GetUserGroup(ctx, resource.Name)
if err != nil {
return nil, trace.Wrap(err)
}
for _, app := range userGroup.GetApplications() {
// Only add to the request if we haven't already added it.
if !addedApps.Contains(app) {
resources = append(resources, types.ResourceID{
ClusterName: resource.ClusterName,
Kind: types.KindApp,
Name: app,
})
addedApps.Add(app)
}
}
}
icAccounts := set.New[string]()
for _, resource := range accountAssignments {
// The UI needs access to the account associated with an Account Assignment
// in order to display the enclosing Account, otherwise the user will not
// be able to see their assigned permission sets.
asmt, err := a.GetIdentityCenterAccountAssignment(ctx, resource.Name)
if err != nil {
return nil, trace.Wrap(err, "fetching identity center account assignment")
}
if icAccounts.Contains(asmt.GetSpec().GetAccountId()) {
continue
}
resources = append(resources, types.ResourceID{
ClusterName: resource.ClusterName,
Kind: types.KindIdentityCenterAccount,
Name: asmt.GetSpec().GetAccountId(),
})
icAccounts.Add(asmt.GetSpec().GetAccountId())
}
return resources, nil
}
func (a *Server) checkResourcesRequestable(ctx context.Context, resourceIDs []types.ResourceID) error {
if len(resourceIDs) == 0 {
return nil
}
err := okta.CheckResourcesRequestable(ctx, resourceIDs, a)
if errors.Is(err, okta.OktaResourceNotRequestableError) {
return trace.Wrap(err)
} else if err != nil {
return trace.Wrap(err, "checking if Okta-originated resources are requestable")
}
return nil
}
// cacheWithFetchedAccessLists is a wrapper around authclient.Cache to provide pre-fetched Access Lists
// for use in both generating long-term resource groupings, and generating access request promotions.
// This avoids the need to fetch Access Lists from the backend multiple times per-CreateAccessRequest call.
type cacheWithFetchedAccessLists struct {
authclient.Cache
fetchedACLs []*accesslist.AccessList
}
func (c *cacheWithFetchedAccessLists) GetAccessLists(context.Context) ([]*accesslist.AccessList, error) {
return c.fetchedACLs, nil
}
func (c *cacheWithFetchedAccessLists) ListAccessLists(context.Context, int, string) ([]*accesslist.AccessList, string, error) {
return c.fetchedACLs, "", nil
}
// generateLongTermResourceGrouping will validate and group resources based on coverage by access lists.
func (a *Server) generateLongTermResourceGrouping(ctx context.Context, req types.AccessRequest, acls []*accesslist.AccessList) (*types.LongTermResourceGrouping, error) {
return modules.GetModules().GenerateLongTermResourceGrouping(ctx, &cacheWithFetchedAccessLists{a.Cache, acls}, req)
}
// generateAccessRequestPromotions will return potential access list promotions for an access request. On error, this function will log
// the error and return whatever it has. The caller is expected to deal with the possibility of a nil promotions object.
func (a *Server) generateAccessRequestPromotions(ctx context.Context, req types.AccessRequest, acls []*accesslist.AccessList) (types.AccessRequest, *types.AccessRequestAllowedPromotions) {
reqCopy := req.Copy()
promotions, err := modules.GetModules().GenerateAccessRequestPromotions(ctx, &cacheWithFetchedAccessLists{a.Cache, acls}, reqCopy)
if err != nil {
// Do not fail the request if the promotions failed to generate.
// The request promotion will be blocked, but the request can still be approved.
a.logger.WarnContext(ctx, "Failed to generate access list promotions", "error", err)
}
return reqCopy, promotions
}
// updateAccessRequestWithAdditionalReviewers will update the given access request with additional reviewers given the promotions
// created for the access request.
func updateAccessRequestWithAdditionalReviewers(ctx context.Context, req types.AccessRequest, accessLists services.AccessListsGetter, promotions *types.AccessRequestAllowedPromotions) {
if promotions == nil {
return
}
// For promotions, add in access list owners as additional suggested reviewers
additionalReviewers := set.New[string]()
// Iterate through the promotions, adding the owners of the corresponding access lists as reviewers.
for _, promotion := range promotions.Promotions {
allOwners, err := accessLists.GetAccessListOwners(ctx, promotion.AccessListName)
if err != nil {
logger.WarnContext(ctx, "Failed to get nested access list owners, skipping additional reviewers", "error", err, "access_list", promotion.AccessListName)
break
}
for _, owner := range allOwners {
additionalReviewers.Add(owner.Name)
}
}
// Only modify the original request if additional reviewers were found.
if additionalReviewers.Len() > 0 {
req.SetSuggestedReviewers(append(req.GetSuggestedReviewers(), additionalReviewers.Elements()...))
}
}
func (a *Server) DeleteAccessRequest(ctx context.Context, name string) error {
if err := a.Services.DeleteAccessRequest(ctx, name); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.AccessRequestDelete{
Metadata: apievents.Metadata{
Type: events.AccessRequestDeleteEvent,
Code: events.AccessRequestDeleteCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
RequestID: name,
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit access request delete event", "error", err)
}
return nil
}
func (a *Server) SetAccessRequestState(ctx context.Context, params types.AccessRequestUpdate) error {
req, err := a.Services.SetAccessRequestState(ctx, params)
if err != nil {
return trace.Wrap(err)
}
event := &apievents.AccessRequestCreate{
Metadata: apievents.Metadata{
Type: events.AccessRequestUpdateEvent,
Code: events.AccessRequestUpdateCode,
},
ResourceMetadata: apievents.ResourceMetadata{
UpdatedBy: authz.ClientUsername(ctx),
Expires: req.GetAccessExpiry(),
},
RequestID: params.RequestID,
RequestState: params.State.String(),
Reason: params.Reason,
Roles: params.Roles,
AssumeStartTime: params.AssumeStartTime,
}
if sa := req.GetSystemAnnotations(); len(sa) > 0 {
var err error
event.Annotations, err = apievents.EncodeMapStrings(sa)
if err != nil {
a.logger.DebugContext(ctx, "Failed to encode access request annotations", "error", err)
}
}
if delegator := apiutils.GetDelegator(ctx); delegator != "" {
event.Delegator = delegator
}
if len(params.Annotations) > 0 {
annotations, err := apievents.EncodeMapStrings(params.Annotations)
if err != nil {
a.logger.DebugContext(ctx, "Failed to encode access request annotations", "error", err)
} else {
event.Annotations = annotations
}
}
err = a.emitter.EmitAuditEvent(a.closeCtx, event)
if err != nil {
a.logger.WarnContext(ctx, "Failed to emit access request update event", "error", err)
}
return trace.Wrap(err)
}
// SubmitAccessReview is used to process a review of an Access Request.
// This is implemented by Server.submitAccessRequest but this method exists
// to provide a matching signature with the auth client. This allows the
// hosted plugins to use the Server struct directly as a client.
func (a *Server) SubmitAccessReview(
ctx context.Context,
params types.AccessReviewSubmission,
) (types.AccessRequest, error) {
// identity is passed as nil as we do not know which user has triggered
// this action.
return a.submitAccessReview(ctx, params, nil)
}
// submitAccessReview implements submitting a review of an Access Request.
// The `identity` parameter should be the identity of the user that has called
// an RPC that has invoked this, if applicable. It may be nil if this is
// unknown.
func (a *Server) submitAccessReview(
ctx context.Context,
params types.AccessReviewSubmission,
identity *tlsca.Identity,
) (types.AccessRequest, error) {
// When promoting a request, the access list name must be set.
if params.Review.ProposedState.IsPromoted() && params.Review.GetAccessListName() == "" {
return nil, trace.BadParameter("promoted access list can be only set when promoting access requests")
}
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
// set up a checker for the review author
checker, err := services.NewReviewPermissionChecker(ctx, a, params.Review.Author, identity)
if err != nil {
return nil, trace.Wrap(err)
}
// don't bother continuing if the author has no allow directives
if !checker.HasAllowDirectives() {
return nil, trace.AccessDenied("user %q cannot submit reviews", params.Review.Author)
}
// final permission checks and review application must be done by the local backend
// service, as their validity depends upon optimistic locking.
req, err := a.ApplyAccessReview(ctx, params, checker)
if err != nil {
return nil, trace.Wrap(err)
}
event := &apievents.AccessRequestCreate{
Metadata: apievents.Metadata{
Type: events.AccessRequestReviewEvent,
Code: events.AccessRequestReviewCode,
ClusterName: clusterName.GetClusterName(),
},
ResourceMetadata: apievents.ResourceMetadata{
Expires: req.GetAccessExpiry(),
},
RequestID: params.RequestID,
RequestState: req.GetState().String(),
ProposedState: params.Review.ProposedState.String(),
Reason: params.Review.Reason,
Reviewer: params.Review.Author,
MaxDuration: req.GetMaxDuration(),
PromotedAccessListName: req.GetPromotedAccessListName(),
}
// Create a notification.
if !req.GetState().IsPending() {
_, err = a.Services.CreateUserNotification(ctx, generateAccessRequestReviewedNotification(req, params))
if err != nil {
a.logger.DebugContext(ctx, "Failed to emit access request reviewed notification", "error", err)
}
}
if len(params.Review.Annotations) > 0 {
annotations, err := apievents.EncodeMapStrings(params.Review.Annotations)
if err != nil {
a.logger.DebugContext(ctx, "Failed to encode access request annotations", "error", err)
} else {
event.Annotations = annotations
}
}
if err := a.emitter.EmitAuditEvent(a.closeCtx, event); err != nil {
a.logger.WarnContext(ctx, "Failed to emit access request update event", "error", err)
}
resources := []string{}
if len(req.GetRoles()) != 0 {
resources = append(resources, types.KindRole)
}
for _, resource := range req.GetRequestedResourceIDs() {
resources = append(resources, resource.Kind)
}
a.AnonymizeAndSubmit(&usagereporter.AccessRequestReviewEvent{
UserName: params.Review.Author,
ResourceKinds: apiutils.Deduplicate(resources),
IsBotReviewed: (params.Review.Author == teleport.SystemAccessApproverUserName),
ProposedState: prehogProposedStateFromRequestState(params.Review.ProposedState),
})
return req, nil
}
func prehogProposedStateFromRequestState(state types.RequestState) prehogv1a.AccessRequestReviewEvent_ProposedState {
switch state {
case types.RequestState_APPROVED:
return prehogv1a.AccessRequestReviewEvent_PROPOSED_STATE_APPROVED
case types.RequestState_DENIED:
return prehogv1a.AccessRequestReviewEvent_PROPOSED_STATE_DENIED
default:
return prehogv1a.AccessRequestReviewEvent_PROPOSED_STATE_UNSPECIFIED
}
}
// generateAccessRequestReviewedNotification returns the notification object for a notification notifying a user of their
// access request being approved or denied.
func generateAccessRequestReviewedNotification(req types.AccessRequest, params types.AccessReviewSubmission) *notificationsv1.Notification {
var subKind string
var reviewVerb string
if req.GetState().IsApproved() {
subKind = types.NotificationAccessRequestApprovedSubKind
reviewVerb = "approved"
} else if req.GetState().IsPromoted() {
subKind = types.NotificationAccessRequestPromotedSubKind
} else {
subKind = types.NotificationAccessRequestDeniedSubKind
reviewVerb = "denied"
}
var notificationText string
if req.GetState().IsPromoted() {
notificationText = fmt.Sprintf("%s promoted your access request to long-term access.", params.Review.Author)
} else {
// If this was a resource request.
if len(req.GetRequestedResourceIDs()) > 0 {
notificationText = fmt.Sprintf("%s %s your access request for %d resources.", params.Review.Author, reviewVerb, len(req.GetRequestedResourceIDs()))
if len(req.GetRequestedResourceIDs()) == 1 {
notificationText = fmt.Sprintf("%s %s your access request for a resource.", params.Review.Author, reviewVerb)
}
// If this was a role request.
} else {
notificationText = fmt.Sprintf("%s %s your access request for the '%s' role.", params.Review.Author, reviewVerb, req.GetRoles()[0])
if len(req.GetRoles()) > 1 {
notificationText = fmt.Sprintf("%s %s your access request for %d roles.", params.Review.Author, reviewVerb, len(req.GetRoles()))
}
}
}
assumableTime := ""
if req.GetAssumeStartTime() != nil {
assumableTime = req.GetAssumeStartTime().Format("2006-01-02T15:04:05.000Z0700")
}
return &notificationsv1.Notification{
Spec: &notificationsv1.NotificationSpec{
Username: req.GetUser(),
},
SubKind: subKind,
Metadata: &headerv1.Metadata{
Labels: map[string]string{
types.NotificationTitleLabel: notificationText,
"request-id": params.RequestID,
"roles": strings.Join(req.GetRoles(), ","),
"assumable-time": assumableTime,
},
Expires: timestamppb.New(req.Expiry()),
},
}
}
func (a *Server) GetAccessCapabilities(ctx context.Context, req types.AccessCapabilitiesRequest) (*types.AccessCapabilities, error) {
user, err := authz.UserFromContext(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
caps, err := services.CalculateAccessCapabilities(ctx, a.clock, a, user.GetIdentity(), req)
if err != nil {
return nil, trace.Wrap(err)
}
return caps, nil
}
func (a *Server) getCache() (c *cache.Cache, ok bool) {
c, ok = a.Cache.(*cache.Cache)
return
}
func (a *Server) NewStream(ctx context.Context, watch types.Watch) (stream.Stream[types.Event], error) {
if cache, ok := a.getCache(); ok {
// cache exposes a native stream implementation
return cache.NewStream(ctx, watch)
}
// fallback to wrapping a watcher in a stream.Stream adapter
watcher, err := a.Cache.NewWatcher(ctx, watch)
if err != nil {
return nil, trace.Wrap(err)
}
closer := func() {
watcher.Close()
}
return stream.Func(func() (types.Event, error) {
select {
case event := <-watcher.Events():
return event, nil
case <-watcher.Done():
err := watcher.Error()
if err == nil {
// stream.Func needs an error to signal end of stream. io.EOF is
// the expected "happy" end of stream singnal.
err = io.EOF
}
return types.Event{}, trace.Wrap(err)
}
}, closer), nil
}
// NewKeepAliver returns a new instance of keep aliver
func (a *Server) NewKeepAliver(ctx context.Context) (types.KeepAliver, error) {
cancelCtx, cancel := context.WithCancel(ctx)
k := &authKeepAliver{
a: a,
ctx: cancelCtx,
cancel: cancel,
keepAlivesC: make(chan types.KeepAlive),
}
go k.forwardKeepAlives()
return k, nil
}
// KeepAliveServer implements [services.Presence] by delegating to
// [Server.Services] and potentially emitting a [usagereporter] event.
func (a *Server) KeepAliveServer(ctx context.Context, h types.KeepAlive) error {
if err := a.Services.KeepAliveServer(ctx, h); err != nil {
return trace.Wrap(err)
}
// ResourceHeartbeatEvent only cares about a few KeepAlive types
kind := usagereporter.ResourceKindFromKeepAliveType(h.Type)
if kind == 0 {
return nil
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
Name: h.Name,
Kind: kind,
Static: h.Expires.IsZero(),
})
return nil
}
const (
serverHostnameMaxLen = 256
serverHostnameRegexPattern = `^[a-zA-Z0-9]+[a-zA-Z0-9\.-]*$`
replacedHostnameLabel = types.TeleportInternalLabelPrefix + "invalid-hostname"
)
var serverHostnameRegex = regexp.MustCompile(serverHostnameRegexPattern)
// validServerHostname returns false if the hostname is longer than 256 characters or
// does not entirely consist of alphanumeric characters as well as '-' and '.'. A valid hostname also
// cannot begin with a symbol.
func validServerHostname(hostname string) bool {
return len(hostname) <= serverHostnameMaxLen && serverHostnameRegex.MatchString(hostname)
}
func sanitizeHostname(server types.Server) error {
invalidHostname := server.GetHostname()
replacedHostname := server.GetName()
if server.GetSubKind() == types.SubKindOpenSSHNode {
host, _, err := net.SplitHostPort(server.GetAddr())
if err != nil || !validServerHostname(host) {
id, err := uuid.NewRandom()
if err != nil {
return trace.Wrap(err)
}
host = id.String()
}
replacedHostname = host
}
switch s := server.(type) {
case *types.ServerV2:
s.Spec.Hostname = replacedHostname
if s.Metadata.Labels == nil {
s.Metadata.Labels = map[string]string{}
}
s.Metadata.Labels[replacedHostnameLabel] = invalidHostname
default:
return trace.BadParameter("invalid server provided")
}
return nil
}
// restoreSanitizedHostname restores the original hostname of a server and removes the label.
func restoreSanitizedHostname(server types.Server) error {
oldHostname, ok := server.GetLabels()[replacedHostnameLabel]
// if the label is not present or the hostname is invalid under the most recent rules, do nothing.
if !ok || !validServerHostname(oldHostname) {
return nil
}
switch s := server.(type) {
case *types.ServerV2:
// restore the original hostname and remove the label.
s.Spec.Hostname = oldHostname
delete(s.Metadata.Labels, replacedHostnameLabel)
default:
return trace.BadParameter("invalid server provided")
}
return nil
}
// UpsertNode implements [services.Presence] by delegating to [Server.Services]
// and potentially emitting a [usagereporter] event.
func (a *Server) UpsertNode(ctx context.Context, server types.Server) (*types.KeepAlive, error) {
if !validServerHostname(server.GetHostname()) {
a.logger.DebugContext(a.closeCtx, "sanitizing invalid server hostname",
"server", server.GetName(),
"hostname", server.GetHostname(),
)
if err := sanitizeHostname(server); err != nil {
return nil, trace.Wrap(err)
}
}
lease, err := a.Services.UpsertNode(ctx, server)
if err != nil {
return nil, trace.Wrap(err)
}
kind := usagereporter.ResourceKindNode
switch server.GetSubKind() {
case types.SubKindOpenSSHNode:
kind = usagereporter.ResourceKindNodeOpenSSH
case types.SubKindOpenSSHEICENode:
kind = usagereporter.ResourceKindNodeOpenSSHEICE
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
Name: server.GetName(),
Kind: kind,
Static: server.Expiry().IsZero(),
})
return lease, nil
}
// enforceLicense checks if the license allows the given resource type to be
// created.
func enforceLicense(t string) error {
switch t {
case types.KindKubeServer, types.KindKubernetesCluster:
if !modules.GetModules().Features().GetEntitlement(entitlements.K8s).Enabled {
return trace.AccessDenied(
"this Teleport cluster is not licensed for Kubernetes, please contact the cluster administrator")
}
}
return nil
}
// UpsertKubernetesServer implements [services.Presence] by delegating to
// [Server.Services] and then potentially emitting a [usagereporter] event.
func (a *Server) UpsertKubernetesServer(ctx context.Context, server types.KubeServer) (*types.KeepAlive, error) {
if err := enforceLicense(types.KindKubeServer); err != nil {
return nil, trace.Wrap(err)
}
k, err := a.Services.UpsertKubernetesServer(ctx, server)
if err != nil {
return nil, trace.Wrap(err)
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
// the name of types.KubeServer might include a -proxy_service suffix
Name: server.GetCluster().GetName(),
Kind: usagereporter.ResourceKindKubeServer,
Static: server.Expiry().IsZero(),
})
return k, nil
}
// UpsertApplicationServer implements [services.Presence] by delegating to
// [Server.Services] and then potentially emitting a [usagereporter] event.
func (a *Server) UpsertApplicationServer(ctx context.Context, server types.AppServer) (*types.KeepAlive, error) {
lease, err := a.Services.UpsertApplicationServer(ctx, server)
if err != nil {
return nil, trace.Wrap(err)
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
Name: server.GetName(),
Kind: usagereporter.ResourceKindAppServer,
Static: server.Expiry().IsZero(),
})
return lease, nil
}
// UpsertDatabaseServer implements [services.Presence] by delegating to
// [Server.Services] and then potentially emitting a [usagereporter] event.
func (a *Server) UpsertDatabaseServer(ctx context.Context, server types.DatabaseServer) (*types.KeepAlive, error) {
lease, err := a.Services.UpsertDatabaseServer(ctx, server)
if err != nil {
return nil, trace.Wrap(err)
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
Name: server.GetName(),
Kind: usagereporter.ResourceKindDBServer,
Static: server.Expiry().IsZero(),
})
return lease, nil
}
func (a *Server) DeleteWindowsDesktop(ctx context.Context, hostID, name string) error {
if err := a.Services.DeleteWindowsDesktop(ctx, hostID, name); err != nil {
return trace.Wrap(err)
}
if _, err := a.desktopsLimitExceeded(ctx); err != nil {
a.logger.WarnContext(ctx, "Can't check OSS non-AD desktops limit", "error", err)
}
return nil
}
// CreateWindowsDesktop implements [services.WindowsDesktops] by delegating to
// [Server.Services] and then potentially emitting a [usagereporter] event.
func (a *Server) CreateWindowsDesktop(ctx context.Context, desktop types.WindowsDesktop) error {
if err := a.Services.CreateWindowsDesktop(ctx, desktop); err != nil {
return trace.Wrap(err)
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
Name: desktop.GetName(),
Kind: usagereporter.ResourceKindWindowsDesktop,
Static: desktop.Expiry().IsZero(),
})
return nil
}
// UpdateWindowsDesktop implements [services.WindowsDesktops] by delegating to
// [Server.Services] and then potentially emitting a [usagereporter] event.
func (a *Server) UpdateWindowsDesktop(ctx context.Context, desktop types.WindowsDesktop) error {
if err := a.Services.UpdateWindowsDesktop(ctx, desktop); err != nil {
return trace.Wrap(err)
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
Name: desktop.GetName(),
Kind: usagereporter.ResourceKindWindowsDesktop,
Static: desktop.Expiry().IsZero(),
})
return nil
}
// UpsertWindowsDesktop implements [services.WindowsDesktops] by delegating to
// [Server.Services] and then potentially emitting a [usagereporter] event.
func (a *Server) UpsertWindowsDesktop(ctx context.Context, desktop types.WindowsDesktop) error {
if err := a.Services.UpsertWindowsDesktop(ctx, desktop); err != nil {
return trace.Wrap(err)
}
a.AnonymizeAndSubmit(&usagereporter.ResourceHeartbeatEvent{
Name: desktop.GetName(),
Kind: usagereporter.ResourceKindWindowsDesktop,
Static: desktop.Expiry().IsZero(),
})
return nil
}
func (a *Server) streamWindowsDesktops(ctx context.Context, req types.ListWindowsDesktopsRequest) stream.Stream[types.WindowsDesktop] {
var done bool
return stream.PageFunc(func() ([]types.WindowsDesktop, error) {
if done {
return nil, io.EOF
}
resp, err := a.ListWindowsDesktops(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
req.StartKey = resp.NextKey
done = req.StartKey == ""
return resp.Desktops, nil
})
}
func (a *Server) syncDesktopsLimitAlert(ctx context.Context) {
exceeded, err := a.desktopsLimitExceeded(ctx)
if err != nil {
a.logger.WarnContext(ctx, "Can't check OSS non-AD desktops limit", "error", err)
}
if !exceeded {
return
}
alert, err := types.NewClusterAlert(OSSDesktopsAlertID, OSSDesktopsAlertMessage,
types.WithAlertSeverity(types.AlertSeverity_MEDIUM),
types.WithAlertLabel(types.AlertOnLogin, "yes"),
types.WithAlertLabel(types.AlertPermitAll, "yes"),
types.WithAlertLabel(types.AlertLink, OSSDesktopsAlertLink),
types.WithAlertLabel(types.AlertLinkText, OSSDesktopsAlertLinkText),
types.WithAlertExpires(time.Now().Add(OSSDesktopsCheckPeriod)))
if err != nil {
a.logger.WarnContext(ctx, "Can't create OSS non-AD desktops limit alert", "error", err)
}
if err := a.UpsertClusterAlert(ctx, alert); err != nil {
a.logger.WarnContext(ctx, "Can't upsert OSS non-AD desktops limit alert", "error", err)
}
}
// desktopsLimitExceeded checks if number of non-AD desktops exceeds limit for OSS distribution. Returns always false for Enterprise.
func (a *Server) desktopsLimitExceeded(ctx context.Context) (bool, error) {
if modules.GetModules().IsEnterpriseBuild() {
return false, nil
}
desktops := stream.FilterMap(
a.streamWindowsDesktops(ctx, types.ListWindowsDesktopsRequest{Limit: 50}),
func(d types.WindowsDesktop) (struct{}, bool) {
return struct{}{}, d.NonAD()
},
)
count := 0
for desktops.Next() {
count++
if count > OSSDesktopsLimit {
desktops.Done()
return true, nil
}
}
return false, trace.Wrap(desktops.Done())
}
func (a *Server) syncDynamicLabelsAlert(ctx context.Context) {
roles, err := a.GetRoles(ctx)
if err != nil {
a.logger.WarnContext(ctx, "Can't get roles", "error", err)
}
var rolesWithDynamicDenyLabels bool
for _, role := range roles {
err := services.CheckDynamicLabelsInDenyRules(role)
if trace.IsBadParameter(err) {
rolesWithDynamicDenyLabels = true
break
}
if err != nil {
a.logger.WarnContext(ctx, "Error checking labels in role",
"role", role.GetName(),
"error", err,
)
continue
}
}
if !rolesWithDynamicDenyLabels {
return
}
alert, err := types.NewClusterAlert(
dynamicLabelAlertID,
dynamicLabelAlertMessage,
types.WithAlertSeverity(types.AlertSeverity_MEDIUM),
types.WithAlertLabel(types.AlertVerbPermit, fmt.Sprintf("%s:%s", types.KindRole, types.VerbRead)),
)
if err != nil {
a.logger.WarnContext(ctx, "Failed to build dynamic-labels-in-deny-rules alert(this is a bug)", "error", err)
}
if err := a.UpsertClusterAlert(ctx, alert); err != nil {
a.logger.WarnContext(ctx, "Failed to set dynamic-labels-in-deny-rules alert", "error", err)
}
}
// CleanupNotifications deletes all expired user notifications and global notifications, as well as any associated notification states, for all users.
func (a *Server) CleanupNotifications(ctx context.Context) {
var userNotifications []*notificationsv1.Notification
var userNotificationsPageKey string
userNotificationsReadLimiter := time.NewTicker(notificationsPageReadInterval)
defer userNotificationsReadLimiter.Stop()
for {
select {
case <-userNotificationsReadLimiter.C:
case <-ctx.Done():
return
}
response, nextKey, err := a.Cache.ListUserNotifications(ctx, 20, userNotificationsPageKey)
if err != nil {
a.logger.WarnContext(ctx, "failed to list user notifications for periodic cleanup", "error", err)
}
userNotifications = append(userNotifications, response...)
if nextKey == "" {
break
}
userNotificationsPageKey = nextKey
}
var globalNotifications []*notificationsv1.GlobalNotification
var globalNotificationsPageKey string
globalNotificationsReadLimiter := time.NewTicker(notificationsPageReadInterval)
defer globalNotificationsReadLimiter.Stop()
for {
select {
case <-globalNotificationsReadLimiter.C:
case <-ctx.Done():
return
}
response, nextKey, err := a.Cache.ListGlobalNotifications(ctx, 20, globalNotificationsPageKey)
if err != nil {
a.logger.WarnContext(ctx, "failed to list global notifications for periodic cleanup", "error", err)
}
globalNotifications = append(globalNotifications, response...)
if nextKey == "" {
break
}
globalNotificationsPageKey = nextKey
}
timeNow := a.clock.Now()
notificationsDeleteLimiter := time.NewTicker(notificationsWriteInterval)
defer notificationsDeleteLimiter.Stop()
// Initialize a map for non-expired notifications where the key is the notification id.
nonExpiredGlobalNotificationsByID := make(map[string]*notificationsv1.GlobalNotification)
for _, gn := range globalNotifications {
notificationID := gn.GetMetadata().GetName()
expiry := gn.GetSpec().GetNotification().GetMetadata().GetExpires()
if timeNow.After(expiry.AsTime()) {
select {
case <-notificationsDeleteLimiter.C:
case <-ctx.Done():
return
}
if err := a.DeleteGlobalNotification(ctx, notificationID); err != nil && !trace.IsNotFound(err) {
a.logger.WarnContext(ctx, "encountered error attempting to cleanup global notification", "error", err, "notification_id", notificationID)
}
} else {
nonExpiredGlobalNotificationsByID[notificationID] = gn
}
}
// Initialize a map for non-expired notifications where the key is the notification id.
nonExpiredUserNotificationsByID := make(map[string]*notificationsv1.Notification)
for _, un := range userNotifications {
notificationID := un.GetMetadata().GetName()
user := un.GetSpec().GetUsername()
expiry := un.GetMetadata().GetExpires()
if timeNow.After(expiry.AsTime()) {
select {
case <-notificationsDeleteLimiter.C:
case <-ctx.Done():
return
}
if err := a.DeleteUserNotification(ctx, user, notificationID); err != nil && !trace.IsNotFound(err) {
a.logger.WarnContext(ctx, "encountered error attempting to cleanup user notification", "error", err, "notification_id", notificationID, "target_user", user)
}
} else {
nonExpiredUserNotificationsByID[notificationID] = un
}
}
var userNotificationStates []*notificationsv1.UserNotificationState
var userNotificationStatesPageKey string
notificationStatesTicker := time.NewTicker(notificationsPageReadInterval)
defer notificationStatesTicker.Stop()
for {
select {
case <-notificationStatesTicker.C:
case <-ctx.Done():
return
}
response, nextKey, err := a.ListNotificationStatesForAllUsers(ctx, 20, userNotificationStatesPageKey)
if err != nil {
a.logger.WarnContext(ctx, "encountered error attempting to list notification states for cleanup", "error", err)
}
userNotificationStates = append(userNotificationStates, response...)
if nextKey == "" {
break
}
userNotificationStatesPageKey = nextKey
}
for _, uns := range userNotificationStates {
id := uns.GetSpec().GetNotificationId()
username := uns.GetSpec().GetUsername()
// If this notification state is for a notification which doesn't exist in either the non-expired global notifications map or
// the non-expired user notifications map, then delete it.
if nonExpiredGlobalNotificationsByID[id] == nil && nonExpiredUserNotificationsByID[id] == nil {
select {
case <-notificationsDeleteLimiter.C:
case <-ctx.Done():
return
}
if err := a.DeleteUserNotificationState(ctx, username, id); err != nil {
a.logger.WarnContext(ctx, "encountered error attempting to cleanup notification state", "error", err, "user", username, "id", id)
}
}
}
}
const (
accessListReminderSemaphoreName = "access-list-reminder-check"
accessListReminderSemaphoreMaxLeases = 1
)
// CreateAccessListReminderNotifications checks if there are any access lists expiring soon and creates notifications to remind their owners if so.
func (a *Server) CreateAccessListReminderNotifications(ctx context.Context) {
// Ensure only one auth server is running this check at a time.
lease, err := services.AcquireSemaphoreLock(ctx, services.SemaphoreLockConfig{
Service: a,
Clock: a.clock,
Expiry: 5 * time.Minute,
Params: types.AcquireSemaphoreRequest{
SemaphoreKind: types.SemaphoreKindAccessListReminderLimiter,
SemaphoreName: accessListReminderSemaphoreName,
MaxLeases: accessListReminderSemaphoreMaxLeases,
Holder: a.ServerID,
},
})
if err != nil {
a.logger.WarnContext(ctx, "unable to acquire semaphore, will skip this access list reminder check", "server_id", a.ServerID)
return
}
defer func() {
lease.Stop()
if err := lease.Wait(); err != nil {
a.logger.WarnContext(ctx, "error cleaning up semaphore", "error", err)
}
}()
now := a.clock.Now()
// TODO(kiosion): Check possible Okta plugin configuration if Bidirectional Sync is explicitly disabled,
// we shouldn't create notifications for any Okta-synced Access Lists, as they are managed in Okta and
// their grants, owners/members, and ownership/membership requirements cannot be edited in Teleport.
// Fetch all access lists
var accessLists []*accesslist.AccessList
var accessListsPageKey string
accessListsReadLimiter := time.NewTicker(accessListsPageReadInterval)
defer accessListsReadLimiter.Stop()
for {
select {
case <-accessListsReadLimiter.C:
case <-ctx.Done():
return
}
response, nextKey, err := a.Cache.ListAccessLists(ctx, 20, accessListsPageKey)
if err != nil {
a.logger.WarnContext(ctx, "failed to list access lists for periodic reminder notification check", "error", err)
}
for _, al := range response {
if !al.IsReviewable() {
continue
}
daysDiff := int(al.Spec.Audit.NextAuditDate.Sub(now).Hours() / 24)
// Only keep access lists that fall within our thresholds in memory
if daysDiff <= 15 {
accessLists = append(accessLists, al)
}
}
if nextKey == "" {
break
}
accessListsPageKey = nextKey
}
reminderThresholds := []struct {
days int
prefix string
notificationSubkind string
}{
{14, types.NotificationIdentifierPrefixAccessListDueReminder14d, types.NotificationAccessListReviewDue14dSubKind},
{7, types.NotificationIdentifierPrefixAccessListDueReminder7d, types.NotificationAccessListReviewDue7dSubKind},
{3, types.NotificationIdentifierPrefixAccessListDueReminder3d, types.NotificationAccessListReviewDue3dSubKind},
{0, types.NotificationIdentifierPrefixAccessListDueReminder0d, types.NotificationAccessListReviewDue0dSubKind},
{-3, types.NotificationIdentifierPrefixAccessListOverdue3d, types.NotificationAccessListReviewOverdue3dSubKind},
{-7, types.NotificationIdentifierPrefixAccessListOverdue7d, types.NotificationAccessListReviewOverdue7dSubKind},
}
for _, threshold := range reminderThresholds {
var relevantLists []*accesslist.AccessList
// Filter access lists based on due date
for _, al := range accessLists {
dueDate := al.Spec.Audit.NextAuditDate
timeDiff := dueDate.Sub(now)
daysDiff := int(timeDiff.Hours() / 24)
if threshold.days < 0 {
if daysDiff <= threshold.days {
relevantLists = append(relevantLists, al)
}
} else {
if daysDiff >= 0 && daysDiff <= threshold.days {
relevantLists = append(relevantLists, al)
}
}
}
if len(relevantLists) == 0 {
continue
}
// Fetch all identifiers for this treshold prefix.
var identifiers []*notificationsv1.UniqueNotificationIdentifier
var nextKey string
for {
identifiersResp, nextKey, err := a.ListUniqueNotificationIdentifiersForPrefix(ctx, threshold.prefix, 0, nextKey)
if err != nil {
a.logger.WarnContext(ctx, "failed to list notification identifiers", "error", err, "prefix", threshold.prefix)
continue
}
identifiers = append(identifiers, identifiersResp...)
if nextKey == "" {
break
}
}
accessListIDs := set.New[string]()
for _, id := range identifiers {
// id.Spec.UniqueIdentifier is the access list ID
accessListIDs.Add(id.Spec.UniqueIdentifier)
}
// owners is the combined list of owners for relevant access lists we are creating the notification for.
var owners []string
// Check for access lists which haven't already been accounted for in a notification
var needsNotification bool
writeLimiter := time.NewTicker(notificationsWriteInterval)
for _, accessList := range relevantLists {
select {
case <-writeLimiter.C:
case <-ctx.Done():
return
}
if !accessListIDs.Contains(accessList.GetName()) {
needsNotification = true
// Create a unique identifier for this access list so that we know it has been accounted for.
// Note that if the auth server crashes between creating this identifier and creating the notification,
// the notification will be missed. This has been judged as an acceptable outcome for access lists,
// but the same strategy may not be acceptable for other notification types.
if _, err := a.CreateUniqueNotificationIdentifier(ctx, threshold.prefix, accessList.GetName()); err != nil {
a.logger.WarnContext(ctx, "failed to create notification identifier", "error", err, "access_list", accessList.GetName())
continue
}
for _, owner := range accessList.Spec.Owners {
owners = append(owners, owner.Name)
}
}
}
writeLimiter.Stop()
owners = apiutils.Deduplicate(owners)
var title string
if threshold.days == 0 {
title = "You have access lists due for review today."
} else if threshold.days < 0 {
title = fmt.Sprintf("You have access lists that are more than %d days overdue for review", -threshold.days)
} else {
title = fmt.Sprintf("You have access lists due for review in less than %d days.", threshold.days)
}
// Create the notification for this reminder treshold for all relevant owners.
if needsNotification {
err := a.createAccessListReminderNotification(ctx, owners, threshold.notificationSubkind, title)
if err != nil {
a.logger.WarnContext(ctx, "Failed to create access list reminder notification", "error", err)
}
}
}
}
// createAccessListReminderNotification is a helper function to create a notification for an access list reminder.
func (a *Server) createAccessListReminderNotification(ctx context.Context, owners []string, subkind string, title string) error {
_, err := a.Services.CreateGlobalNotification(ctx, &notificationsv1.GlobalNotification{
Spec: &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByUsers{
ByUsers: &notificationsv1.ByUsers{
Users: owners,
},
},
Notification: &notificationsv1.Notification{
Spec: &notificationsv1.NotificationSpec{},
SubKind: subkind,
Metadata: &headerv1.Metadata{
Labels: map[string]string{types.NotificationTitleLabel: title},
},
},
},
})
if err != nil {
return err
}
// Also create a notification for users who have CRUD permissions for access lists. This is because they can also review access lists.
_, err = a.Services.CreateGlobalNotification(ctx, &notificationsv1.GlobalNotification{
Spec: &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByPermissions{
ByPermissions: &notificationsv1.ByPermissions{
RoleConditions: []*types.RoleConditions{
{
Rules: []types.Rule{
{
Resources: []string{types.KindAccessList},
Verbs: services.RW(),
},
},
},
},
},
},
// Exclude the list of owners so that they don't get a duplicate notification, since we already created a notification for them.
ExcludeUsers: owners,
Notification: &notificationsv1.Notification{
Spec: &notificationsv1.NotificationSpec{},
SubKind: subkind,
Metadata: &headerv1.Metadata{
Labels: map[string]string{types.NotificationTitleLabel: title},
},
},
},
})
if err != nil {
return err
}
return nil
}
// GenerateCertAuthorityCRL generates an empty CRL for the local CA of a given type.
//
// WARNING: This is not safe for use in clusters using HSMs or KMS for private key material.
// Instead, you should prefer using the CRLs that are already present in the certificate_authority resource.
func (a *Server) GenerateCertAuthorityCRL(ctx context.Context, caType types.CertAuthType) ([]byte, error) {
// Generate a CRL for the current cluster CA.
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
ca, err := a.GetCertAuthority(ctx, types.CertAuthID{
Type: caType,
DomainName: clusterName.GetClusterName(),
}, true)
if err != nil {
return nil, trace.Wrap(err)
}
// Note: this will only create a CRL for a single active signer.
// If there are multiple signers (HSMs), we won't have the full CRL coverage.
cert, signer, err := a.keyStore.GetTLSCertAndSigner(ctx, ca)
if trace.IsNotFound(err) {
// If there is no local TLS signer found in the host CA ActiveKeys, this
// auth server may have a newly configured HSM and has only populated
// local keys in the AdditionalTrustedKeys until the next CA rotation.
// This is the only case where we should be able to get a signer from
// AdditionalTrustedKeys but not ActiveKeys.
cert, signer, err = a.keyStore.GetAdditionalTrustedTLSCertAndSigner(ctx, ca)
}
if err != nil {
return nil, trace.Wrap(err)
}
tlsAuthority, err := tlsca.FromCertAndSigner(cert, signer)
if err != nil {
return nil, trace.Wrap(err)
}
crl, err := keystore.GenerateCRL(tlsAuthority.Cert, tlsAuthority.Signer)
if err != nil {
return nil, trace.Wrap(err)
}
return crl, nil
}
// ErrDone indicates that resource iteration is complete
var ErrDone = errors.New("done iterating")
// IterateResources loads all resources matching the provided request and passes them one by one to the provided
// callback function. To stop iteration callers may return ErrDone from the callback function, which will result in
// a nil return from IterateResources. Any other errors returned from the callback function cause iteration to stop
// and the error to be returned.
func (a *Server) IterateResources(ctx context.Context, req proto.ListResourcesRequest, f func(resource types.ResourceWithLabels) error) error {
for {
resp, err := a.ListResources(ctx, req)
if err != nil {
return trace.Wrap(err)
}
for _, resource := range resp.Resources {
if err := f(resource); err != nil {
if errors.Is(err, ErrDone) {
return nil
}
return trace.Wrap(err)
}
}
if resp.NextKey == "" {
return nil
}
req.StartKey = resp.NextKey
}
}
// CreateApp creates a new application resource.
func (a *Server) CreateApp(ctx context.Context, app types.Application) error {
if err := a.Services.CreateApp(ctx, app); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.AppCreate{
Metadata: apievents.Metadata{
Type: events.AppCreateEvent,
Code: events.AppCreateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: app.GetName(),
Expires: app.Expiry(),
},
AppMetadata: apievents.AppMetadata{
AppURI: app.GetURI(),
AppPublicAddr: app.GetPublicAddr(),
AppLabels: app.GetStaticLabels(),
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit app create event", "error", err)
}
return nil
}
// UpdateApp updates an existing application resource.
func (a *Server) UpdateApp(ctx context.Context, app types.Application) error {
if err := a.Services.UpdateApp(ctx, app); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.AppUpdate{
Metadata: apievents.Metadata{
Type: events.AppUpdateEvent,
Code: events.AppUpdateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: app.GetName(),
Expires: app.Expiry(),
},
AppMetadata: apievents.AppMetadata{
AppURI: app.GetURI(),
AppPublicAddr: app.GetPublicAddr(),
AppLabels: app.GetStaticLabels(),
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit app update event", "error", err)
}
return nil
}
// DeleteApp deletes an application resource.
func (a *Server) DeleteApp(ctx context.Context, name string) error {
if err := a.Services.DeleteApp(ctx, name); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.AppDelete{
Metadata: apievents.Metadata{
Type: events.AppDeleteEvent,
Code: events.AppDeleteCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: name,
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit app delete event", "error", err)
}
return nil
}
// CreateSessionTracker creates a tracker resource for an active session.
func (a *Server) CreateSessionTracker(ctx context.Context, tracker types.SessionTracker) (types.SessionTracker, error) {
// Don't allow sessions that require moderation without the enterprise feature enabled.
for _, policySet := range tracker.GetHostPolicySets() {
if len(policySet.RequireSessionJoin) != 0 {
if modules.GetModules().BuildType() != modules.BuildEnterprise {
return nil, fmt.Errorf("moderated sessions: %w", ErrRequiresEnterprise)
}
}
}
return a.Services.CreateSessionTracker(ctx, tracker)
}
// CreateDatabase creates a new database resource.
func (a *Server) CreateDatabase(ctx context.Context, database types.Database) error {
if err := a.Services.CreateDatabase(ctx, database); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.DatabaseCreate{
Metadata: apievents.Metadata{
Type: events.DatabaseCreateEvent,
Code: events.DatabaseCreateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: database.GetName(),
Expires: database.Expiry(),
},
DatabaseMetadata: apievents.DatabaseMetadata{
DatabaseProtocol: database.GetProtocol(),
DatabaseURI: database.GetURI(),
DatabaseLabels: database.GetStaticLabels(),
DatabaseAWSRegion: database.GetAWS().Region,
DatabaseAWSRedshiftClusterID: database.GetAWS().Redshift.ClusterID,
DatabaseGCPProjectID: database.GetGCP().ProjectID,
DatabaseGCPInstanceID: database.GetGCP().InstanceID,
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit database create event", "error", err)
}
return nil
}
// UpdateDatabase updates an existing database resource.
func (a *Server) UpdateDatabase(ctx context.Context, database types.Database) error {
if err := a.Services.UpdateDatabase(ctx, database); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.DatabaseUpdate{
Metadata: apievents.Metadata{
Type: events.DatabaseUpdateEvent,
Code: events.DatabaseUpdateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: database.GetName(),
Expires: database.Expiry(),
},
DatabaseMetadata: apievents.DatabaseMetadata{
DatabaseProtocol: database.GetProtocol(),
DatabaseURI: database.GetURI(),
DatabaseLabels: database.GetStaticLabels(),
DatabaseAWSRegion: database.GetAWS().Region,
DatabaseAWSRedshiftClusterID: database.GetAWS().Redshift.ClusterID,
DatabaseGCPProjectID: database.GetGCP().ProjectID,
DatabaseGCPInstanceID: database.GetGCP().InstanceID,
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit database update event", "error", err)
}
return nil
}
// DeleteDatabase deletes a database resource.
func (a *Server) DeleteDatabase(ctx context.Context, name string) error {
if err := a.Services.DeleteDatabase(ctx, name); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.DatabaseDelete{
Metadata: apievents.Metadata{
Type: events.DatabaseDeleteEvent,
Code: events.DatabaseDeleteCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: name,
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit database delete event", "error", err)
}
return nil
}
// ListResources returns paginated resources depending on the resource type..
func (a *Server) ListResources(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
// Because WindowsDesktopService does not contain the desktop resources,
// this is not implemented at the cache level and requires the workaround
// here in order to support KindWindowsDesktop for ListResources.
if req.ResourceType == types.KindWindowsDesktop {
wResp, err := a.ListWindowsDesktops(ctx, types.ListWindowsDesktopsRequest{
WindowsDesktopFilter: req.WindowsDesktopFilter,
Limit: int(req.Limit),
StartKey: req.StartKey,
PredicateExpression: req.PredicateExpression,
Labels: req.Labels,
SearchKeywords: req.SearchKeywords,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &types.ListResourcesResponse{
Resources: types.WindowsDesktops(wResp.Desktops).AsResources(),
NextKey: wResp.NextKey,
}, nil
}
if req.ResourceType == types.KindWindowsDesktopService {
wResp, err := a.ListWindowsDesktopServices(ctx, types.ListWindowsDesktopServicesRequest{
Limit: int(req.Limit),
StartKey: req.StartKey,
PredicateExpression: req.PredicateExpression,
Labels: req.Labels,
SearchKeywords: req.SearchKeywords,
})
if err != nil {
return nil, trace.Wrap(err)
}
return &types.ListResourcesResponse{
Resources: types.WindowsDesktopServices(wResp.DesktopServices).AsResources(),
NextKey: wResp.NextKey,
}, nil
}
return a.Cache.ListResources(ctx, req)
}
// CreateKubernetesCluster creates a new kubernetes cluster resource.
func (a *Server) CreateKubernetesCluster(ctx context.Context, kubeCluster types.KubeCluster) error {
if err := enforceLicense(types.KindKubernetesCluster); err != nil {
return trace.Wrap(err)
}
if err := a.Services.CreateKubernetesCluster(ctx, kubeCluster); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.KubernetesClusterCreate{
Metadata: apievents.Metadata{
Type: events.KubernetesClusterCreateEvent,
Code: events.KubernetesClusterCreateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: kubeCluster.GetName(),
Expires: kubeCluster.Expiry(),
},
KubeClusterMetadata: apievents.KubeClusterMetadata{
KubeLabels: kubeCluster.GetStaticLabels(),
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit kube cluster create event", "error", err)
}
return nil
}
// UpdateKubernetesCluster updates an existing kubernetes cluster resource.
func (a *Server) UpdateKubernetesCluster(ctx context.Context, kubeCluster types.KubeCluster) error {
if err := enforceLicense(types.KindKubernetesCluster); err != nil {
return trace.Wrap(err)
}
if err := a.Kubernetes.UpdateKubernetesCluster(ctx, kubeCluster); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.KubernetesClusterUpdate{
Metadata: apievents.Metadata{
Type: events.KubernetesClusterUpdateEvent,
Code: events.KubernetesClusterUpdateCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: kubeCluster.GetName(),
Expires: kubeCluster.Expiry(),
},
KubeClusterMetadata: apievents.KubeClusterMetadata{
KubeLabels: kubeCluster.GetStaticLabels(),
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit kube cluster update event", "error", err)
}
return nil
}
// DeleteKubernetesCluster deletes a kubernetes cluster resource.
func (a *Server) DeleteKubernetesCluster(ctx context.Context, name string) error {
if err := a.Kubernetes.DeleteKubernetesCluster(ctx, name); err != nil {
return trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.KubernetesClusterDelete{
Metadata: apievents.Metadata{
Type: events.KubernetesClusterDeleteEvent,
Code: events.KubernetesClusterDeleteCode,
},
UserMetadata: authz.ClientUserMetadata(ctx),
ResourceMetadata: apievents.ResourceMetadata{
Name: name,
},
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit kube cluster delete event", "error", err)
}
return nil
}
// SubmitUsageEvent submits an external usage event.
func (a *Server) SubmitUsageEvent(ctx context.Context, req *proto.SubmitUsageEventRequest) error {
username, err := authz.GetClientUsername(ctx)
if err != nil {
return trace.Wrap(err)
}
userIsSSO, err := authz.GetClientUserIsSSO(ctx)
if err != nil {
return trace.Wrap(err)
}
userMetadata := usagereporter.UserMetadata{
Username: username,
IsSSO: userIsSSO,
}
event, err := usagereporter.ConvertUsageEvent(req.GetEvent(), userMetadata)
if err != nil {
return trace.Wrap(err)
}
a.AnonymizeAndSubmit(event)
return nil
}
// Ping gets basic info about the auth server.
// Please note that Ping is publicly accessible (not protected by any RBAC) by design,
// and thus PingResponse must never contain any sensitive information.
func (a *Server) Ping(ctx context.Context) (proto.PingResponse, error) {
cn, err := a.GetClusterName(ctx)
if err != nil {
return proto.PingResponse{}, trace.Wrap(err)
}
features := modules.GetModules().Features().ToProto()
authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return proto.PingResponse{}, nil
}
licenseExpiry := modules.GetModules().LicenseExpiry()
return proto.PingResponse{
ClusterName: cn.GetClusterName(),
ServerVersion: teleport.Version,
ServerFeatures: features,
ProxyPublicAddr: a.getProxyPublicAddr(),
IsBoring: modules.GetModules().IsBoringBinary(),
LoadAllCAs: a.loadAllCAs,
SignatureAlgorithmSuite: authPref.GetSignatureAlgorithmSuite(),
LicenseExpiry: &licenseExpiry,
}, nil
}
type maintenanceWindowCacheKey struct {
key string
}
// agentWindowLookahead is the number of upgrade windows, starting from 'today', that we export
// when compiling agent upgrade schedules. The choice is arbitrary. We must export at least 2, because upgraders
// treat a schedule value whose windows all end in the past to be stale and therefore a sign that the agent is
// unhealthy. 3 was picked to give us some leeway in terms of how long an agent can be turned off before its
// upgrader starts complaining of a stale schedule.
const agentWindowLookahead = 3
// exportUpgradeWindowsCached generates the export value of all upgrade window schedule types. Since schedules
// are reloaded frequently in large clusters and export incurs string/json encoding, we use the ttl cache to store
// the encoded schedule values for a few seconds.
func (a *Server) exportUpgradeWindowsCached(ctx context.Context) (proto.ExportUpgradeWindowsResponse, error) {
return utils.FnCacheGet(ctx, a.ttlCache, maintenanceWindowCacheKey{"export"}, func(ctx context.Context) (proto.ExportUpgradeWindowsResponse, error) {
var rsp proto.ExportUpgradeWindowsResponse
cmc, err := a.GetClusterMaintenanceConfig(ctx)
if err != nil {
if trace.IsNotFound(err) {
// "not found" is treated as an empty schedule value
return rsp, nil
}
return rsp, trace.Wrap(err)
}
agentWindow, ok := cmc.GetAgentUpgradeWindow()
if !ok {
// "unconfigured" is treated as an empty schedule value
return rsp, nil
}
sched := agentWindow.Export(time.Now(), agentWindowLookahead)
rsp.CanonicalSchedule = &sched
rsp.KubeControllerSchedule, err = uw.EncodeKubeControllerSchedule(sched)
if err != nil {
a.logger.WarnContext(ctx, "Failed to encode kube controller maintenance schedule", "error", err)
}
rsp.SystemdUnitSchedule, err = uw.EncodeSystemdUnitSchedule(sched)
if err != nil {
a.logger.WarnContext(ctx, "Failed to encode systemd unit maintenance schedule", "error", err)
}
return rsp, nil
})
}
func (a *Server) ExportUpgradeWindows(ctx context.Context, req proto.ExportUpgradeWindowsRequest) (proto.ExportUpgradeWindowsResponse, error) {
var rsp proto.ExportUpgradeWindowsResponse
// get the cached collection of all export values
cached, err := a.exportUpgradeWindowsCached(ctx)
if err != nil {
return rsp, nil
}
switch req.UpgraderKind {
case "", types.UpgraderKindTeleportUpdate:
rsp.CanonicalSchedule = cached.CanonicalSchedule.Clone()
case types.UpgraderKindKubeController:
rsp.KubeControllerSchedule = cached.KubeControllerSchedule
if sched := os.Getenv("TELEPORT_UNSTABLE_KUBE_UPGRADE_SCHEDULE"); sched != "" {
rsp.KubeControllerSchedule = sched
}
case types.UpgraderKindSystemdUnit:
rsp.SystemdUnitSchedule = cached.SystemdUnitSchedule
if sched := os.Getenv("TELEPORT_UNSTABLE_SYSTEMD_UPGRADE_SCHEDULE"); sched != "" {
rsp.SystemdUnitSchedule = sched
}
default:
return rsp, trace.NotImplemented("unsupported upgrader kind %q in upgrade window export request", req.UpgraderKind)
}
return rsp, nil
}
// MFARequiredToBool translates a [proto.MFARequired] value to a simple
// "required bool".
func MFARequiredToBool(m proto.MFARequired) (required bool) {
switch m {
case proto.MFARequired_MFA_REQUIRED_NO:
return false
default: // _UNSPECIFIED or _YES are both treated as required.
return true
}
}
func (a *Server) isMFARequired(ctx context.Context, checker services.AccessChecker, req *proto.IsMFARequiredRequest) (resp *proto.IsMFARequiredResponse, err error) {
// Assign Required as a function of MFARequired.
defer func() {
if resp != nil {
resp.Required = MFARequiredToBool(resp.MFARequired)
}
}()
authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
switch state := checker.GetAccessState(authPref); state.MFARequired {
case services.MFARequiredAlways:
return &proto.IsMFARequiredResponse{
MFARequired: proto.MFARequired_MFA_REQUIRED_YES,
}, nil
case services.MFARequiredNever:
return &proto.IsMFARequiredResponse{
MFARequired: proto.MFARequired_MFA_REQUIRED_NO,
}, nil
}
var noMFAAccessErr error
switch t := req.Target.(type) {
case *proto.IsMFARequiredRequest_Node:
if t.Node.Node == "" {
return nil, trace.BadParameter("empty Node field")
}
if t.Node.Login == "" {
return nil, trace.BadParameter("empty Login field")
}
// state.MFARequired is "per-role", so if the user is joining
// a session, MFA is required no matter what node they are
// connecting to. We don't preform an RBAC check like we do
// below when users are starting a session to selectively
// require MFA because we don't know what session the user
// is joining, nor do we know what role allowed the session
// creator to start the session that is attempting to be joined.
// We need this info to be able to selectively skip MFA in
// this case.
if t.Node.Login == teleport.SSHSessionJoinPrincipal {
return &proto.IsMFARequiredResponse{
MFARequired: proto.MFARequired_MFA_REQUIRED_YES,
}, nil
}
// Find the target node and check whether MFA is required.
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)
}
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
// `tctl auth sign`. In these cases, let the user through without
// extra checks.
//
// If t.Node.Node turns out to be an alias for a real node (e.g.
// private network IP), and MFA check was actually required, the
// Node itself will check the cert extensions and reject the
// connection.
return &proto.IsMFARequiredResponse{
MFARequired: proto.MFARequired_MFA_REQUIRED_NO,
}, nil
}
// 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 {
srv, ok := n.(types.Server)
if !ok {
continue
}
// Filter out any matches on labels before checking access
fieldVals := append(srv.GetPublicAddrs(), srv.GetName(), srv.GetHostname(), srv.GetAddr())
if !types.MatchSearch(fieldVals, []string{t.Node.Node}, nil) {
continue
}
err = checker.CheckAccess(
n,
services.AccessState{},
services.NewLoginMatcher(t.Node.Login),
)
// Ignore other errors; they'll be caught on the real access attempt.
if err != nil && errors.Is(err, services.ErrSessionMFARequired) {
noMFAAccessErr = err
break
}
}
case *proto.IsMFARequiredRequest_KubernetesCluster:
if t.KubernetesCluster == "" {
return nil, trace.BadParameter("missing KubernetesCluster field in a kubernetes-only UserCertsRequest")
}
// Find the target cluster and check whether MFA is required.
svcs, err := a.GetKubernetesServers(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
var cluster types.KubeCluster
for _, svc := range svcs {
kubeCluster := svc.GetCluster()
if kubeCluster.GetName() == t.KubernetesCluster {
cluster = kubeCluster
break
}
}
if cluster == nil {
return nil, trace.NotFound("kubernetes cluster %q not found", t.KubernetesCluster)
}
noMFAAccessErr = checker.CheckAccess(cluster, services.AccessState{})
case *proto.IsMFARequiredRequest_Database:
if t.Database.ServiceName == "" {
return nil, trace.BadParameter("missing ServiceName field in a database-only UserCertsRequest")
}
servers, err := a.GetDatabaseServers(ctx, apidefaults.Namespace)
if err != nil {
return nil, trace.Wrap(err)
}
var db types.Database
for _, server := range servers {
if server.GetDatabase().GetName() == t.Database.ServiceName {
db = server.GetDatabase()
break
}
}
if db == nil {
return nil, trace.NotFound("database service %q not found", t.Database.ServiceName)
}
autoCreate, err := checker.DatabaseAutoUserMode(db)
switch {
case errors.Is(err, services.ErrSessionMFARequired):
noMFAAccessErr = err
case err != nil:
return nil, trace.Wrap(err)
default:
dbRoleMatchers := role.GetDatabaseRoleMatchers(role.RoleMatchersConfig{
Database: db,
DatabaseUser: t.Database.Username,
DatabaseName: t.Database.GetDatabase(),
AutoCreateUser: autoCreate.IsEnabled(),
})
noMFAAccessErr = checker.CheckAccess(
db,
services.AccessState{},
dbRoleMatchers...,
)
}
case *proto.IsMFARequiredRequest_WindowsDesktop:
desktops, err := a.GetWindowsDesktops(ctx, types.WindowsDesktopFilter{Name: t.WindowsDesktop.GetWindowsDesktop()})
if err != nil {
return nil, trace.Wrap(err)
}
if len(desktops) == 0 {
return nil, trace.NotFound("windows desktop %q not found", t.WindowsDesktop.GetWindowsDesktop())
}
noMFAAccessErr = checker.CheckAccess(desktops[0],
services.AccessState{},
services.NewWindowsLoginMatcher(t.WindowsDesktop.GetLogin()))
case *proto.IsMFARequiredRequest_App:
if t.App.Name == "" {
return nil, trace.BadParameter("missing Name field in an app-only UserCertsRequest")
}
servers, err := a.GetApplicationServers(ctx, apidefaults.Namespace)
if err != nil {
return nil, trace.Wrap(err)
}
i := slices.IndexFunc(servers, func(server types.AppServer) bool {
return server.GetApp().GetName() == t.App.Name
})
if i == -1 {
return nil, trace.NotFound("application service %q not found", t.App.Name)
}
app := servers[i].GetApp()
noMFAAccessErr = checker.CheckAccess(app, services.AccessState{})
default:
return nil, trace.BadParameter("unknown Target %T", req.Target)
}
// No error means that MFA is not required for this resource by
// AccessChecker.
if noMFAAccessErr == nil {
return &proto.IsMFARequiredResponse{
MFARequired: proto.MFARequired_MFA_REQUIRED_NO,
}, nil
}
// Errors other than ErrSessionMFARequired mean something else is wrong,
// most likely access denied.
if !errors.Is(noMFAAccessErr, services.ErrSessionMFARequired) {
if !trace.IsAccessDenied(noMFAAccessErr) {
a.logger.WarnContext(ctx, "Could not determine MFA access", "error", noMFAAccessErr)
}
// Mask the access denied errors by returning false to prevent resource
// name oracles. Auth will be denied (and generate an audit log entry)
// when the client attempts to connect.
return &proto.IsMFARequiredResponse{
MFARequired: proto.MFARequired_MFA_REQUIRED_NO,
}, nil
}
// If we reach here, the error from AccessChecker was
// ErrSessionMFARequired.
return &proto.IsMFARequiredResponse{
MFARequired: proto.MFARequired_MFA_REQUIRED_YES,
}, nil
}
// mfaAuthChallenge constructs an MFAAuthenticateChallenge for all MFA devices
// registered by the user.
func (a *Server) mfaAuthChallenge(ctx context.Context, user, ssoClientRedirectURL, proxyAddress string, challengeExtensions *mfav1.ChallengeExtensions) (*proto.MFAAuthenticateChallenge, error) {
isPasswordless := challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN
// Check what kind of MFA is enabled.
apref, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
enableTOTP := apref.IsSecondFactorTOTPAllowed()
enableWebauthn := apref.IsSecondFactorWebauthnAllowed()
enableSSO := apref.IsSecondFactorSSOAllowed()
// Fetch configurations. The IsSecondFactor*Allowed calls above already
// include the necessary checks of config empty, disabled, etc.
var u2fPref *types.U2F
switch val, err := apref.GetU2F(); {
case trace.IsNotFound(err): // OK, may happen.
case err != nil: // NOK, unexpected.
return nil, trace.Wrap(err)
default:
u2fPref = val
}
var webConfig *types.Webauthn
switch val, err := apref.GetWebauthn(); {
case trace.IsNotFound(err): // OK, may happen.
case err != nil: // NOK, unexpected.
return nil, trace.Wrap(err)
default:
webConfig = val
}
// Handle passwordless separately, it works differently from MFA.
if isPasswordless {
if !enableWebauthn {
return nil, trace.Wrap(types.ErrPasswordlessRequiresWebauthn)
}
if !apref.GetAllowPasswordless() {
return nil, trace.Wrap(types.ErrPasswordlessDisabledBySettings)
}
webLogin := &wanlib.PasswordlessFlow{
Webauthn: webConfig,
Identity: a.Services,
}
assertion, err := webLogin.Begin(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.CreateMFAAuthChallenge{
Metadata: apievents.Metadata{
Type: events.CreateMFAAuthChallengeEvent,
Code: events.CreateMFAAuthChallengeCode,
ClusterName: clusterName.GetClusterName(),
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, user),
ChallengeScope: challengeExtensions.Scope.String(),
ChallengeAllowReuse: challengeExtensions.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES,
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit CreateMFAAuthChallenge event", "error", err)
}
return &proto.MFAAuthenticateChallenge{
WebauthnChallenge: wantypes.CredentialAssertionToProto(assertion),
}, nil
}
// User required for non-passwordless.
if user == "" {
return nil, trace.BadParameter("user required")
}
devs, err := a.Services.GetMFADevices(ctx, user, true /* withSecrets */)
if err != nil {
return nil, trace.Wrap(err)
}
groupedDevs := groupByDeviceType(devs)
challenge := &proto.MFAAuthenticateChallenge{}
// TOTP challenge.
if enableTOTP && groupedDevs.TOTP {
challenge.TOTP = &proto.TOTPChallenge{}
}
// WebAuthn challenge.
if enableWebauthn && len(groupedDevs.Webauthn) > 0 {
webLogin := &wanlib.LoginFlow{
U2F: u2fPref,
Webauthn: webConfig,
Identity: wanlib.WithDevices(a.Services, groupedDevs.Webauthn),
}
assertion, err := webLogin.Begin(ctx, user, challengeExtensions)
if err != nil {
return nil, trace.Wrap(err)
}
challenge.WebauthnChallenge = wantypes.CredentialAssertionToProto(assertion)
}
// If the user has an SSO device and the client provided a redirect URL to handle
// the MFA SSO flow, create an SSO challenge.
if enableSSO && groupedDevs.SSO != nil && ssoClientRedirectURL != "" {
if challenge.SSOChallenge, err = a.beginSSOMFAChallenge(ctx, user, groupedDevs.SSO.GetSso(), ssoClientRedirectURL, proxyAddress, challengeExtensions); err != nil {
return nil, trace.Wrap(err)
}
}
clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
if err := a.emitter.EmitAuditEvent(ctx, &apievents.CreateMFAAuthChallenge{
Metadata: apievents.Metadata{
Type: events.CreateMFAAuthChallengeEvent,
Code: events.CreateMFAAuthChallengeCode,
ClusterName: clusterName.GetClusterName(),
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, user),
ChallengeScope: challengeExtensions.Scope.String(),
ChallengeAllowReuse: challengeExtensions.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES,
}); err != nil {
a.logger.WarnContext(ctx, "Failed to emit CreateMFAAuthChallenge event", "error", err)
}
return challenge, nil
}
type devicesByType struct {
TOTP bool
Webauthn []*types.MFADevice
SSO *types.MFADevice
}
func groupByDeviceType(devs []*types.MFADevice) devicesByType {
res := devicesByType{}
for _, dev := range devs {
switch dev.Device.(type) {
case *types.MFADevice_Totp:
res.TOTP = true
case *types.MFADevice_U2F:
res.Webauthn = append(res.Webauthn, dev)
case *types.MFADevice_Webauthn:
res.Webauthn = append(res.Webauthn, dev)
case *types.MFADevice_Sso:
res.SSO = dev
default:
logger.WarnContext(context.Background(), "Skipping MFA device with unknown type", "device_type", logutils.TypeAttr(dev.Device))
}
}
return res
}
// validateMFAAuthResponseForRegister is akin to [validateMFAAuthResponse], but
// it allows users with no devices to supply a nil/empty response.
//
// The hasDevices response value can only be trusted in the absence of errors.
//
// Use only for registration purposes.
func (a *Server) validateMFAAuthResponseForRegister(ctx context.Context, resp *proto.MFAAuthenticateResponse, username string, requiredExtensions *mfav1.ChallengeExtensions) (hasDevices bool, err error) {
// Let users without a useable device go through registration.
if resp == nil || (resp.GetTOTP() == nil && resp.GetWebauthn() == nil && resp.GetSSO() == nil) {
devices, err := a.Services.GetMFADevices(ctx, username, false /* withSecrets */)
if err != nil {
return false, trace.Wrap(err)
}
if len(devices) == 0 {
// Allowed, no devices registered.
return false, nil
}
devsByType := groupByDeviceType(devices)
authPref, err := a.GetAuthPreference(ctx)
if err != nil {
return false, trace.Wrap(err)
}
hasTOTP := authPref.IsSecondFactorTOTPAllowed() && devsByType.TOTP
hasWebAuthn := authPref.IsSecondFactorWebauthnAllowed() && len(devsByType.Webauthn) > 0
hasSSO := authPref.IsSecondFactorSSOAllowed() && devsByType.SSO != nil
if hasTOTP || hasWebAuthn || hasSSO {
return false, trace.BadParameter("second factor authentication required")
}
// Allowed, no useable devices registered.
return false, nil
}
if err := a.WithUserLock(ctx, username, func() error {
_, err := a.ValidateMFAAuthResponse(ctx, resp, username, requiredExtensions)
return err
}); err != nil {
return false, trace.Wrap(err)
}
return true, nil
}
// ValidateMFAAuthResponse validates an MFA or passwordless challenge. The provided
// required challenge extensions will be checked against the stored challenge when
// applicable (webauthn only). Returns the authentication data derived from the solved
// challenge.
func (a *Server) ValidateMFAAuthResponse(
ctx context.Context,
resp *proto.MFAAuthenticateResponse,
user string,
requiredExtensions *mfav1.ChallengeExtensions,
) (*authz.MFAAuthData, error) {
if requiredExtensions == nil {
return nil, trace.BadParameter("required challenge extensions parameter required")
}
authData, validateErr := a.validateMFAAuthResponseInternal(ctx, resp, user, requiredExtensions)
// validateErr handled after audit.
// Read ClusterName for audit.
var clusterName string
if cn, err := a.GetClusterName(ctx); err != nil {
a.logger.WarnContext(ctx, "Failed to read cluster name", "error", err)
// err swallowed on purpose.
} else {
clusterName = cn.GetClusterName()
}
// Take the user from the authData if the user param is empty.
// This happens for passwordless.
if user == "" && authData != nil {
user = authData.User
}
// Emit audit event.
auditEvent := &apievents.ValidateMFAAuthResponse{
Metadata: apievents.Metadata{
Type: events.ValidateMFAAuthResponseEvent,
ClusterName: clusterName,
},
UserMetadata: authz.ClientUserMetadataWithUser(ctx, user),
ChallengeScope: requiredExtensions.Scope.String(),
}
if validateErr != nil {
auditEvent.Code = events.ValidateMFAAuthResponseFailureCode
auditEvent.Success = false
auditEvent.UserMessage = validateErr.Error()
auditEvent.Error = validateErr.Error()
} else {
auditEvent.Code = events.ValidateMFAAuthResponseCode
auditEvent.Success = true
deviceMetadata := mfaDeviceEventMetadata(authData.Device)
auditEvent.MFADevice = &deviceMetadata
auditEvent.ChallengeAllowReuse = authData.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES
}
if err := a.emitter.EmitAuditEvent(ctx, auditEvent); err != nil {
a.logger.WarnContext(ctx, "Failed to emit ValidateMFAAuthResponse event", "error", err)
// err swallowed on purpose.
}
return authData, trace.Wrap(validateErr)
}
func (a *Server) validateMFAAuthResponseInternal(
ctx context.Context,
resp *proto.MFAAuthenticateResponse,
user string,
requiredExtensions *mfav1.ChallengeExtensions,
) (*authz.MFAAuthData, error) {
if requiredExtensions == nil {
return nil, trace.BadParameter("required challenge extensions parameter required")
}
isPasswordless := requiredExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN
// Sanity check user/passwordless.
if user == "" && !isPasswordless {
return nil, trace.BadParameter("user required")
}
switch res := resp.Response.(type) {
// cases in order of preference
case *proto.MFAAuthenticateResponse_Webauthn:
// Read necessary configurations.
cap, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
u2f, err := cap.GetU2F()
switch {
case trace.IsNotFound(err): // OK, may happen.
case err != nil: // Unexpected.
return nil, trace.Wrap(err)
}
webConfig, err := cap.GetWebauthn()
if err != nil {
return nil, trace.Wrap(err)
}
assertionResp := wantypes.CredentialAssertionResponseFromProto(res.Webauthn)
var loginData *wanlib.LoginData
if isPasswordless {
webLogin := &wanlib.PasswordlessFlow{
Webauthn: webConfig,
Identity: a.Services,
}
loginData, err = webLogin.Finish(ctx, assertionResp)
// Disallow non-local users from logging in with passwordless.
if err == nil {
u, getErr := a.GetUser(ctx, loginData.User, false /* withSecrets */)
if getErr != nil {
err = trace.Wrap(getErr)
} else if u.GetUserType() != types.UserTypeLocal {
// Return the error unmodified, without the "MFA response validation
// failed" prefix.
return nil, trace.Wrap(types.ErrPassswordlessLoginBySSOUser)
}
}
} else {
webLogin := &wanlib.LoginFlow{
U2F: u2f,
Webauthn: webConfig,
Identity: a.Services,
}
loginData, err = webLogin.Finish(ctx, user, wantypes.CredentialAssertionResponseFromProto(res.Webauthn), requiredExtensions)
}
if err != nil {
if requiredExtensions.AllowReuse == mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES &&
trace.IsNotFound(err) {
// Do not add extra user messages to
// mfa.ErrExpiredReusableMFAResponse. Doing so will prevent
// client-side code from using errors.Is to reliably identify
// this specific error after it goes through gRPC. The original
// error isn't particularly useful to the user anyway, so just
// log it at debug level.
a.logger.DebugContext(ctx, "Reusable MFA response validation failed and possibly expired", "error", err)
return nil, trace.Wrap(&mfa.ErrExpiredReusableMFAResponse)
}
return nil, trace.AccessDenied("MFA response validation failed: %v", err)
}
return &authz.MFAAuthData{
Device: loginData.Device,
User: loginData.User,
AllowReuse: loginData.AllowReuse,
}, nil
case *proto.MFAAuthenticateResponse_TOTP:
dev, err := a.checkOTP(ctx, user, res.TOTP.Code)
if err != nil {
return nil, trace.Wrap(err)
}
return &authz.MFAAuthData{
Device: dev,
User: user,
// We store the last used token so OTP reuse is never allowed.
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO,
}, nil
case *proto.MFAAuthenticateResponse_SSO:
mfaAuthData, err := a.verifySSOMFASession(ctx, user, res.SSO.RequestId, res.SSO.Token, requiredExtensions)
return mfaAuthData, trace.Wrap(err)
default:
return nil, trace.BadParameter("unknown or missing MFAAuthenticateResponse type %T", resp.Response)
}
}
func mergeKeySets(a, b types.CAKeySet) types.CAKeySet {
newKeySet := a.Clone()
newKeySet.SSH = append(newKeySet.SSH, b.SSH...)
newKeySet.TLS = append(newKeySet.TLS, b.TLS...)
newKeySet.JWT = append(newKeySet.JWT, b.JWT...)
return newKeySet
}
// addAdditionalTrustedKeysAtomic performs an atomic update to
// the given CA with newKeys added to the AdditionalTrustedKeys
func (a *Server) addAdditionalTrustedKeysAtomic(ctx context.Context, ca types.CertAuthority, newKeys types.CAKeySet, needsUpdate func(types.CertAuthority) (bool, error)) error {
const maxIterations = 64
for range maxIterations {
if update, err := needsUpdate(ca); err != nil || !update {
return trace.Wrap(err)
}
err := ca.SetAdditionalTrustedKeys(mergeKeySets(
ca.GetAdditionalTrustedKeys(),
newKeys,
))
if err != nil {
return trace.Wrap(err)
}
if _, err := a.UpdateCertAuthority(ctx, ca); err == nil {
return nil
} else if !errors.Is(err, backend.ErrIncorrectRevision) {
return trace.Wrap(err)
}
ca, err = a.Services.GetCertAuthority(ctx, ca.GetID(), true)
if err != nil {
return trace.Wrap(err)
}
}
return trace.Errorf("too many conflicts attempting to set additional trusted keys for ca %q of type %q", ca.GetClusterName(), ca.GetType())
}
// newKeySet generates a new sets of keys for a given CA type.
// Keep this function in sync with lib/services/suite/suite.go:NewTestCAWithConfig().
func newKeySet(ctx context.Context, keyStore *keystore.Manager, caID types.CertAuthID) (types.CAKeySet, error) {
var keySet types.CAKeySet
// Add SSH keys if necessary.
switch caID.Type {
case types.UserCA, types.HostCA, types.OpenSSHCA:
sshKeyPair, err := keyStore.NewSSHKeyPair(ctx, sshCAKeyPurpose(caID.Type))
if err != nil {
return keySet, trace.Wrap(err)
}
keySet.SSH = append(keySet.SSH, sshKeyPair)
}
// Add TLS keys if necessary.
switch caID.Type {
case types.UserCA, types.HostCA, types.DatabaseCA, types.DatabaseClientCA, types.SAMLIDPCA, types.SPIFFECA, types.AWSRACA:
tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName, tlsCAKeyPurpose(caID.Type))
if err != nil {
return keySet, trace.Wrap(err)
}
keySet.TLS = append(keySet.TLS, tlsKeyPair)
}
// Add JWT keys if necessary.
switch caID.Type {
case types.JWTSigner, types.OIDCIdPCA, types.SPIFFECA, types.OktaCA, types.BoundKeypairCA:
jwtKeyPair, err := keyStore.NewJWTKeyPair(ctx, jwtCAKeyPurpose(caID.Type))
if err != nil {
return keySet, trace.Wrap(err)
}
keySet.JWT = append(keySet.JWT, jwtKeyPair)
}
return keySet, nil
}
func sshCAKeyPurpose(caType types.CertAuthType) cryptosuites.KeyPurpose {
switch caType {
case types.UserCA:
return cryptosuites.UserCASSH
case types.HostCA:
return cryptosuites.HostCASSH
case types.OpenSSHCA:
return cryptosuites.OpenSSHCASSH
}
return cryptosuites.KeyPurposeUnspecified
}
func tlsCAKeyPurpose(caType types.CertAuthType) cryptosuites.KeyPurpose {
switch caType {
case types.UserCA:
return cryptosuites.UserCATLS
case types.HostCA:
return cryptosuites.HostCATLS
case types.DatabaseCA:
return cryptosuites.DatabaseCATLS
case types.DatabaseClientCA:
return cryptosuites.DatabaseClientCATLS
case types.SAMLIDPCA:
return cryptosuites.SAMLIdPCATLS
case types.SPIFFECA:
return cryptosuites.SPIFFECATLS
case types.AWSRACA:
return cryptosuites.AWSRACATLS
}
return cryptosuites.KeyPurposeUnspecified
}
func jwtCAKeyPurpose(caType types.CertAuthType) cryptosuites.KeyPurpose {
switch caType {
case types.JWTSigner:
return cryptosuites.JWTCAJWT
case types.OIDCIdPCA:
return cryptosuites.OIDCIdPCAJWT
case types.SPIFFECA:
return cryptosuites.SPIFFECAJWT
case types.OktaCA:
return cryptosuites.OktaCAJWT
case types.BoundKeypairCA:
return cryptosuites.BoundKeypairCAJWT
}
return cryptosuites.KeyPurposeUnspecified
}
// ensureLocalAdditionalKeys adds additional trusted keys to the CA if they are not
// already present.
func (a *Server) ensureLocalAdditionalKeys(ctx context.Context, ca types.CertAuthority) error {
usableKeysResult, err := a.keyStore.HasUsableAdditionalKeys(ctx, ca)
if err != nil {
return trace.Wrap(err)
}
if usableKeysResult.CAHasPreferredKeyType {
// Nothing to do.
return nil
}
newKeySet, err := newKeySet(ctx, a.keyStore, ca.GetID())
if err != nil {
return trace.Wrap(err)
}
// The CA still needs an update while the CA does not contain any keys of
// the preferred type.
needsUpdate := func(ca types.CertAuthority) (bool, error) {
usableKeysResult, err := a.keyStore.HasUsableAdditionalKeys(ctx, ca)
return !usableKeysResult.CAHasPreferredKeyType, trace.Wrap(err)
}
err = a.addAdditionalTrustedKeysAtomic(ctx, ca, newKeySet, needsUpdate)
if err != nil {
return trace.Wrap(err)
}
a.logger.InfoContext(ctx, "Successfully added locally usable additional trusted keys to CA", "ca_type", ca.GetType())
return nil
}
// GetLicense return the license used the start the teleport enterprise auth server
func (a *Server) GetLicense(ctx context.Context) (string, error) {
if modules.GetModules().Features().Cloud {
return "", trace.AccessDenied("license cannot be downloaded on Cloud")
}
if a.license == nil {
return "", trace.NotFound("license not found")
}
return fmt.Sprintf("%s%s", a.license.CertPEM, a.license.KeyPEM), nil
}
// GetHeadlessAuthenticationFromWatcher gets a headless authentication from the headless
// authentication watcher.
func (a *Server) GetHeadlessAuthenticationFromWatcher(ctx context.Context, username, name string) (*types.HeadlessAuthentication, error) {
sub, err := a.headlessAuthenticationWatcher.Subscribe(ctx, username, name)
if err != nil {
return nil, trace.Wrap(err)
}
defer sub.Close()
// Wait for the login process to insert the headless authentication resource into the backend.
// If it already exists and passes the condition, WaitForUpdate will return it immediately.
headlessAuthn, err := sub.WaitForUpdate(ctx, func(ha *types.HeadlessAuthentication) (bool, error) {
return services.ValidateHeadlessAuthentication(ha) == nil, nil
})
return headlessAuthn, trace.Wrap(err)
}
// UpsertHeadlessAuthenticationStub creates a headless authentication stub for the user
// that will expire after the standard callback timeout.
func (a *Server) UpsertHeadlessAuthenticationStub(ctx context.Context, username string) error {
// Create the stub. If it already exists, update its expiration.
expires := a.clock.Now().Add(defaults.HeadlessLoginTimeout)
stub, err := types.NewHeadlessAuthentication(username, services.HeadlessAuthenticationUserStubID, expires)
if err != nil {
return trace.Wrap(err)
}
err = a.Services.UpsertHeadlessAuthentication(ctx, stub)
return trace.Wrap(err)
}
// CompareAndSwapHeadlessAuthentication performs a compare
// and swap replacement on a headless authentication resource.
func (a *Server) CompareAndSwapHeadlessAuthentication(ctx context.Context, old, new *types.HeadlessAuthentication) (*types.HeadlessAuthentication, error) {
headlessAuthn, err := a.Services.CompareAndSwapHeadlessAuthentication(ctx, old, new)
return headlessAuthn, trace.Wrap(err)
}
// getAccessRequestMonthlyUsage returns the number of access requests that have been created this month.
func (a *Server) getAccessRequestMonthlyUsage(ctx context.Context) (int, error) {
return resourceusage.GetAccessRequestMonthlyUsage(ctx, a.Services.AuditLogSessionStreamer, a.clock.Now().UTC())
}
// verifyAccessRequestMonthlyLimit checks whether the cluster has exceeded the monthly access request limit.
// If so, it returns an error. This is only applicable on usage-based billing plans.
func (a *Server) verifyAccessRequestMonthlyLimit(ctx context.Context) error {
f := modules.GetModules().Features()
accessRequestsEntitlement := f.GetEntitlement(entitlements.AccessRequests)
if accessRequestsEntitlement.Limit == 0 {
return nil // unlimited access
}
monthlyLimit := accessRequestsEntitlement.Limit
const limitReachedMessage = "cluster has reached its monthly access request limit, please contact the cluster administrator"
usage, err := a.getAccessRequestMonthlyUsage(ctx)
if err != nil {
return trace.Wrap(err)
}
if usage >= int(monthlyLimit) {
return trace.AccessDenied("%s", limitReachedMessage)
}
return nil
}
// getProxyPublicAddr returns the first valid, non-empty proxy public address it
// finds, or empty otherwise.
func (a *Server) getProxyPublicAddr() string {
if proxies, err := a.GetProxies(); err == nil {
for _, p := range proxies {
addr := p.GetPublicAddr()
if addr == "" {
continue
}
if _, err := utils.ParseAddr(addr); err != nil {
a.logger.WarnContext(a.closeCtx, "Invalid public address found in proxy",
"proxy", p.GetName(),
"public_addr", addr,
"error", err,
)
continue
}
return addr
}
}
return ""
}
// GetNodeStream streams a list of registered servers.
func (a *Server) GetNodeStream(ctx context.Context, namespace string) stream.Stream[types.Server] {
var done bool
startKey := ""
return stream.PageFunc(func() ([]types.Server, error) {
if done {
return nil, io.EOF
}
resp, err := a.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindNode,
Namespace: namespace,
Limit: apidefaults.DefaultChunkSize,
StartKey: startKey,
})
if err != nil {
return nil, trace.Wrap(err)
}
startKey = resp.NextKey
done = startKey == ""
resources := types.ResourcesWithLabels(resp.Resources)
servers, err := resources.AsServers()
return servers, trace.Wrap(err)
})
}
// SetWorkloadIdentityX509CAOverrideGetter sets the underlying
// [services.WorkloadIdentityX509CAOverrideGetter] used by the auth server for
// the implementation of [Server.GetWorkloadIdentityX509CAOverride] (provided by
// enterprise code). Not safe for concurrent access, so it should only be called
// during setup.
func (a *Server) SetWorkloadIdentityX509CAOverrideGetter(getter services.WorkloadIdentityX509CAOverrideGetter) {
a.workloadIdentityX509CAOverrideGetter = getter
}
// GetWorkloadIdentityX509CAOverride implements
// [services.WorkloadIdentityX509CAOverrideGetter] by optionally delegating to
// an underlying implementation (provided by enterprise code).
func (a *Server) GetWorkloadIdentityX509CAOverride(ctx context.Context, name string, ca *tlsca.CertAuthority) (*tlsca.CertAuthority, [][]byte, error) {
getter := a.workloadIdentityX509CAOverrideGetter
if getter == nil {
return ca, nil, nil
}
return getter.GetWorkloadIdentityX509CAOverride(ctx, name, ca)
}
// authKeepAliver is a keep aliver using auth server directly
type authKeepAliver struct {
sync.RWMutex
a *Server
ctx context.Context
cancel context.CancelFunc
keepAlivesC chan types.KeepAlive
err error
}
// KeepAlives returns a channel accepting keep alive requests
func (k *authKeepAliver) KeepAlives() chan<- types.KeepAlive {
return k.keepAlivesC
}
func (k *authKeepAliver) forwardKeepAlives() {
for {
select {
case <-k.a.closeCtx.Done():
k.Close()
return
case <-k.ctx.Done():
return
case keepAlive := <-k.keepAlivesC:
err := k.a.KeepAliveServer(k.ctx, keepAlive)
if err != nil {
k.closeWithError(err)
return
}
}
}
}
func (k *authKeepAliver) closeWithError(err error) {
k.Close()
k.Lock()
defer k.Unlock()
k.err = err
}
// Error returns the error if keep aliver
// has been closed
func (k *authKeepAliver) Error() error {
k.RLock()
defer k.RUnlock()
return k.err
}
// Done returns channel that is closed whenever
// keep aliver is closed
func (k *authKeepAliver) Done() <-chan struct{} {
return k.ctx.Done()
}
// Close closes keep aliver and cancels all goroutines
func (k *authKeepAliver) Close() error {
k.cancel()
return nil
}
// DefaultDNSNamesForRole returns default DNS names for the specified role.
func DefaultDNSNamesForRole(role types.SystemRole) []string {
if (types.SystemRoles{role}).IncludeAny(
types.RoleAuth,
types.RoleAdmin,
types.RoleProxy,
types.RoleKube,
types.RoleApp,
types.RoleDatabase,
types.RoleWindowsDesktop,
types.RoleOkta,
) {
return []string{
"*." + constants.APIDomain,
constants.APIDomain,
}
}
return nil
}
// SetSigstorePolicyEvaluator sets the SigstorePolicyEvaluator. It's called from
// the enterprise auth plugin.
func (s *Server) SetSigstorePolicyEvaluator(eval workloadidentityv1.SigstorePolicyEvaluator) {
s.lock.Lock()
defer s.lock.Unlock()
s.sigstorePolicyEvaluator = eval
}
// GetSigstorePolicyEvaluator returns the configured SigstorePolicyEvaluator. If
// none is configured, the Community Edition implementation will be returned.
func (s *Server) GetSigstorePolicyEvaluator() workloadidentityv1.SigstorePolicyEvaluator {
s.lock.RLock()
defer s.lock.RUnlock()
if e := s.sigstorePolicyEvaluator; e != nil {
return e
}
return workloadidentityv1.OSSSigstorePolicyEvaluator{}
}