mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
1956 lines
69 KiB
Go
1956 lines
69 KiB
Go
/*
|
||
* 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
|
||
|
||
import (
|
||
"cmp"
|
||
"context"
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"errors"
|
||
"fmt"
|
||
"log/slog"
|
||
"slices"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/coreos/go-semver/semver"
|
||
"github.com/gravitational/trace"
|
||
"github.com/jonboulle/clockwork"
|
||
"go.opentelemetry.io/otel/attribute"
|
||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||
oteltrace "go.opentelemetry.io/otel/trace"
|
||
"golang.org/x/crypto/ssh"
|
||
"golang.org/x/sync/errgroup"
|
||
|
||
"github.com/gravitational/teleport"
|
||
"github.com/gravitational/teleport/api/client/proto"
|
||
autoupdatev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1"
|
||
clusterconfigpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1"
|
||
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
|
||
joiningv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1"
|
||
summarizerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/summarizer/v1"
|
||
workloadidentityv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
|
||
"github.com/gravitational/teleport/api/types"
|
||
"github.com/gravitational/teleport/api/types/clusterconfig"
|
||
apievents "github.com/gravitational/teleport/api/types/events"
|
||
"github.com/gravitational/teleport/api/types/vnet"
|
||
"github.com/gravitational/teleport/api/utils/clientutils"
|
||
"github.com/gravitational/teleport/api/utils/keys"
|
||
"github.com/gravitational/teleport/lib/auth/autoupdate/autoupdatev1"
|
||
igcredentials "github.com/gravitational/teleport/lib/auth/integration/credentials"
|
||
"github.com/gravitational/teleport/lib/auth/keystore"
|
||
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
|
||
"github.com/gravitational/teleport/lib/auth/migration"
|
||
"github.com/gravitational/teleport/lib/auth/recordingencryption"
|
||
"github.com/gravitational/teleport/lib/auth/recordingencryption/recordingencryptionv1"
|
||
"github.com/gravitational/teleport/lib/auth/recordingmetadata"
|
||
"github.com/gravitational/teleport/lib/auth/state"
|
||
"github.com/gravitational/teleport/lib/auth/summarizer"
|
||
"github.com/gravitational/teleport/lib/backend"
|
||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||
"github.com/gravitational/teleport/lib/defaults"
|
||
"github.com/gravitational/teleport/lib/events"
|
||
"github.com/gravitational/teleport/lib/join/iamjoin"
|
||
"github.com/gravitational/teleport/lib/modules"
|
||
"github.com/gravitational/teleport/lib/scopes"
|
||
"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/srv/db/common/databaseobjectimportrule"
|
||
"github.com/gravitational/teleport/lib/sshca"
|
||
"github.com/gravitational/teleport/lib/tlsca"
|
||
usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport"
|
||
"github.com/gravitational/teleport/lib/utils"
|
||
logutils "github.com/gravitational/teleport/lib/utils/log"
|
||
)
|
||
|
||
var logger = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentAuth)
|
||
|
||
// VersionStorage local storage for saving the version.
|
||
type VersionStorage interface {
|
||
// GetTeleportVersion reads the last known Teleport version from storage.
|
||
GetTeleportVersion(ctx context.Context) (semver.Version, error)
|
||
// WriteTeleportVersion writes the last known Teleport version to the storage.
|
||
WriteTeleportVersion(ctx context.Context, version semver.Version) error
|
||
// DeleteTeleportVersion removes the last known Teleport version in storage.
|
||
DeleteTeleportVersion(ctx context.Context) error
|
||
}
|
||
|
||
// RecordingEncryptionManager wraps a RecordingEncryption backend service with higher level
|
||
// operations.
|
||
type RecordingEncryptionManager interface {
|
||
services.RecordingEncryption
|
||
recordingencryption.KeyUnwrapper
|
||
recordingencryptionv1.KeyRotater
|
||
SetCache(cache recordingencryption.Cache)
|
||
}
|
||
|
||
// InitConfig is auth server init config
|
||
type InitConfig struct {
|
||
// InsecureMode defines whether insecure connections are allowed.
|
||
InsecureMode bool
|
||
|
||
// Backend is auth backend to use
|
||
Backend backend.Backend
|
||
|
||
// VersionStorage is a version storage for local process
|
||
VersionStorage VersionStorage
|
||
|
||
// Authority is key generator that we use
|
||
Authority sshca.Authority
|
||
|
||
// KeyStoreConfig is the config for the KeyStore which handles private CA
|
||
// keys that may be held in an HSM.
|
||
KeyStoreConfig servicecfg.KeystoreConfig
|
||
|
||
// KeyStore handles private CA keys and encryption keys that may be held
|
||
// in an HSM.
|
||
KeyStore *keystore.Manager
|
||
|
||
// HostUUID is a UUID of this host
|
||
HostUUID string
|
||
|
||
// NodeName is the DNS name of the node
|
||
NodeName string
|
||
|
||
// ClusterName stores the FQDN of the signing CA (its certificate will have this
|
||
// name embedded). It is usually set to the GUID of the host the Auth service runs on
|
||
ClusterName types.ClusterName
|
||
|
||
// Authorities is a list of pre-configured authorities to supply on first start
|
||
Authorities []types.CertAuthority
|
||
|
||
// ApplyOnStartupResources is a set of resources that should be applied
|
||
// on each Teleport start.
|
||
ApplyOnStartupResources []types.Resource
|
||
|
||
// BootstrapResources is a list of previously backed-up resources used to
|
||
// bootstrap backend on first start.
|
||
BootstrapResources []types.Resource
|
||
|
||
// 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
|
||
|
||
// DataDir is the full path to the directory where keys, events and logs are kept
|
||
DataDir string
|
||
|
||
// ReverseTunnels is a list of reverse tunnels statically supplied
|
||
// in configuration, so auth server will init the tunnels on the first start
|
||
ReverseTunnels []types.ReverseTunnel
|
||
|
||
// OIDCConnectors is a list of trusted OpenID Connect identity providers
|
||
// in configuration, so auth server will init the tunnels on the first start
|
||
OIDCConnectors []types.OIDCConnector
|
||
|
||
// Trust is a service that manages users and credentials
|
||
Trust services.TrustInternal
|
||
|
||
// Presence service is a discovery and heartbeat tracker
|
||
Presence services.PresenceInternal
|
||
|
||
// Provisioner is a service that keeps track of provisioning tokens
|
||
Provisioner services.ProvisionerInternal
|
||
|
||
// Identity is a service that manages users and credentials
|
||
Identity services.IdentityInternal
|
||
|
||
// Access is service controlling access to resources
|
||
Access services.AccessInternal
|
||
|
||
// DynamicAccessExt is a service that manages dynamic RBAC.
|
||
DynamicAccessExt services.DynamicAccessExt
|
||
|
||
// Events is an event service
|
||
Events types.Events
|
||
|
||
// ClusterConfiguration is a services that holds cluster wide configuration.
|
||
ClusterConfiguration services.ClusterConfigurationInternal
|
||
|
||
// AutoUpdateService is a service of autoupdate configuration and version.
|
||
AutoUpdateService services.AutoUpdateService
|
||
|
||
// Restrictions is a service to access network restrictions, etc
|
||
Restrictions services.Restrictions
|
||
|
||
// Apps is a service that manages application resources.
|
||
Apps services.ApplicationsInternal
|
||
|
||
// Databases is a service that manages database resources.
|
||
Databases services.Databases
|
||
|
||
// DatabaseServices is a service that manages DatabaseService resources.
|
||
DatabaseServices services.DatabaseServices
|
||
|
||
// DelegationSessions is a service that manages delegation sessions.
|
||
DelegationSessions services.DelegationSessions
|
||
|
||
// Status is a service that manages cluster status info.
|
||
Status services.Status
|
||
|
||
// UserPreferences is a service that manages user preferences.
|
||
UserPreferences services.UserPreferences
|
||
|
||
// Roles is a set of roles to create
|
||
Roles []types.Role
|
||
|
||
// StaticTokens are pre-defined host provisioning tokens supplied via config file for
|
||
// environments where paranoid security is not needed
|
||
StaticTokens types.StaticTokens
|
||
|
||
// StaticTokens are pre-defined, scoped host provisioning tokens supplied via config file for
|
||
// environments where paranoid security is not needed
|
||
StaticScopedTokens *joiningv1.StaticScopedTokens
|
||
|
||
// AuthPreference defines the authentication type (local, oidc) and second
|
||
// factor passed in from a configuration file.
|
||
AuthPreference types.AuthPreference
|
||
|
||
// AuditLog is used for emitting events to audit log.
|
||
AuditLog events.AuditLogSessionStreamer
|
||
|
||
// ClusterAuditConfig holds cluster audit configuration.
|
||
ClusterAuditConfig types.ClusterAuditConfig
|
||
|
||
// ClusterNetworkingConfig holds cluster networking configuration.
|
||
ClusterNetworkingConfig types.ClusterNetworkingConfig
|
||
|
||
// SessionRecordingConfig holds session recording configuration.
|
||
SessionRecordingConfig types.SessionRecordingConfig
|
||
|
||
// SkipPeriodicOperations turns off periodic operations
|
||
// used in tests that don't need periodic operations.
|
||
SkipPeriodicOperations bool
|
||
|
||
// CipherSuites is a list of ciphersuites that the auth server supports.
|
||
CipherSuites []uint16
|
||
|
||
// Emitter is events emitter, used to submit discrete events
|
||
Emitter apievents.Emitter
|
||
|
||
// Streamer is events sessionstreamer, used to create continuous
|
||
// session related streams
|
||
Streamer events.Streamer
|
||
|
||
// WindowsDesktops is a service that manages Windows desktop resources.
|
||
WindowsDesktops services.WindowsDesktops
|
||
|
||
// DynamicWindowsDesktops is a service that manages dynamic Windows desktop resources.
|
||
DynamicWindowsDesktops services.DynamicWindowsDesktops
|
||
|
||
// LinuxDesktops is a service that manages Linux desktop resources.
|
||
LinuxDesktops services.LinuxDesktops
|
||
|
||
// SAMLIdPServiceProviders is a service that manages SAML IdP service providers.
|
||
SAMLIdPServiceProviders services.SAMLIdPServiceProviders
|
||
|
||
// UserGroups is a service that manages user groups.
|
||
UserGroups services.UserGroups
|
||
|
||
// CrownJewels is a service that manages CrownJewels.
|
||
CrownJewels services.CrownJewels
|
||
|
||
// Integrations is a service that manages Integrations.
|
||
Integrations services.Integrations
|
||
|
||
// UserTasks is a service that manages UserTasks.
|
||
UserTasks services.UserTasks
|
||
|
||
// DiscoveryConfigs is a service that manages DiscoveryConfigs.
|
||
DiscoveryConfigs services.DiscoveryConfigs
|
||
|
||
// SessionTrackerService is a service that manages trackers for all active sessions.
|
||
SessionTrackerService services.SessionTrackerService
|
||
|
||
// ConnectionsDiagnostic is a service that manages Connection Diagnostics resources.
|
||
ConnectionsDiagnostic services.ConnectionsDiagnostic
|
||
|
||
// LoadAllCAs tells tsh to load the host CAs for all clusters when trying to ssh into a node.
|
||
LoadAllCAs bool
|
||
|
||
// TraceClient is used to forward spans to the upstream telemetry collector
|
||
TraceClient otlptrace.Client
|
||
|
||
// Kubernetes is a service that manages kubernetes cluster resources.
|
||
Kubernetes services.Kubernetes
|
||
|
||
// AssertionReplayService is a service that mitigates SSO assertion replay.
|
||
*local.AssertionReplayService
|
||
|
||
// FIPS means FedRAMP/FIPS compliant configuration was requested.
|
||
FIPS bool
|
||
|
||
// UsageReporter is a service that forwards cluster usage events.
|
||
UsageReporter usagereporter.UsageReporter
|
||
|
||
// Okta is a service that manages Okta resources.
|
||
Okta services.Okta
|
||
|
||
// AccessLists is a service that manages access list resources.
|
||
AccessLists services.AccessListsInternal
|
||
|
||
// DatabaseObjectImportRule is a service that manages database object import rules.
|
||
DatabaseObjectImportRules services.DatabaseObjectImportRules
|
||
|
||
// DatabaseObject is a service that manages database objects.
|
||
DatabaseObjects services.DatabaseObjects
|
||
|
||
// UserLoginStates is a service that manages user login states.
|
||
UserLoginState services.UserLoginStates
|
||
|
||
// SecReports is a service that manages security reports.
|
||
SecReports services.SecReports
|
||
|
||
// PluginData is a service that manages plugin data.
|
||
PluginData services.PluginData
|
||
|
||
// Clock is the clock instance auth uses. Typically you'd only want to set
|
||
// this during testing.
|
||
Clock clockwork.Clock
|
||
|
||
// HTTPClientForAWSSTS overwrites the default HTTP client used for making
|
||
// STS requests. Used in test.
|
||
HTTPClientForAWSSTS utils.HTTPDoClient
|
||
|
||
// AWSOrganizationsClientGetter provides an AWS client that can call Organizations APIs.
|
||
// This is used to allow the IAM join method to validate that an AWS account belongs to a specific AWS Organization.
|
||
AWSOrganizationsClientGetter iamjoin.OrganizationsAPIGetter
|
||
|
||
// Tracer used to create spans.
|
||
Tracer oteltrace.Tracer
|
||
|
||
// AccessMonitoringEnabled is true if access monitoring is enabled.
|
||
AccessMonitoringEnabled bool
|
||
|
||
// AccessMonitoringRules is a service that manages access monitoring rules.
|
||
AccessMonitoringRules services.AccessMonitoringRules
|
||
|
||
// KubeWaitingContainers is a service that manages
|
||
// Kubernetes ephemeral containers that are waiting
|
||
// to be created until moderated session conditions are met.
|
||
KubeWaitingContainers services.KubeWaitingContainer
|
||
|
||
// Notifications is a service that manages notifications.
|
||
Notifications services.Notifications
|
||
|
||
// BotInstance is a service that manages Machine ID bot instances
|
||
BotInstance services.BotInstance
|
||
|
||
// SPIFFEFederations is a service that manages storing SPIFFE federations.
|
||
SPIFFEFederations services.SPIFFEFederations
|
||
|
||
// WorkloadIdentity is the service for storing and retrieving
|
||
// WorkloadIdentity resources.
|
||
WorkloadIdentity services.WorkloadIdentities
|
||
|
||
// WorkloadIdentityX509Revocations is the service for storing and retrieving
|
||
// WorkloadIdentityX509Revocations.
|
||
WorkloadIdentityX509Revocations services.WorkloadIdentityX509Revocations
|
||
|
||
// WorkloadIdentityX509Overrides handles the storage for workload
|
||
// identity-related X.509 certificate overrides.
|
||
WorkloadIdentityX509Overrides services.WorkloadIdentityX509Overrides
|
||
|
||
// SigstorePolicies handles the storage for Sigstore policy objects.
|
||
SigstorePolicies services.SigstorePolicies
|
||
|
||
// StaticHostUsers is a service that manages host users that should be
|
||
// created on SSH nodes.
|
||
StaticHostUsers services.StaticHostUser
|
||
|
||
// ProvisioningStates is a service that manages the storage and retrieval of
|
||
// downstream User and Access List provisioning records
|
||
ProvisioningStates services.ProvisioningStates
|
||
|
||
// Logger is the logger instance for the auth service to use.
|
||
Logger *slog.Logger
|
||
|
||
// IdentityCenter is the Identity Center state storage service to use in
|
||
// this node.
|
||
IdentityCenter services.IdentityCenter
|
||
|
||
// Plugins is a service that manages plugin resources for integrations.
|
||
Plugins *local.PluginsService
|
||
|
||
// PluginStaticCredentials handles credentials for integrations and plugins.
|
||
PluginStaticCredentials services.PluginStaticCredentials
|
||
|
||
// GitServers manages git servers.
|
||
GitServers services.GitServers
|
||
|
||
// StableUNIXUsers handles the storage for stable UNIX users.
|
||
StableUNIXUsers services.StableUNIXUsersInternal
|
||
|
||
// HealthCheckConfig manages health check config resources.
|
||
HealthCheckConfig services.HealthCheckConfig
|
||
|
||
// BackendInfo is a service of backend information.
|
||
BackendInfo services.BackendInfoService
|
||
|
||
// UserExternalCredentials manages per-user credentials for external services.
|
||
UserExternalCredentials services.UserExternalCredentialsService
|
||
|
||
// RecordingEncryption manages state for encrypted session recording.
|
||
RecordingEncryption RecordingEncryptionManager
|
||
|
||
// SkipVersionCheck skips version check during major version upgrade/downgrade.
|
||
SkipVersionCheck bool
|
||
|
||
// VnetConfigService manages the VNet config resource.
|
||
VnetConfigService services.VnetConfigService
|
||
|
||
// MultipartHandler handles multipart uploads.
|
||
MultipartHandler events.MultipartHandler
|
||
|
||
// RunWhileLockedRetryInterval defines the interval at which the auth server retries
|
||
// a locking operation for backend objects.
|
||
// This setting is particularly useful in test environments,
|
||
// as it can help accelerate operations such as updating the access list,
|
||
// especially when the list is also being modified concurrently by the background
|
||
// eligibility handler.
|
||
RunWhileLockedRetryInterval time.Duration
|
||
|
||
// ScopedAccess is a service that manages scoped access resources.
|
||
ScopedAccess services.ScopedAccess
|
||
|
||
// Summarizer manages summary inference configuration resources.
|
||
Summarizer services.Summarizer
|
||
|
||
// 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
|
||
|
||
// RecordingMetadataProvider provides recording metadata for session recordings.
|
||
RecordingMetadataProvider *recordingmetadata.Provider
|
||
|
||
// ScopedTokenService is a service that manages scoped join token resources.
|
||
ScopedTokenService services.ScopedTokenService
|
||
|
||
// AppAuthConfig is the service for storing and retrieving
|
||
// app auth config resources.
|
||
AppAuthConfig services.AppAuthConfig
|
||
|
||
// MFAService is the service that manages backend MFA resources.
|
||
MFAService MFAService
|
||
|
||
// WorkloadClusterService is the service that manages WorkloadClusters.
|
||
WorkloadClusterService services.WorkloadClusterService
|
||
|
||
// Beams is the service for reading and writing beams.
|
||
Beams services.Beams
|
||
|
||
// SubCAService manages CertAuthorityOverride resources.
|
||
SubCAService services.SubCAService
|
||
|
||
// FakePasswordHash is the password hash given to all users without a password.
|
||
// This helps eliminate timing attacks by ensuring that all authentication attempts
|
||
// with a password do a bcrypt comparison.
|
||
FakePasswordHash []byte
|
||
// FakeRecoveryCodeHash is the recovery code hash given to all users without codes.
|
||
// This helps eliminate timing attacks by ensuring that all recovery attempts
|
||
// with codes do a bcrypt comparison.
|
||
FakeRecoveryCodeHash []byte
|
||
// Modules defines build time constraints and licensed features.
|
||
Modules modules.Modules
|
||
|
||
// RemoteClusterRefreshLimit is the maximum number of backend updates that will be performed
|
||
// during periodic remote cluster connection status refresh.
|
||
RemoteClusterRefreshLimit int
|
||
// 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 int
|
||
|
||
// ScopesFeatures dictate which scoped components are enabled.
|
||
ScopesFeatures scopes.Features
|
||
}
|
||
|
||
// Init instantiates and configures an instance of AuthServer
|
||
func Init(ctx context.Context, cfg InitConfig, opts ...ServerOption) (*Server, error) {
|
||
ctx, span := cfg.Tracer.Start(ctx, "auth/Init")
|
||
defer span.End()
|
||
|
||
if cfg.DataDir == "" {
|
||
return nil, trace.BadParameter("DataDir: data dir can not be empty")
|
||
}
|
||
if cfg.HostUUID == "" {
|
||
return nil, trace.BadParameter("HostUUID: host UUID can not be empty")
|
||
}
|
||
|
||
// TODO(tross): return an error once modules are injected everywhere.
|
||
if cfg.Modules == nil {
|
||
cfg.Modules = modules.GetModules()
|
||
}
|
||
|
||
asrv, err := NewServer(&cfg, opts...)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
// Initialize the unified resource cache before running period checks that rely on it.
|
||
unifiedResourcesCache, err := services.NewUnifiedResourceCache(asrv.CloseContext(), services.UnifiedResourceCacheConfig{
|
||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||
QueueSize: defaults.UnifiedResourcesQueueSize,
|
||
Component: teleport.ComponentUnifiedResource,
|
||
Logger: cfg.Logger.With(teleport.ComponentKey, teleport.ComponentUnifiedResource),
|
||
Client: asrv.Cache,
|
||
MaxStaleness: time.Minute,
|
||
},
|
||
ResourceGetter: asrv,
|
||
})
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
asrv.SetUnifiedResourcesCache(unifiedResourcesCache)
|
||
|
||
domainName := cfg.ClusterName.GetClusterName()
|
||
if err := backend.RunWhileLocked(ctx,
|
||
backend.RunWhileLockedConfig{
|
||
LockConfiguration: backend.LockConfiguration{
|
||
Backend: cfg.Backend,
|
||
LockNameComponents: []string{domainName},
|
||
TTL: 30 * time.Second,
|
||
},
|
||
RefreshLockInterval: 20 * time.Second,
|
||
}, func(ctx context.Context) error {
|
||
return trace.Wrap(initCluster(ctx, cfg, asrv))
|
||
}); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
return asrv, nil
|
||
}
|
||
|
||
// initCluster configures the cluster based on the user provided configuration. This should
|
||
// only be called when the init lock is held to prevent multiple instances of Auth from attempting
|
||
// to bootstrap the cluster at the same time.
|
||
func initCluster(ctx context.Context, cfg InitConfig, asrv *Server) error {
|
||
span := oteltrace.SpanFromContext(ctx)
|
||
domainName := cfg.ClusterName.GetClusterName()
|
||
firstStart, err := isFirstStart(ctx, asrv, cfg)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if cfg.SkipVersionCheck {
|
||
if err := upsertTeleportVersion(ctx, cfg.VersionStorage, asrv.Services.BackendInfoService, *teleport.SemVer()); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
} else {
|
||
if err := validateAndUpdateTeleportVersion(ctx, cfg.VersionStorage, asrv.Services.BackendInfoService, *teleport.SemVer()); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
// if bootstrap resources are supplied, use them to bootstrap backend state
|
||
// on initial startup.
|
||
if len(cfg.BootstrapResources) > 0 {
|
||
if firstStart {
|
||
asrv.logger.InfoContext(ctx, "Applying bootstrap resources (first initialization)", "resource_count", len(cfg.BootstrapResources))
|
||
if err := checkResourceConsistency(ctx, cfg.Modules, asrv.keyStore, domainName, cfg.BootstrapResources...); err != nil {
|
||
return trace.Wrap(err, "refusing to bootstrap backend")
|
||
}
|
||
if err := local.CreateResources(ctx, cfg.Backend, cfg.BootstrapResources...); err != nil {
|
||
return trace.Wrap(err, "backend bootstrap failed")
|
||
}
|
||
} else {
|
||
asrv.logger.WarnContext(ctx, "Ignoring bootstrap resources (previously initialized)", "resource_count", len(cfg.BootstrapResources))
|
||
}
|
||
}
|
||
|
||
// if apply-on-startup resources are supplied, apply them
|
||
if len(cfg.ApplyOnStartupResources) > 0 {
|
||
asrv.logger.InfoContext(ctx, "Applying resources (apply-on-startup)", "resource_count", len(cfg.ApplyOnStartupResources))
|
||
|
||
if err := applyResources(ctx, asrv.Services, cfg.ApplyOnStartupResources, cfg.Modules.Features(), cfg.ScopesFeatures); err != nil {
|
||
return trace.Wrap(err, "applying resources failed")
|
||
}
|
||
}
|
||
|
||
// Set the ciphersuites that this auth server supports.
|
||
asrv.cipherSuites = cfg.CipherSuites
|
||
|
||
// INTERNAL: Authorities (plus Roles) and ReverseTunnels don't follow the
|
||
// same pattern as the rest of the configuration (they are not configuration
|
||
// singletons). However, we need to keep them around while Telekube uses them.
|
||
for _, role := range cfg.Roles {
|
||
if _, err := asrv.UpsertRole(ctx, role); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
asrv.logger.InfoContext(ctx, "Created role", "role", role.GetName())
|
||
}
|
||
for i := range cfg.Authorities {
|
||
ca := cfg.Authorities[i]
|
||
|
||
// Remove private key from leaf clusters.
|
||
if domainName != ca.GetClusterName() {
|
||
ca = ca.Clone()
|
||
types.RemoveCASecrets(ca)
|
||
}
|
||
// Don't re-create CA if it already exists, otherwise
|
||
// the existing cluster configuration will be corrupted;
|
||
// this part of code is only used in tests.
|
||
if err := asrv.CreateCertAuthority(ctx, ca); err != nil {
|
||
if !trace.IsAlreadyExists(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
} else {
|
||
asrv.logger.InfoContext(ctx, "Created trusted certificate authority",
|
||
"ca_name", ca.GetName(),
|
||
"ca_type", ca.GetType(),
|
||
)
|
||
}
|
||
}
|
||
for _, tunnel := range cfg.ReverseTunnels {
|
||
if _, err := asrv.UpsertReverseTunnel(ctx, tunnel); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
asrv.logger.InfoContext(ctx, "Created reverse tunnel", "tunnel", tunnel.GetName())
|
||
}
|
||
|
||
g, gctx := errgroup.WithContext(ctx)
|
||
g.Go(func() error {
|
||
ctx, span := cfg.Tracer.Start(gctx, "auth/SetClusterAuditConfig")
|
||
defer span.End()
|
||
return trace.Wrap(asrv.SetClusterAuditConfig(ctx, cfg.ClusterAuditConfig))
|
||
})
|
||
|
||
g.Go(func() error {
|
||
ctx, span := cfg.Tracer.Start(gctx, "auth/InitializeClusterNetworkingConfig")
|
||
defer span.End()
|
||
return trace.Wrap(initializeClusterNetworkingConfig(ctx, asrv, cfg.ClusterNetworkingConfig))
|
||
})
|
||
|
||
g.Go(func() error {
|
||
ctx, span := cfg.Tracer.Start(gctx, "auth/initializeAuthPreference")
|
||
if err := initializeAuthPreference(ctx, asrv, cfg.AuthPreference); err != nil {
|
||
span.End()
|
||
return trace.Wrap(err)
|
||
}
|
||
span.End()
|
||
|
||
ctx, span = cfg.Tracer.Start(gctx, "auth/InitializeSessionRecordingConfig")
|
||
defer span.End()
|
||
return trace.Wrap(initializeSessionRecordingConfig(ctx, asrv, cfg.SessionRecordingConfig))
|
||
})
|
||
|
||
g.Go(func() error {
|
||
ctx, span := cfg.Tracer.Start(gctx, "auth/InitializeAccessGraphSettings")
|
||
defer span.End()
|
||
return trace.Wrap(initializeAccessGraphSettings(ctx, asrv))
|
||
})
|
||
|
||
g.Go(func() error {
|
||
ctx, span := cfg.Tracer.Start(gctx, "auth/InitializeVnetConfig")
|
||
defer span.End()
|
||
return trace.Wrap(initializeVnetConfig(ctx, asrv))
|
||
})
|
||
|
||
g.Go(func() error {
|
||
_, span := cfg.Tracer.Start(gctx, "auth/SetStaticTokens")
|
||
defer span.End()
|
||
asrv.logger.InfoContext(ctx, "Updating cluster configuration", "static_tokens", cfg.StaticTokens)
|
||
return trace.Wrap(asrv.SetStaticTokens(cfg.StaticTokens))
|
||
})
|
||
|
||
g.Go(func() error {
|
||
_, span := cfg.Tracer.Start(gctx, "auth/SetStaticScopedTokens")
|
||
defer span.End()
|
||
if cfg.StaticScopedTokens != nil {
|
||
return trace.Wrap(asrv.SetStaticScopedTokens(gctx, cfg.StaticScopedTokens))
|
||
}
|
||
return nil
|
||
})
|
||
|
||
var cn types.ClusterName
|
||
g.Go(func() error {
|
||
_, span := cfg.Tracer.Start(gctx, "auth/SetClusterName")
|
||
defer span.End()
|
||
|
||
// The first Auth Server that starts gets to set the name of the cluster.
|
||
// If a cluster name/ID is already stored in the backend, the attempt to set
|
||
// a new name returns an AlreadyExists error.
|
||
err := asrv.SetClusterName(cfg.ClusterName)
|
||
if err == nil {
|
||
cn = cfg.ClusterName
|
||
return nil
|
||
}
|
||
|
||
if !trace.IsAlreadyExists(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
// If the cluster name has already been set, log a warning if the user
|
||
// is trying to change the name.
|
||
cn, err = asrv.Services.GetClusterName(ctx)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if cn.GetClusterName() != cfg.ClusterName.GetClusterName() {
|
||
const msg = "Cannot rename cluster: continuing with current cluster name. Teleport " +
|
||
"clusters can not be renamed once they are created. You are seeing this " +
|
||
"message for one of two reasons. Either you have not set \"cluster_name\" in " +
|
||
"Teleport configuration and changed the hostname of the auth server or you " +
|
||
"are trying to change the value of \"cluster_name\"."
|
||
asrv.logger.ErrorContext(ctx, msg,
|
||
"current_cluster_name", cn.GetClusterName(),
|
||
"configured_cluster_name", cfg.ClusterName.GetClusterName(),
|
||
)
|
||
}
|
||
|
||
asrv.logger.DebugContext(ctx, "Cluster configuration", "cluster_name", cn.GetClusterName())
|
||
return nil
|
||
})
|
||
|
||
if err := g.Wait(); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
// Override user passed in cluster name with what is in the backend.
|
||
cfg.ClusterName = cn
|
||
|
||
// Apply any outstanding migrations.
|
||
if err := migration.Apply(ctx, asrv.logger, cfg.Backend); err != nil {
|
||
return trace.Wrap(err, "applying migrations")
|
||
}
|
||
|
||
// Clone UserCA into WindowsCA. Must happen before initializeAuthorities() so
|
||
// it only affects upgraded clusters.
|
||
// Added on Teleport 18.x and 19.
|
||
clusterConfiguration, err := local.NewClusterConfigurationService(cfg.Backend)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err := migrateWindowsCA(ctx, migrateWindowsCAParams{
|
||
Logger: asrv.logger,
|
||
ClusterConfiguration: clusterConfiguration,
|
||
Trust: local.NewCAService(cfg.Backend),
|
||
}); err != nil {
|
||
return trace.Wrap(err, "migrate WindowsCA")
|
||
}
|
||
|
||
// generate certificate authorities if they don't exist
|
||
if err := initializeAuthorities(ctx, asrv, &cfg); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if cfg.InsecureMode {
|
||
const warningMessage = "Starting teleport in insecure mode. This is " +
|
||
"dangerous! Sensitive information will be logged to console and " +
|
||
"certificates will not be verified. Proceed with caution!"
|
||
asrv.logger.WarnContext(ctx, warningMessage)
|
||
}
|
||
|
||
span.AddEvent("migrating legacy resources")
|
||
// Migrate any legacy resources to new format.
|
||
if err := migrateLegacyResources(ctx, asrv); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
span.AddEvent("completed migration legacy resources")
|
||
|
||
span.AddEvent("checking certificate authority cluster names")
|
||
if err := checkAuthorityClusterNames(ctx, asrv); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
span.AddEvent("completed checking certificate authority cluster names")
|
||
|
||
// Create presets - convenience and example resources.
|
||
if !services.IsDashboard(*cfg.Modules.Features().ToProto()) {
|
||
span.AddEvent("creating preset roles")
|
||
if err := createPresetRoles(ctx, asrv.modules.BuildType(), asrv); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
span.AddEvent("completed creating preset roles")
|
||
|
||
span.AddEvent("creating preset users")
|
||
if err := createPresetUsers(ctx, asrv.modules.BuildType(), asrv); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
span.AddEvent("completed creating preset users")
|
||
|
||
span.AddEvent("creating preset database object import rules")
|
||
if err := createPresetDatabaseObjectImportRule(ctx, asrv); err != nil {
|
||
// merely raise a warning; this is not a fatal error.
|
||
asrv.logger.WarnContext(ctx, "error creating preset database object import rules", "error", err)
|
||
}
|
||
span.AddEvent("completed creating database object import rules")
|
||
} else {
|
||
asrv.logger.InfoContext(ctx, "skipping preset role and user creation")
|
||
}
|
||
|
||
if !cfg.SkipPeriodicOperations {
|
||
asrv.logger.InfoContext(ctx, "Auth server is running periodic operations")
|
||
go asrv.runPeriodicOperations()
|
||
} else {
|
||
asrv.logger.InfoContext(ctx, "Auth server is skipping periodic operations")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func initializeAuthorities(ctx context.Context, asrv *Server, cfg *InitConfig) error {
|
||
var (
|
||
mu sync.Mutex
|
||
allKeysInUse [][]byte
|
||
)
|
||
usableKeysResults := make(map[types.CertAuthType]*keystore.UsableKeysResult)
|
||
g, gctx := errgroup.WithContext(ctx)
|
||
for _, caType := range types.CertAuthTypes {
|
||
g.Go(func() error {
|
||
tctx, span := cfg.Tracer.Start(gctx, "auth/initializeAuthority", oteltrace.WithAttributes(attribute.String("type", string(caType))))
|
||
defer span.End()
|
||
|
||
caID := types.CertAuthID{Type: caType, DomainName: cfg.ClusterName.GetClusterName()}
|
||
usableKeysResult, keysInUse, err := initializeAuthority(tctx, asrv, caID)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
usableKeysResults[caType] = usableKeysResult
|
||
allKeysInUse = append(allKeysInUse, keysInUse...)
|
||
return nil
|
||
})
|
||
}
|
||
if err := g.Wait(); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if err := asrv.syncUsableKeysAlert(ctx, usableKeysResults); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
// Collect CAs from integrations to avoid deleting them.
|
||
for ig, err := range clientutils.Resources(ctx, asrv.Services.ListIntegrations) {
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
caKeySet, err := igcredentials.GetIntegrationCertAuthorities(ctx, ig, asrv.Services)
|
||
switch {
|
||
case trace.IsNotImplemented(err):
|
||
case err != nil:
|
||
// This should not happen by design. In case integration is in a
|
||
// bad state, log a warning instead of failing this initialization.
|
||
asrv.logger.WarnContext(ctx, "Failed to fetch integration CAs", "ig", ig.GetName(), "error", err)
|
||
default:
|
||
allKeysInUse = append(allKeysInUse, collectKeysInUse(*caKeySet)...)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Delete any unused keys from the keyStore. This is to avoid exhausting
|
||
// (or wasting) HSM resources.
|
||
if err := asrv.keyStore.DeleteUnusedKeys(ctx, allKeysInUse); err != nil {
|
||
// Key deletion is best-effort, log a warning if it fails and carry on.
|
||
// We don't want to prevent a CA rotation, which may be necessary in
|
||
// some cases where this would fail.
|
||
asrv.logger.WarnContext(ctx, "An attempt to clean up unused HSM or KMS CA keys has failed unexpectedly", "error", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func initializeAuthority(ctx context.Context, asrv *Server, caID types.CertAuthID) (*keystore.UsableKeysResult, [][]byte, error) {
|
||
ca, err := asrv.Services.GetCertAuthority(ctx, caID, true)
|
||
if err != nil {
|
||
if !trace.IsNotFound(err) {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
|
||
asrv.logger.InfoContext(ctx, "First start: generating certificate authority", "ca_type", caID.Type)
|
||
if ca, err = generateAuthority(ctx, asrv, caID); err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
|
||
if err := asrv.CreateCertAuthority(ctx, ca); err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
// Add [empty] CRLs to any issuers that are missing them.
|
||
// These are valid for 10 years and regenerated on CA rotation.
|
||
// DELETE IN v20(probakowski, zmb3): by v20 all auths will have
|
||
// at least been on v19, and all versions of 19 backfill missing CRLs.
|
||
updated := false
|
||
for _, kp := range ca.GetActiveKeys().TLS {
|
||
cert, err := tlsca.ParseCertificatePEM(kp.Cert)
|
||
if err != nil {
|
||
asrv.logger.WarnContext(ctx, "Couldn't parse CA certificate", "ca_type", caID.Type, "error", err)
|
||
continue
|
||
}
|
||
|
||
needsNewCRL := len(kp.CRL) == 0
|
||
if !needsNewCRL {
|
||
// An earlier version of this code generated CRLs that may have been signed with the wrong keypair.
|
||
// To fix this, we must also validate the signature of existing CRLs.
|
||
if crl, err := x509.ParseRevocationList(kp.CRL); err != nil {
|
||
needsNewCRL = true
|
||
} else if err := crl.CheckSignatureFrom(cert); err != nil {
|
||
asrv.logger.WarnContext(ctx, "Detected CRL with invalid signature, regenerating", "ca_type", caID.Type, "ca", ca.GetName(), "error", err)
|
||
needsNewCRL = true
|
||
}
|
||
}
|
||
|
||
if !needsNewCRL {
|
||
continue
|
||
}
|
||
|
||
signer, err := asrv.keyStore.TLSSigner(ctx, kp)
|
||
if err != nil {
|
||
if !errors.Is(err, keystore.ErrUnusableKey) {
|
||
asrv.logger.WarnContext(ctx, "Couldn't get TLS signer for CA", "ca", ca.GetName(), "ca_type", caID.Type, "error", err)
|
||
}
|
||
continue
|
||
}
|
||
|
||
if cert.KeyUsage&x509.KeyUsageCRLSign == 0 {
|
||
asrv.logger.WarnContext(ctx, "Certificate authority can't sign CRLs, some Active Directory integrations will require a CA rotation", "ca_type", caID.Type)
|
||
continue
|
||
}
|
||
|
||
crl, err := keystore.GenerateCRL(cert, signer)
|
||
if err != nil {
|
||
asrv.logger.WarnContext(ctx, "Failed to generate CRL", "ca_type", caID.Type, "error", err)
|
||
continue
|
||
}
|
||
kp.CRL = crl
|
||
updated = true
|
||
}
|
||
|
||
if updated {
|
||
if ca, err = asrv.UpdateCertAuthority(ctx, ca); err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
// Make sure the keystore has usable keys. This is a bit redundant if the CA
|
||
// was just generated above, but cheap relative to generating the CA, and
|
||
// it's nice to get the usableKeysResult.
|
||
usableKeysResult, err := asrv.keyStore.HasUsableActiveKeys(ctx, ca)
|
||
if err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
if !usableKeysResult.CAHasUsableKeys {
|
||
if ca.GetType() == types.HostCA {
|
||
// We need to sign the local Admin identity to support auth startup
|
||
// and local tctl. For this special case we add new
|
||
// AdditionalTrustedKeys without any active keys. These keys will
|
||
// sign the local Admin identity but nothing else (until a CA
|
||
// rotation). Only the Host CA is necessary to issue the Admin
|
||
// identity.
|
||
//
|
||
// We can only get here if all the active keys for this CA are in an
|
||
// HSM or KMS that this auth is not configured to use. Because the
|
||
// auth will not use PKCS#11 keys created by a different host UUID,
|
||
// for clusters using HSM keys this includes cases where a new auth is
|
||
// added to an HA cluster, or an existing auth's host UUID is reset.
|
||
if err := asrv.ensureLocalAdditionalKeys(ctx, ca); err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
ca, err = asrv.Services.GetCertAuthority(ctx, caID, true)
|
||
if err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
usableKeysResult, err = asrv.keyStore.HasUsableActiveKeys(ctx, ca)
|
||
if err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
} else {
|
||
const msg = "This Auth Service is configured to use key types that the CA does not contain. " +
|
||
"No new certificates can be signed with the existing keys. " +
|
||
"You must perform a CA rotation to generate new keys, or adjust your configuration to use the existing keys."
|
||
asrv.logger.WarnContext(ctx, msg,
|
||
"configured_key_type", usableKeysResult.PreferredKeyType,
|
||
"ca_type", caID.Type,
|
||
"available_key_types", usableKeysResult.CAKeyTypes,
|
||
)
|
||
}
|
||
} else if !usableKeysResult.CAHasPreferredKeyType {
|
||
const msg = "This Auth Service is configured to use key types that the CA does not contain. " +
|
||
"New certificates will continue to be signed with raw software keys but you must perform a CA rotation to begin using the new key type."
|
||
asrv.logger.WarnContext(ctx, msg,
|
||
"configured_key_type", usableKeysResult.PreferredKeyType,
|
||
"ca_type", caID.Type,
|
||
"available_key_types", usableKeysResult.CAKeyTypes,
|
||
)
|
||
}
|
||
allKeyTypes := ca.AllKeyTypes()
|
||
numKeyTypes := len(allKeyTypes)
|
||
if numKeyTypes > 1 {
|
||
const msg = "CA contains a combination of key types. If you are attempting to" +
|
||
" configure HSM or KMS key storage, make sure it is configured on all auth servers in" +
|
||
" this cluster and then perform a CA rotation: https://goteleport.com/docs/admin-guides/management/operations/ca-rotation/"
|
||
asrv.logger.WarnContext(ctx, msg,
|
||
"ca_type", caID.Type,
|
||
"key_types", []string{strings.Join(allKeyTypes[:numKeyTypes-1], ", "), allKeyTypes[numKeyTypes-1]},
|
||
)
|
||
}
|
||
ca, err = applyAuthorityConfig(ctx, asrv, ca)
|
||
if err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
keysInUse := collectKeysInUse(ca.GetActiveKeys(), ca.GetAdditionalTrustedKeys())
|
||
return usableKeysResult, keysInUse, nil
|
||
}
|
||
|
||
func collectKeysInUse(cas ...types.CAKeySet) (keysInUse [][]byte) {
|
||
for _, keySet := range cas {
|
||
for _, sshKeyPair := range keySet.SSH {
|
||
keysInUse = append(keysInUse, sshKeyPair.PrivateKey)
|
||
}
|
||
for _, tlsKeyPair := range keySet.TLS {
|
||
keysInUse = append(keysInUse, tlsKeyPair.Key)
|
||
}
|
||
for _, jwtKeyPair := range keySet.JWT {
|
||
keysInUse = append(keysInUse, jwtKeyPair.PrivateKey)
|
||
}
|
||
}
|
||
return keysInUse
|
||
}
|
||
|
||
// applyAuthorityConfig applies the latest keystore config to active keys updating
|
||
// the stored CA if any changes occur.
|
||
func applyAuthorityConfig(ctx context.Context, asrv *Server, ca types.CertAuthority) (types.CertAuthority, error) {
|
||
activeKeys := ca.GetActiveKeys()
|
||
var (
|
||
changed bool
|
||
err error
|
||
)
|
||
|
||
apply := func(curr []byte) ([]byte, error) {
|
||
next, err := asrv.keyStore.ApplyMultiRegionConfig(ctx, curr)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if !slices.Equal(curr, next) {
|
||
changed = true
|
||
}
|
||
return next, nil
|
||
}
|
||
|
||
for _, key := range activeKeys.SSH {
|
||
key.PrivateKey, err = apply(key.PrivateKey)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
for _, key := range activeKeys.TLS {
|
||
key.Key, err = apply(key.Key)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
for _, key := range activeKeys.JWT {
|
||
key.PrivateKey, err = apply(key.PrivateKey)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
if !changed {
|
||
return ca, nil
|
||
}
|
||
if err := ca.SetActiveKeys(activeKeys); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
// This is only executed during cluster init while holding a lock to prevent
|
||
// other auth servers from updating CAs simulaniously.
|
||
ca, err = asrv.UpdateCertAuthority(ctx, ca)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return ca, nil
|
||
}
|
||
|
||
// generateAuthority creates a new self-signed authority of the provided type
|
||
// and returns it to the caller. It is the responsibility of callers to persist
|
||
// the authority.
|
||
func generateAuthority(ctx context.Context, asrv *Server, caID types.CertAuthID) (types.CertAuthority, error) {
|
||
keySet, err := newKeySet(ctx, asrv.keyStore, caID)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
|
||
Type: caID.Type,
|
||
ClusterName: caID.DomainName,
|
||
ActiveKeys: keySet,
|
||
})
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
return ca, nil
|
||
}
|
||
|
||
func checkAuthorityClusterNames(ctx context.Context, asrv *Server) error {
|
||
for _, caType := range types.CertAuthTypes {
|
||
authorities, err := asrv.Services.GetCertAuthorities(ctx, caType, false)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
for _, ca := range authorities {
|
||
caClusterName := ca.GetClusterName()
|
||
// sanity check that the cluster name in the CA certificates
|
||
// matches the authority resource's cluster name
|
||
for _, keyPair := range ca.GetTrustedTLSKeyPairs() {
|
||
cert, err := tlsca.ParseCertificatePEM(keyPair.Cert)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
clusterName, err := tlsca.ClusterName(cert.Subject)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if clusterName != caClusterName {
|
||
return trace.BadParameter("CA certificate of type %s has cluster name %q that does not match the cluster name %q found in the subject", ca.GetType(), caClusterName, clusterName)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
var secondFactorUpgradeInstructions = `
|
||
Teleport requires second factor authentication for local users.
|
||
The auth_service configuration should be updated to enable it.
|
||
|
||
auth_service:
|
||
authentication:
|
||
second_factor: on
|
||
webauthn:
|
||
rp_id: example.com
|
||
|
||
For more information:
|
||
- https://goteleport.com/docs/admin-guides/access-controls/guides/webauthn/
|
||
`
|
||
|
||
func initializeAuthPreference(ctx context.Context, asrv *Server, newAuthPref types.AuthPreference) error {
|
||
const iterationLimit = 3
|
||
for range iterationLimit {
|
||
storedAuthPref, err := asrv.Services.GetAuthPreference(ctx)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
shouldReplace, err := shouldInitReplaceResourceWithOrigin(ctx, storedAuthPref, newAuthPref, asrv.logger)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if !shouldReplace {
|
||
if err := modules.ValidateResource(storedAuthPref); err != nil {
|
||
if errors.Is(err, modules.ErrCannotDisableSecondFactor) {
|
||
return trace.Wrap(err, secondFactorUpgradeInstructions)
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
switch {
|
||
case storedAuthPref == nil:
|
||
// This is a brand new cluster with no stored auth pref, set a
|
||
// default signature algorithm suite.
|
||
newAuthPref = newAuthPref.Clone()
|
||
newAuthPref.SetDefaultSignatureAlgorithmSuite(types.SignatureAlgorithmSuiteParams{
|
||
FIPS: asrv.fips,
|
||
UsingHSMOrKMS: asrv.keyStore.UsingHSMOrKMS(),
|
||
Cloud: asrv.modules.Features().Cloud,
|
||
})
|
||
case newAuthPref.Origin() == types.OriginDefaults:
|
||
// There is a stored auth preference which we are overwriting with a
|
||
// default auth preference. Maintain the stored signature algorithm
|
||
// suite to avoid automatically changing it on version upgrades, new
|
||
// suites should always be opt-in for existing clusters.
|
||
newAuthPref = newAuthPref.Clone()
|
||
newAuthPref.SetSignatureAlgorithmSuite(storedAuthPref.GetSignatureAlgorithmSuite())
|
||
case newAuthPref.GetSignatureAlgorithmSuite() == types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED:
|
||
// There is a stored auth preference and the new auth preference is
|
||
// coming from a config file where the signature_algorithm_suite is
|
||
// unset. Maintain the stored signature algorithm suite otherwise we
|
||
// would unset the default suite after the first auth restart.
|
||
newAuthPref = newAuthPref.Clone()
|
||
newAuthPref.SetSignatureAlgorithmSuite(storedAuthPref.GetSignatureAlgorithmSuite())
|
||
}
|
||
|
||
if err := modules.ValidateResource(newAuthPref); err != nil {
|
||
if errors.Is(err, modules.ErrCannotDisableSecondFactor) {
|
||
return trace.Wrap(err, secondFactorUpgradeInstructions)
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if storedAuthPref == nil {
|
||
asrv.logger.InfoContext(ctx, "Creating cluster auth preference", "auth_preference", newAuthPref)
|
||
_, err := asrv.CreateAuthPreference(ctx, newAuthPref)
|
||
if trace.IsAlreadyExists(err) {
|
||
continue
|
||
}
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
newAuthPref.SetRevision(storedAuthPref.GetRevision())
|
||
_, err = asrv.UpdateAuthPreference(ctx, newAuthPref)
|
||
if trace.IsCompareFailed(err) {
|
||
continue
|
||
}
|
||
if errors.Is(err, modules.ErrCannotDisableSecondFactor) {
|
||
return trace.Wrap(err, secondFactorUpgradeInstructions)
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
return trace.LimitExceeded("failed to initialize auth preference in %v iterations", iterationLimit)
|
||
}
|
||
|
||
func initializeClusterNetworkingConfig(ctx context.Context, asrv *Server, newNetConfig types.ClusterNetworkingConfig) error {
|
||
const iterationLimit = 3
|
||
for range 3 {
|
||
storedNetConfig, err := asrv.Services.GetClusterNetworkingConfig(ctx)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
shouldReplace, err := shouldInitReplaceResourceWithOrigin(ctx, storedNetConfig, newNetConfig, asrv.logger)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if !shouldReplace {
|
||
return nil
|
||
}
|
||
|
||
if storedNetConfig == nil {
|
||
asrv.logger.InfoContext(ctx, "Creating cluster networking configuration", "networking_config", newNetConfig)
|
||
_, err = asrv.CreateClusterNetworkingConfig(ctx, newNetConfig)
|
||
if trace.IsAlreadyExists(err) {
|
||
continue
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
asrv.logger.InfoContext(ctx, "Updating cluster networking configuration", "networking_config", newNetConfig)
|
||
newNetConfig.SetRevision(storedNetConfig.GetRevision())
|
||
_, err = asrv.UpdateClusterNetworkingConfig(ctx, newNetConfig)
|
||
if trace.IsCompareFailed(err) {
|
||
continue
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
return trace.LimitExceeded("failed to initialize cluster networking config in %v iterations", iterationLimit)
|
||
}
|
||
|
||
func initializeSessionRecordingConfig(ctx context.Context, asrv *Server, newRecConfig types.SessionRecordingConfig) error {
|
||
const iterationLimit = 3
|
||
for range iterationLimit {
|
||
storedRecConfig, err := asrv.Services.GetSessionRecordingConfig(ctx)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
shouldReplace, err := shouldInitReplaceResourceWithOrigin(ctx, storedRecConfig, newRecConfig, asrv.logger)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if !shouldReplace {
|
||
return nil
|
||
}
|
||
|
||
if storedRecConfig == nil {
|
||
asrv.logger.InfoContext(ctx, "Creating session recording config", "recording_config", newRecConfig)
|
||
_, err := asrv.CreateSessionRecordingConfig(ctx, newRecConfig)
|
||
if trace.IsAlreadyExists(err) {
|
||
continue
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
asrv.logger.InfoContext(ctx, "Updating session recording config", "recording_config", newRecConfig)
|
||
newRecConfig.SetRevision(storedRecConfig.GetRevision())
|
||
_, err = asrv.UpdateSessionRecordingConfig(ctx, newRecConfig)
|
||
if trace.IsCompareFailed(err) {
|
||
continue
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
return trace.LimitExceeded("failed to initialize session recording config in %v iterations", iterationLimit)
|
||
}
|
||
|
||
func initializeAccessGraphSettings(ctx context.Context, asrv *Server) error {
|
||
stored, err := asrv.Services.GetAccessGraphSettings(ctx)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
if stored != nil {
|
||
return nil
|
||
}
|
||
|
||
stored, err = clusterconfig.NewAccessGraphSettings(&clusterconfigpb.AccessGraphSettingsSpec{
|
||
SecretsScanConfig: clusterconfigpb.AccessGraphSecretsScanConfig_ACCESS_GRAPH_SECRETS_SCAN_CONFIG_DISABLED,
|
||
})
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
asrv.logger.InfoContext(ctx, "Creating access graph settings", "settings", stored)
|
||
_, err = asrv.CreateAccessGraphSettings(ctx, stored)
|
||
if trace.IsAlreadyExists(err) {
|
||
return nil
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
func initializeVnetConfig(ctx context.Context, asrv *Server) error {
|
||
stored, err := asrv.Services.GetVnetConfig(ctx)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
if stored != nil {
|
||
return nil
|
||
}
|
||
|
||
defaultConfig, err := vnet.DefaultVnetConfig()
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
asrv.logger.InfoContext(ctx, "Creating VNet config", "vnet_config", defaultConfig)
|
||
_, err = asrv.CreateVnetConfig(ctx, defaultConfig)
|
||
if trace.IsAlreadyExists(err) {
|
||
return nil
|
||
}
|
||
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
// shouldInitReplaceResourceWithOrigin determines whether the candidate
|
||
// resource should be used to replace the stored resource during auth server
|
||
// initialization. Dynamically configured resources must not be overwritten
|
||
// when the corresponding file config is left unspecified (i.e., by defaults).
|
||
func shouldInitReplaceResourceWithOrigin(ctx context.Context, stored, candidate types.ResourceWithOrigin, logger *slog.Logger) (bool, error) {
|
||
if candidate == nil || (candidate.Origin() != types.OriginDefaults && candidate.Origin() != types.OriginConfigFile) {
|
||
return false, trace.BadParameter("candidate origin must be either defaults or config-file (this is a bug)")
|
||
}
|
||
|
||
// If there is no resource stored in the backend or it was not dynamically
|
||
// configured, the candidate resource should be stored in the backend.
|
||
if stored == nil || stored.Origin() != types.OriginDynamic {
|
||
return true, nil
|
||
}
|
||
|
||
// If the candidate resource is explicitly configured in the config file,
|
||
// store this config-file resource in the backend no matter what.
|
||
if candidate.Origin() == types.OriginConfigFile {
|
||
// Log a warning when about to overwrite a dynamically configured resource.
|
||
if stored.Origin() == types.OriginDynamic {
|
||
logger.WarnContext(ctx, "Stored resource that was configured dynamically is about to be discarded in favor of explicit file configuration", "resource", stored.GetKind())
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
// The resource in the backend was configured dynamically, and there is no
|
||
// more authoritative file configuration to replace it. Keep the stored
|
||
// dynamic resource.
|
||
return false, nil
|
||
}
|
||
|
||
// migrationStart marks the migration as active.
|
||
// It should be called when a migration starts.
|
||
func migrationStart(ctx context.Context, migrationName string, logger *slog.Logger) {
|
||
logger.DebugContext(ctx, "Migration started", "migration_name", migrationName)
|
||
migrations.WithLabelValues(migrationName).Set(1)
|
||
}
|
||
|
||
// migrationEnd marks the migration as inactive.
|
||
// It should be called when a migration ends.
|
||
func migrationEnd(ctx context.Context, migrationName string, logger *slog.Logger) {
|
||
logger.DebugContext(ctx, "Migration ended", "migration_name", migrationName)
|
||
migrations.WithLabelValues(migrationName).Set(0)
|
||
}
|
||
|
||
func migrateLegacyResources(ctx context.Context, asrv *Server) error {
|
||
if err := migrateRemoteClusters(ctx, asrv); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err := migrateGitHubIntegrationStatus(ctx, asrv); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// migrateGitHubIntegrationStatus populates the status fields for existing
|
||
// GitHub integrations (SSHCAConfigured and GitHubApp).
|
||
func migrateGitHubIntegrationStatus(ctx context.Context, asrv *Server) error {
|
||
migrationStart(ctx, "github_integration_status", asrv.logger)
|
||
defer migrationEnd(ctx, "github_integration_status", asrv.logger)
|
||
|
||
var nextToken string
|
||
for {
|
||
integrations, token, err := asrv.Services.ListIntegrations(ctx, 0, nextToken)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
for _, ig := range integrations {
|
||
if ig.GetSubKind() != types.SubKindGitHub {
|
||
continue
|
||
}
|
||
if gh := ig.GetStatus().GitHub; gh != nil && gh.ClientID != "" {
|
||
continue
|
||
}
|
||
if err := populateGitHubIntegrationStatus(ctx, asrv, ig); err != nil {
|
||
asrv.logger.WarnContext(ctx, "Failed to migrate GitHub integration status",
|
||
"integration", ig.GetName(),
|
||
"error", err,
|
||
)
|
||
continue
|
||
}
|
||
}
|
||
if token == "" {
|
||
break
|
||
}
|
||
nextToken = token
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func populateGitHubIntegrationStatus(ctx context.Context, asrv *Server, ig types.Integration) error {
|
||
status := &types.GitHubIntegrationStatusV1{}
|
||
|
||
// Check if SSH CA credentials exist.
|
||
ref := ig.GetCredentials().GetStaticCredentialsRef()
|
||
if ref != nil {
|
||
_, err := igcredentials.GetByPurpose(ctx, ref, igcredentials.PurposeGitHubSSHCA, asrv.Services)
|
||
if err == nil {
|
||
status.SSHCAConfigured = true
|
||
} else if !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
// Determine if the OAuth client is a GitHub App based on client ID prefix.
|
||
if ref != nil {
|
||
oauthCred, err := igcredentials.GetByPurpose(ctx, ref, igcredentials.PurposeGitHubOAuth, asrv.Services)
|
||
if err == nil {
|
||
status.ClientID = oauthCred.GetOAuthClientID()
|
||
} else if !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
ig.SetStatus(types.IntegrationStatusV1{
|
||
GitHub: status,
|
||
})
|
||
if _, err := asrv.Services.UpdateIntegration(ctx, ig); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
asrv.logger.InfoContext(ctx, "Migrated GitHub integration status",
|
||
"integration", ig.GetName(),
|
||
"ssh_ca_configured", status.SSHCAConfigured,
|
||
"client_id", status.ClientID,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// PresetRoleManager contains the required Role Management methods to create a Preset Role.
|
||
type PresetRoleManager interface {
|
||
// GetRole returns role by name.
|
||
GetRole(ctx context.Context, name string) (types.Role, error)
|
||
// CreateRole creates a role.
|
||
CreateRole(ctx context.Context, role types.Role) (types.Role, error)
|
||
// UpsertRole creates or updates a role and emits a related audit event.
|
||
UpsertRole(ctx context.Context, role types.Role) (types.Role, error)
|
||
}
|
||
|
||
// GetPresetRoles returns a list of all preset roles expected to be available on
|
||
// this cluster.
|
||
func GetPresetRoles(buildType string) []types.Role {
|
||
presets := []types.Role{
|
||
services.NewPresetGroupAccessRole(buildType),
|
||
services.NewPresetEditorRole(),
|
||
services.NewPresetAccessRole(),
|
||
services.NewPresetAuditorRole(),
|
||
services.NewPresetReviewerRole(buildType),
|
||
services.NewPresetRequesterRole(buildType),
|
||
services.NewSystemAutomaticAccessApproverRole(buildType),
|
||
services.NewPresetDeviceAdminRole(buildType),
|
||
services.NewPresetDeviceEnrollRole(buildType),
|
||
services.NewPresetRequireTrustedDeviceRole(buildType),
|
||
services.NewSystemOktaAccessRole(buildType),
|
||
services.NewSystemOktaRequesterRole(buildType),
|
||
services.NewPresetTerraformProviderRole(),
|
||
services.NewSystemIdentityCenterAccessRole(buildType),
|
||
services.NewPresetWildcardWorkloadIdentityIssuerRole(),
|
||
services.NewPresetAccessPluginRole(),
|
||
services.NewPresetAccessPluginWithReviewRole(),
|
||
services.NewPresetListAccessRequestResourcesRole(),
|
||
services.NewPresetMCPUserRole(),
|
||
services.NewSystemBeamRole(buildType),
|
||
services.NewPresetBeamUserRole(buildType),
|
||
services.NewPresetBeamAdminRole(buildType),
|
||
}
|
||
|
||
// Certain `New$FooRole()` functions will return a nil role if the
|
||
// corresponding feature is disabled. They should be filtered out as they
|
||
// are not actually made available on the cluster.
|
||
return slices.DeleteFunc(presets, func(r types.Role) bool { return r == nil })
|
||
}
|
||
|
||
// createPresetRoles creates preset role resources
|
||
func createPresetRoles(ctx context.Context, buildType string, rm PresetRoleManager) error {
|
||
roles := GetPresetRoles(buildType)
|
||
|
||
g, gctx := errgroup.WithContext(ctx)
|
||
for _, role := range roles {
|
||
// If the role is nil, skip because it doesn't apply to this Teleport installation.
|
||
if role == nil {
|
||
continue
|
||
}
|
||
|
||
role := role
|
||
g.Go(func() error {
|
||
// Specifically skip the Okta requester role, as it will be
|
||
// modified by the Okta access list sync.
|
||
if types.IsSystemResource(role) && role.GetName() != teleport.SystemOktaRequesterRoleName {
|
||
// System resources *always* get reset on every auth startup
|
||
if _, err := rm.UpsertRole(gctx, role); err != nil {
|
||
return trace.Wrap(err, "failed upserting system role %s", role.GetName())
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
if _, err := rm.CreateRole(gctx, role); err != nil {
|
||
if !trace.IsAlreadyExists(err) {
|
||
return trace.WrapWithMessage(err, "failed to create preset role %v", role.GetName())
|
||
}
|
||
|
||
currentRole, err := rm.GetRole(gctx, role.GetName())
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
role, err := services.AddRoleDefaults(gctx, buildType, currentRole)
|
||
if trace.IsAlreadyExists(err) {
|
||
return nil
|
||
}
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if _, err := rm.UpsertRole(gctx, role); err != nil {
|
||
return trace.WrapWithMessage(err, "failed to update preset role %v", role.GetName())
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
return trace.Wrap(g.Wait())
|
||
}
|
||
|
||
// PresetUsers contains the required User Management methods to
|
||
// create a preset User. Method names represent the appropriate
|
||
// subset
|
||
type PresetUsers interface {
|
||
// CreateUser creates a new user record based on the supplied `user` instance.
|
||
CreateUser(ctx context.Context, user types.User) (types.User, error)
|
||
// GetUser fetches a user from the repository by name, optionally fetching
|
||
// any associated secrets.
|
||
GetUser(ctx context.Context, username string, withSecrets bool) (types.User, error)
|
||
// UpsertUser user creates or updates a user record as needed.
|
||
UpsertUser(ctx context.Context, user types.User) (types.User, error)
|
||
}
|
||
|
||
// getPresetUsers returns a list of all preset users expected to be available on
|
||
// this cluster.
|
||
func getPresetUsers(buildType string) []types.User {
|
||
presets := []types.User{
|
||
services.NewSystemAutomaticAccessBotUser(buildType),
|
||
}
|
||
|
||
// Certain `New$FooUser()` functions will return a nil role if the
|
||
// corresponding feature is disabled. They should be filtered out as they
|
||
// are not actually made available on the cluster.
|
||
return slices.DeleteFunc(presets, func(u types.User) bool { return u == nil })
|
||
}
|
||
|
||
// createPresetUsers creates all of the required user presets. No attempt is
|
||
// made to migrate any existing users to the lastest preset.
|
||
func createPresetUsers(ctx context.Context, buildType string, um PresetUsers) error {
|
||
users := getPresetUsers(buildType)
|
||
for _, user := range users {
|
||
// Some users are only valid for enterprise Teleport, and so will be
|
||
// nil for an OSS build and can be skipped
|
||
if user == nil {
|
||
continue
|
||
}
|
||
|
||
if types.IsSystemResource(user) {
|
||
// System resources *always* get reset on every auth startup
|
||
if _, err := um.UpsertUser(ctx, user); err != nil {
|
||
return trace.Wrap(err, "failed upserting system user %s", user.GetName())
|
||
}
|
||
continue
|
||
}
|
||
|
||
if _, err := um.CreateUser(ctx, user); err != nil && !trace.IsAlreadyExists(err) {
|
||
return trace.Wrap(err, "failed creating preset user %s", user.GetName())
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// createPresetDatabaseObjectImportRule will create a sample database object import rule if there are none.
|
||
func createPresetDatabaseObjectImportRule(ctx context.Context, rules services.DatabaseObjectImportRules) error {
|
||
importRules, _, err := rules.ListDatabaseObjectImportRules(ctx, 100, "")
|
||
if err != nil {
|
||
return trace.Wrap(err, "failed listing available database object import rules")
|
||
}
|
||
// If there are existing rules, don't create a preset.
|
||
if len(importRules) > 0 {
|
||
return nil
|
||
}
|
||
|
||
rule := databaseobjectimportrule.NewPresetImportAllObjectsRule()
|
||
if rule == nil {
|
||
return nil
|
||
}
|
||
|
||
_, err = rules.CreateDatabaseObjectImportRule(ctx, rule)
|
||
if err != nil {
|
||
return trace.Wrap(err, "failed to create default database object import rule")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// isFirstStart returns 'true' if the auth server is starting for the 1st time
|
||
// on this server.
|
||
func isFirstStart(ctx context.Context, authServer *Server, cfg InitConfig) (bool, error) {
|
||
// check if the CA exists?
|
||
_, err := authServer.Services.GetCertAuthority(
|
||
ctx,
|
||
types.CertAuthID{
|
||
DomainName: cfg.ClusterName.GetClusterName(),
|
||
Type: types.HostCA,
|
||
}, false)
|
||
if err != nil {
|
||
if !trace.IsNotFound(err) {
|
||
return false, trace.Wrap(err)
|
||
}
|
||
// If a CA was not found, that means this is the first start.
|
||
return true, nil
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
// checkResourceConsistency checks far basic conflicting state issues.
|
||
func checkResourceConsistency(ctx context.Context, mod modules.Modules, keyStore *keystore.Manager, clusterName string, resources ...types.Resource) error {
|
||
for _, rsc := range resources {
|
||
switch r := rsc.(type) {
|
||
case types.CertAuthority:
|
||
// check that signing CAs have expected cluster name and that
|
||
// all CAs for this cluster do having signing keys.
|
||
caClusterName := r.GetClusterName()
|
||
seemsLocal := caClusterName == clusterName
|
||
|
||
var hasKeys bool
|
||
var signerErr error
|
||
switch r.GetType() {
|
||
case types.HostCA,
|
||
types.UserCA,
|
||
types.OpenSSHCA:
|
||
_, signerErr = keyStore.GetSSHSigner(ctx, r)
|
||
case types.DatabaseCA,
|
||
types.DatabaseClientCA,
|
||
types.SAMLIDPCA,
|
||
types.SPIFFECA,
|
||
types.AWSRACA,
|
||
types.WindowsCA,
|
||
types.AppClientCA:
|
||
_, _, signerErr = keyStore.GetTLSCertAndSigner(ctx, r)
|
||
case types.JWTSigner,
|
||
types.OIDCIdPCA,
|
||
types.OktaCA,
|
||
types.BoundKeypairCA:
|
||
_, signerErr = keyStore.GetJWTSigner(ctx, r)
|
||
default:
|
||
return trace.BadParameter("unexpected cert_authority type %s for cluster %v", r.GetType(), clusterName)
|
||
}
|
||
switch {
|
||
case signerErr == nil:
|
||
hasKeys = true
|
||
case trace.IsNotFound(signerErr):
|
||
hasKeys = false
|
||
default:
|
||
return trace.Wrap(signerErr)
|
||
}
|
||
|
||
if seemsLocal && !hasKeys {
|
||
return trace.BadParameter("ca for local cluster %q missing signing keys", clusterName)
|
||
}
|
||
if !seemsLocal && hasKeys {
|
||
return trace.BadParameter("unexpected cluster name %q for signing ca (expected %q)", r.GetClusterName(), clusterName)
|
||
}
|
||
if !seemsLocal {
|
||
continue
|
||
}
|
||
case types.TrustedCluster:
|
||
if r.GetName() == clusterName {
|
||
return trace.BadParameter("trusted cluster has same name as local cluster (%q)", clusterName)
|
||
}
|
||
case types.Role:
|
||
// Some options are only available with enterprise subscription
|
||
if err := checkRoleFeatureSupport(mod, r); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
default:
|
||
// No validation checks for this resource type
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GenerateIdentity generates identity for the auth server
|
||
func GenerateIdentity(a *Server, id state.IdentityID, additionalPrincipals, dnsNames []string) (*state.Identity, error) {
|
||
key, err := cryptosuites.GenerateKey(context.Background(), cryptosuites.GetCurrentSuiteFromAuthPreference(a), cryptosuites.HostIdentity)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
sshPub, err := ssh.NewPublicKey(key.Public())
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
tlsPub, err := keys.MarshalPublicKey(key.Public())
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
certs, err := a.GenerateHostCerts(context.Background(), HostCertsParams{
|
||
Req: &proto.HostCertsRequest{
|
||
HostID: id.HostUUID,
|
||
NodeName: id.NodeName,
|
||
Role: id.Role,
|
||
AdditionalPrincipals: additionalPrincipals,
|
||
DNSNames: dnsNames,
|
||
PublicSSHKey: ssh.MarshalAuthorizedKey(sshPub),
|
||
PublicTLSKey: tlsPub,
|
||
},
|
||
})
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
priv, err := keys.MarshalPrivateKey(key)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return state.ReadIdentityFromKeyPair(priv, certs)
|
||
}
|
||
|
||
// CertInfo returns diagnostic information about certificate
|
||
func CertInfo(cert *x509.Certificate) string {
|
||
return fmt.Sprintf("cert(%v issued by %v:%v)", cert.Subject.CommonName, cert.Issuer.CommonName, cert.Issuer.SerialNumber)
|
||
}
|
||
|
||
// TLSCertInfo returns diagnostic information about certificate
|
||
func TLSCertInfo(cert *tls.Certificate) string {
|
||
x509cert, err := x509.ParseCertificate(cert.Certificate[0])
|
||
if err != nil {
|
||
return err.Error()
|
||
}
|
||
return CertInfo(x509cert)
|
||
}
|
||
|
||
// CertAuthorityInfo returns debugging information about certificate authority
|
||
func CertAuthorityInfo(ca types.CertAuthority) string {
|
||
var out []string
|
||
for _, keyPair := range ca.GetTrustedTLSKeyPairs() {
|
||
cert, err := tlsca.ParseCertificatePEM(keyPair.Cert)
|
||
if err != nil {
|
||
out = append(out, err.Error())
|
||
} else {
|
||
out = append(out, fmt.Sprintf("trust root(%v:%v)", cert.Subject.CommonName, cert.Subject.SerialNumber))
|
||
}
|
||
}
|
||
return fmt.Sprintf("cert authority(state: %v, phase: %v, roots: %v)", ca.GetRotation().State, ca.GetRotation().Phase, strings.Join(out, ", "))
|
||
}
|
||
|
||
// DELETE IN: 2.7.0
|
||
// NOTE: Sadly, our integration tests depend on this logic
|
||
// because they create remote cluster resource. Our integration
|
||
// tests should be migrated to use trusted clusters instead of manually
|
||
// creating tunnels.
|
||
// This migration adds remote cluster resource migrating from 2.5.0
|
||
// where the presence of remote cluster was identified only by presence
|
||
// of host certificate authority with cluster name not equal local cluster name
|
||
func migrateRemoteClusters(ctx context.Context, asrv *Server) error {
|
||
migrationStart(ctx, "remote_clusters", asrv.logger)
|
||
defer migrationEnd(ctx, "remote_clusters", asrv.logger)
|
||
|
||
clusterName, err := asrv.Services.GetClusterName(ctx)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
certAuthorities, err := asrv.Services.GetCertAuthorities(ctx, types.HostCA, false)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
// loop over all roles and make sure any v3 roles have permit port
|
||
// forward and forward agent allowed
|
||
for _, certAuthority := range certAuthorities {
|
||
if certAuthority.GetName() == clusterName.GetClusterName() {
|
||
asrv.logger.DebugContext(ctx, "Migrations: skipping local cluster cert authority", "cert_authority", certAuthority.GetName())
|
||
continue
|
||
}
|
||
// remote cluster already exists
|
||
_, err = asrv.Services.GetRemoteCluster(ctx, certAuthority.GetName())
|
||
if err == nil {
|
||
asrv.logger.DebugContext(ctx, "Migrations: remote cluster already exists for cert authority", "cert_authority", certAuthority.GetName())
|
||
continue
|
||
}
|
||
if !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
// the cert authority is associated with trusted cluster
|
||
_, err = asrv.Services.GetTrustedCluster(ctx, certAuthority.GetName())
|
||
if err == nil {
|
||
asrv.logger.DebugContext(ctx, "Migrations: trusted cluster resource exists for cert authority", "cert_authority", certAuthority.GetName())
|
||
continue
|
||
}
|
||
if !trace.IsNotFound(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
remoteCluster, err := types.NewRemoteCluster(certAuthority.GetName())
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
_, err = asrv.CreateRemoteCluster(ctx, remoteCluster)
|
||
if err != nil {
|
||
if !trace.IsAlreadyExists(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
}
|
||
asrv.logger.InfoContext(ctx, "Migrations: added remote cluster resource for cert authority", "cert_authority", certAuthority.GetName())
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ResourceApplyPriority specifies in which order the resources must be applied
|
||
// to avoid consistency issues. A lower priority means the resource is applied
|
||
// before.
|
||
var ResourceApplyPriority = map[string]int{
|
||
types.KindRole: 1,
|
||
types.KindInferenceSecret: 1,
|
||
|
||
types.KindUser: 2, // Users must be applied after Roles
|
||
types.KindInferenceModel: 2,
|
||
|
||
types.KindToken: 3,
|
||
types.KindClusterNetworkingConfig: 3,
|
||
types.KindClusterAuthPreference: 3,
|
||
types.KindWorkloadIdentity: 3,
|
||
// Bots should be applied after users and roles as at the moment they are actually converted to users and roles.
|
||
// This will ensure that Bot Users/Roles are properly created regardless of the Teleport version from which the
|
||
// resources have been exported.
|
||
types.KindBot: 3,
|
||
types.KindInferencePolicy: 3,
|
||
types.KindRetrievalModel: 3,
|
||
}
|
||
|
||
// Unlike when resources are loaded via --bootstrap, we're inserting elements via their service.
|
||
// This means consistency is checked. This function support applying resources
|
||
// with dependencies (like a user referring to a role).
|
||
func applyResources(ctx context.Context, service *Services, resources []types.Resource, features modules.Features, scopesFeatures scopes.Features) error {
|
||
var err error
|
||
slices.SortFunc(resources, func(a, b types.Resource) int {
|
||
priorityA := ResourceApplyPriority[a.GetKind()]
|
||
priorityB := ResourceApplyPriority[b.GetKind()]
|
||
return cmp.Compare(priorityA, priorityB)
|
||
})
|
||
for _, resource := range resources {
|
||
// DO NOT ADD EVERY RESOURCE TO THIS SWITCH
|
||
// Apply-on-startup should not be supported for every resource, this is a way to bootstrap a
|
||
// minimal cluster (bootstrap IaC user, role, bot, join token, config so you have enough
|
||
// trust to join core components without human intervention).
|
||
// Resources not required for a minimal cluster should be created via Infrastructure-as-Code
|
||
// (tctl, Terraform Provider, Kube Operator)
|
||
// You MUST also add a test case for your resource in `TestInit_ApplyOnStartup` because adding
|
||
// your resource to this switch in not enough for the --apply-on-startup flag to work, you also
|
||
// need to RegisterResourceUnmarshaler() your resource.
|
||
switch r := resource.(type) {
|
||
case types.ProvisionToken:
|
||
err = service.UpsertToken(ctx, r)
|
||
case types.User:
|
||
err = services.ValidateUserRoles(ctx, r, service)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
_, err = service.UpsertUser(ctx, r)
|
||
case types.Role:
|
||
_, err = service.UpsertRole(ctx, r)
|
||
case types.ClusterNetworkingConfig:
|
||
_, err = service.ClusterConfigurationInternal.UpsertClusterNetworkingConfig(ctx, r)
|
||
case types.AuthPreference:
|
||
_, err = service.ClusterConfigurationInternal.UpsertAuthPreference(ctx, r)
|
||
case types.Resource153UnwrapperT[*machineidv1pb.Bot]:
|
||
_, err = machineidv1.UpsertBot(ctx, service, r.UnwrapT(), time.Now(), "system", scopesFeatures)
|
||
case types.Resource153UnwrapperT[*autoupdatev1pb.AutoUpdateConfig]:
|
||
_, err = autoupdatev1.UpsertAutoUpdateConfig(ctx, service, r.UnwrapT(), features)
|
||
case types.Resource153UnwrapperT[*autoupdatev1pb.AutoUpdateVersion]:
|
||
_, err = autoupdatev1.UpsertAutoUpdateVersion(ctx, service, r.UnwrapT())
|
||
case types.Resource153UnwrapperT[*summarizerv1.InferenceModel]:
|
||
_, err = service.Summarizer.UpsertInferenceModel(ctx, r.UnwrapT())
|
||
case types.Resource153UnwrapperT[*summarizerv1.InferencePolicy]:
|
||
_, err = service.Summarizer.UpsertInferencePolicy(ctx, r.UnwrapT())
|
||
case types.Resource153UnwrapperT[*summarizerv1.InferenceSecret]:
|
||
_, err = service.Summarizer.UpsertInferenceSecret(ctx, r.UnwrapT())
|
||
case types.Resource153UnwrapperT[*summarizerv1.RetrievalModel]:
|
||
_, err = service.Summarizer.UpsertRetrievalModel(ctx, r.UnwrapT())
|
||
case types.Resource153UnwrapperT[*workloadidentityv1.WorkloadIdentity]:
|
||
_, err = service.WorkloadIdentities.UpsertWorkloadIdentity(ctx, r.UnwrapT())
|
||
default:
|
||
return trace.NotImplemented("cannot apply resource of type %T", resource)
|
||
}
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|