mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:16:43 +08:00
* Add type for GitLab ProvisionToken * Add default behaviour for domain * Add IDTokenClaims for GitLab * Add gitlab token source and token validator * Thread GitLab support through auth and tbot packages * Adjust cluster name fetching in token validator * Initialize GitLab token validator in auth * Improve comment on `sub` * Working GitLab CI delegated joining * Add additional token rule fields * Add checking for new configuration fields * add additional test cases for validation of gitlab config struct * Add TestAuth_RegisterUsingToken_GitLab * Add tests for IDTokenSource * Fix imports * Add tests for GitLab Token Validator * Fix some comments that were incomplete * Add license headers
4895 lines
161 KiB
Go
4895 lines
161 KiB
Go
/*
|
||
Copyright 2015-2019 Gravitational, Inc.
|
||
|
||
Licensed under the Apache License, Version 2.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
You may obtain a copy of the License at
|
||
|
||
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
||
Unless required by applicable law or agreed to in writing, software
|
||
distributed under the License is distributed on an "AS IS" BASIS,
|
||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
*/
|
||
|
||
// 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"
|
||
"context"
|
||
"crypto/rand"
|
||
"crypto/subtle"
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"math/big"
|
||
insecurerand "math/rand"
|
||
"net"
|
||
"os"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/coreos/go-oidc/oauth2"
|
||
"github.com/google/uuid"
|
||
liblicense "github.com/gravitational/license"
|
||
"github.com/gravitational/trace"
|
||
"github.com/jonboulle/clockwork"
|
||
"github.com/prometheus/client_golang/prometheus"
|
||
"github.com/sirupsen/logrus"
|
||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||
"golang.org/x/crypto/ssh"
|
||
"golang.org/x/exp/slices"
|
||
"google.golang.org/protobuf/types/known/durationpb"
|
||
|
||
"github.com/gravitational/teleport"
|
||
"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"
|
||
"github.com/gravitational/teleport/api/types"
|
||
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/keys"
|
||
"github.com/gravitational/teleport/api/utils/retryutils"
|
||
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
|
||
"github.com/gravitational/teleport/lib/auth/keystore"
|
||
"github.com/gravitational/teleport/lib/auth/native"
|
||
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
|
||
"github.com/gravitational/teleport/lib/authz"
|
||
"github.com/gravitational/teleport/lib/backend"
|
||
"github.com/gravitational/teleport/lib/circleci"
|
||
"github.com/gravitational/teleport/lib/defaults"
|
||
"github.com/gravitational/teleport/lib/events"
|
||
"github.com/gravitational/teleport/lib/githubactions"
|
||
"github.com/gravitational/teleport/lib/gitlab"
|
||
"github.com/gravitational/teleport/lib/inventory"
|
||
kubeutils "github.com/gravitational/teleport/lib/kube/utils"
|
||
"github.com/gravitational/teleport/lib/kubernetestoken"
|
||
"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/services"
|
||
"github.com/gravitational/teleport/lib/services/local"
|
||
"github.com/gravitational/teleport/lib/session"
|
||
"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/tlsca"
|
||
usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport"
|
||
"github.com/gravitational/teleport/lib/utils"
|
||
"github.com/gravitational/teleport/lib/utils/interval"
|
||
vc "github.com/gravitational/teleport/lib/versioncontrol"
|
||
"github.com/gravitational/teleport/lib/versioncontrol/github"
|
||
)
|
||
|
||
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
|
||
)
|
||
|
||
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) (*Server, error) {
|
||
err := metrics.RegisterPrometheusCollectors(prometheusCollectors...)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
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 = local.NewIdentityService(cfg.Backend)
|
||
}
|
||
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.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.NewDiscardEmitter()
|
||
}
|
||
if cfg.WindowsDesktops == nil {
|
||
cfg.WindowsDesktops = local.NewWindowsDesktopService(cfg.Backend)
|
||
}
|
||
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 = local.NewUserGroupService(cfg.Backend)
|
||
}
|
||
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.Enforcer == nil {
|
||
cfg.Enforcer = local.NewNoopEnforcer()
|
||
}
|
||
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{}
|
||
}
|
||
|
||
limiter, err := limiter.NewConnectionsLimiter(limiter.Config{
|
||
MaxConnections: defaults.LimiterMaxConcurrentSignatures,
|
||
})
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
if cfg.KeyStoreConfig.PKCS11 != (keystore.PKCS11Config{}) {
|
||
if !modules.GetModules().Features().HSM {
|
||
return nil, fmt.Errorf("PKCS11 HSM support requires a license with the HSM feature enabled: %w", ErrRequiresEnterprise)
|
||
}
|
||
cfg.KeyStoreConfig.PKCS11.HostUUID = cfg.HostUUID
|
||
} else if cfg.KeyStoreConfig.GCPKMS != (keystore.GCPKMSConfig{}) {
|
||
if !modules.GetModules().Features().HSM {
|
||
return nil, fmt.Errorf("Google Cloud KMS support requires a license with the HSM feature enabled: %w", ErrRequiresEnterprise)
|
||
}
|
||
cfg.KeyStoreConfig.GCPKMS.HostUUID = cfg.HostUUID
|
||
} else {
|
||
native.PrecomputeKeys()
|
||
cfg.KeyStoreConfig.Software.RSAKeyPairSource = native.GenerateKeyPair
|
||
}
|
||
cfg.KeyStoreConfig.Logger = log
|
||
keyStore, err := keystore.NewManager(context.Background(), cfg.KeyStoreConfig)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
services := &Services{
|
||
Trust: cfg.Trust,
|
||
PresenceInternal: cfg.Presence,
|
||
Provisioner: cfg.Provisioner,
|
||
Identity: cfg.Identity,
|
||
Access: cfg.Access,
|
||
DynamicAccessExt: cfg.DynamicAccessExt,
|
||
ClusterConfiguration: cfg.ClusterConfiguration,
|
||
Restrictions: cfg.Restrictions,
|
||
Apps: cfg.Apps,
|
||
Kubernetes: cfg.Kubernetes,
|
||
Databases: cfg.Databases,
|
||
DatabaseServices: cfg.DatabaseServices,
|
||
AuditLogSessionStreamer: cfg.AuditLog,
|
||
Events: cfg.Events,
|
||
WindowsDesktops: cfg.WindowsDesktops,
|
||
SAMLIdPServiceProviders: cfg.SAMLIdPServiceProviders,
|
||
UserGroups: cfg.UserGroups,
|
||
SessionTrackerService: cfg.SessionTrackerService,
|
||
Enforcer: cfg.Enforcer,
|
||
ConnectionsDiagnostic: cfg.ConnectionsDiagnostic,
|
||
StatusInternal: cfg.Status,
|
||
UsageReporter: cfg.UsageReporter,
|
||
}
|
||
|
||
closeCtx, cancelFunc := context.WithCancel(context.TODO())
|
||
as := Server{
|
||
bk: cfg.Backend,
|
||
clock: cfg.Clock,
|
||
limiter: limiter,
|
||
Authority: cfg.Authority,
|
||
AuthServiceName: cfg.AuthServiceName,
|
||
ServerID: cfg.HostUUID,
|
||
githubClients: make(map[string]*githubClient),
|
||
cancelFunc: cancelFunc,
|
||
closeCtx: closeCtx,
|
||
emitter: cfg.Emitter,
|
||
streamer: cfg.Streamer,
|
||
Unstable: local.NewUnstableService(cfg.Backend, cfg.AssertionReplayService),
|
||
Services: services,
|
||
Cache: services,
|
||
keyStore: keyStore,
|
||
inventory: inventory.NewController(cfg.Presence, services, inventory.WithAuthServerID(cfg.HostUUID)),
|
||
traceClient: cfg.TraceClient,
|
||
fips: cfg.FIPS,
|
||
loadAllCAs: cfg.LoadAllCAs,
|
||
}
|
||
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)
|
||
}
|
||
|
||
if as.ghaIDTokenValidator == nil {
|
||
as.ghaIDTokenValidator = githubactions.NewIDTokenValidator(
|
||
githubactions.IDTokenValidatorConfig{
|
||
Clock: as.clock,
|
||
},
|
||
)
|
||
}
|
||
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.circleCITokenValidate == nil {
|
||
as.circleCITokenValidate = func(
|
||
ctx context.Context, organizationID, token string,
|
||
) (*circleci.IDTokenClaims, error) {
|
||
return circleci.ValidateToken(
|
||
ctx, as.clock, circleci.IssuerURLTemplate, organizationID, token,
|
||
)
|
||
}
|
||
}
|
||
if as.kubernetesTokenValidator == nil {
|
||
as.kubernetesTokenValidator = &kubernetestoken.Validator{}
|
||
}
|
||
|
||
return &as, nil
|
||
}
|
||
|
||
type Services struct {
|
||
services.Trust
|
||
services.PresenceInternal
|
||
services.Provisioner
|
||
services.Identity
|
||
services.Access
|
||
services.DynamicAccessExt
|
||
services.ClusterConfiguration
|
||
services.Restrictions
|
||
services.Apps
|
||
services.Kubernetes
|
||
services.Databases
|
||
services.DatabaseServices
|
||
services.WindowsDesktops
|
||
services.SAMLIdPServiceProviders
|
||
services.UserGroups
|
||
services.SessionTrackerService
|
||
services.Enforcer
|
||
services.ConnectionsDiagnostic
|
||
services.StatusInternal
|
||
usagereporter.UsageReporter
|
||
types.Events
|
||
events.AuditLogSessionStreamer
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// GetWebToken returns existing web token described by req.
|
||
// Implements ReadAccessPoint
|
||
func (r *Services) GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) {
|
||
return r.Identity.WebTokens().Get(ctx, req)
|
||
}
|
||
|
||
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",
|
||
},
|
||
)
|
||
|
||
registeredAgents = prometheus.NewGaugeVec(
|
||
prometheus.GaugeOpts{
|
||
Namespace: teleport.MetricNamespace,
|
||
Name: teleport.MetricRegisteredServers,
|
||
Help: "The number of Teleport services that are connected to an auth server by version.",
|
||
},
|
||
[]string{teleport.TagVersion},
|
||
)
|
||
|
||
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},
|
||
)
|
||
|
||
prometheusCollectors = []prometheus.Collector{
|
||
generateRequestsCount, generateThrottledRequestsCount,
|
||
generateRequestsCurrent, generateRequestsLatencies, UserLoginCount, heartbeatsMissedByAuth,
|
||
registeredAgents, migrations,
|
||
}
|
||
)
|
||
|
||
// 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
|
||
githubClients map[string]*githubClient
|
||
clock clockwork.Clock
|
||
bk backend.Backend
|
||
|
||
closeCtx context.Context
|
||
cancelFunc context.CancelFunc
|
||
|
||
samlAuthService SAMLService
|
||
oidcAuthService OIDCService
|
||
|
||
releaseService release.Client
|
||
|
||
loginRuleEvaluator loginrule.Evaluator
|
||
|
||
sshca.Authority
|
||
|
||
// 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.
|
||
Cache
|
||
|
||
// 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 events sessionstreamer, used to create continuous
|
||
// session related streams
|
||
streamer 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
|
||
|
||
inventory *inventory.Controller
|
||
|
||
// 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
|
||
|
||
// 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
|
||
|
||
// 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)
|
||
|
||
// kubernetesTokenValidator allows tokens from Kubernetes to be validated
|
||
// by the auth server. It can be overridden for the purpose of tests.
|
||
kubernetesTokenValidator kubernetesTokenValidator
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// CloseContext returns the close context
|
||
func (a *Server) CloseContext() context.Context {
|
||
return a.closeCtx
|
||
}
|
||
|
||
// SetLockWatcher sets the lock watcher.
|
||
func (a *Server) SetLockWatcher(lockWatcher *services.LockWatcher) {
|
||
a.lock.Lock()
|
||
defer a.lock.Unlock()
|
||
a.lockWatcher = lockWatcher
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// runPeriodicOperations runs some periodic bookkeeping operations
|
||
// performed by auth server
|
||
func (a *Server) runPeriodicOperations() {
|
||
ctx := context.TODO()
|
||
// 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.
|
||
r := insecurerand.New(insecurerand.NewSource(a.GetClock().Now().UnixNano()))
|
||
period := defaults.HighResPollingPeriod + time.Duration(r.Intn(int(defaults.HighResPollingPeriod/time.Second)))*time.Second
|
||
log.Debugf("Ticking with period: %v.", period)
|
||
a.lock.RLock()
|
||
ticker := a.clock.NewTicker(period)
|
||
a.lock.RUnlock()
|
||
// Create a ticker with jitter
|
||
heartbeatCheckTicker := interval.New(interval.Config{
|
||
Duration: apidefaults.ServerKeepAliveTTL() * 2,
|
||
Jitter: retryutils.NewSeventhJitter(),
|
||
})
|
||
promTicker := interval.New(interval.Config{
|
||
Duration: defaults.PrometheusScrapeInterval,
|
||
Jitter: retryutils.NewSeventhJitter(),
|
||
})
|
||
missedKeepAliveCount := 0
|
||
defer ticker.Stop()
|
||
defer heartbeatCheckTicker.Stop()
|
||
defer promTicker.Stop()
|
||
|
||
firstReleaseCheck := utils.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 = utils.HalfJitter(time.Second * 10)
|
||
}
|
||
|
||
// 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.
|
||
releaseCheck := interval.New(interval.Config{
|
||
Duration: time.Hour * 24,
|
||
FirstDuration: firstReleaseCheck,
|
||
Jitter: retryutils.NewFullJitter(),
|
||
})
|
||
defer releaseCheck.Stop()
|
||
|
||
// more frequent release check that just re-calculates alerts based on previously
|
||
// pulled versioning info.
|
||
localReleaseCheck := interval.New(interval.Config{
|
||
Duration: time.Minute * 10,
|
||
FirstDuration: utils.HalfJitter(time.Second * 10),
|
||
Jitter: retryutils.NewHalfJitter(),
|
||
})
|
||
defer localReleaseCheck.Stop()
|
||
|
||
// 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.NewSeventhJitter(),
|
||
})
|
||
defer remoteClustersRefresh.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-a.closeCtx.Done():
|
||
return
|
||
case <-remoteClustersRefresh.Next():
|
||
a.refreshRemoteClusters(ctx, r)
|
||
}
|
||
}
|
||
}()
|
||
|
||
for {
|
||
select {
|
||
case <-a.closeCtx.Done():
|
||
return
|
||
case <-ticker.Chan():
|
||
err := a.autoRotateCertAuthorities(ctx)
|
||
if err != nil {
|
||
if trace.IsCompareFailed(err) {
|
||
log.Debugf("Cert authority has been updated concurrently: %v.", err)
|
||
} else {
|
||
log.Errorf("Failed to perform cert rotation check: %v.", err)
|
||
}
|
||
}
|
||
case <-heartbeatCheckTicker.Next():
|
||
nodes, err := a.GetNodes(ctx, apidefaults.Namespace)
|
||
if err != nil {
|
||
log.Errorf("Failed to load nodes for heartbeat metric calculation: %v", err)
|
||
}
|
||
for _, node := range nodes {
|
||
if services.NodeHasMissedKeepAlives(node) {
|
||
missedKeepAliveCount++
|
||
}
|
||
}
|
||
// Update prometheus gauge
|
||
heartbeatsMissedByAuth.Set(float64(missedKeepAliveCount))
|
||
case <-promTicker.Next():
|
||
a.updateVersionMetrics()
|
||
case <-releaseCheck.Next():
|
||
a.syncReleaseAlerts(ctx, true)
|
||
case <-localReleaseCheck.Next():
|
||
a.syncReleaseAlerts(ctx, false)
|
||
}
|
||
}
|
||
}
|
||
|
||
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) {
|
||
log.Debug("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 {
|
||
log.Warnf("Failed to load github releases: %v (this will not impact teleport functionality)", err)
|
||
loadFailed = true
|
||
}
|
||
} else {
|
||
if err := a.visitCachedAlertVersions(ctx, &visitor); err != nil {
|
||
log.Warnf("Failed to load release alert into: %v (this will not impact teleport functionality)", 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.Iter(func(handle inventory.UpstreamHandle) {
|
||
v := vc.Normalize(handle.Hello().Version)
|
||
instanceVisitor.Visit(vc.NewTarget(v))
|
||
})
|
||
|
||
// build the general alert msg meant for broader consumption
|
||
msg, verInUse := makeUpgradeSuggestionMsg(visitor, current, instanceVisitor.Oldest())
|
||
|
||
if msg != "" {
|
||
alert, err := types.NewClusterAlert(
|
||
releaseAlertID,
|
||
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.AlertOnLogin, "yes"),
|
||
types.WithAlertLabel(types.AlertPermitAll, "yes"),
|
||
types.WithAlertLabel(verInUseLabel, verInUse),
|
||
types.WithAlertExpires(a.clock.Now().Add(alertTTL)),
|
||
)
|
||
if err != nil {
|
||
log.Warnf("Failed to build %s alert: %v (this is a bug)", releaseAlertID, err)
|
||
return
|
||
}
|
||
if err := a.UpsertClusterAlert(ctx, alert); err != nil {
|
||
log.Warnf("Failed to set %s alert: %v", releaseAlertID, err)
|
||
return
|
||
}
|
||
} else if cleanup {
|
||
log.Debugf("Cluster appears up to date, clearing %s alert.", releaseAlertID)
|
||
err := a.DeleteClusterAlert(ctx, releaseAlertID)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
log.Warnf("Failed to delete %s alert: %v", releaseAlertID, err)
|
||
}
|
||
}
|
||
|
||
if sp := visitor.NewestSecurityPatch(); sp.Ok() && sp.NewerThan(current) {
|
||
// explicit security patch alerts have a more limited audience, so we generate
|
||
// them as their own separate alert.
|
||
log.Warnf("A newer security patch has been detected. current=%s, patch=%s", current.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 {
|
||
log.Warnf("Failed to build %s alert: %v (this is a bug)", secAlertID, err)
|
||
return
|
||
}
|
||
|
||
if err := a.UpsertClusterAlert(ctx, alert); err != nil {
|
||
log.Warnf("Failed to set %s alert: %v", secAlertID, err)
|
||
return
|
||
}
|
||
} else if cleanup {
|
||
err := a.DeleteClusterAlert(ctx, secAlertID)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
log.Warnf("Failed to delete %s alert: %v", secAlertID, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// makeUpgradeSuggestionMsg generates an upgrade suggestion alert msg if one is
|
||
// needed (returns "" if everything looks up to date).
|
||
func makeUpgradeSuggestionMsg(visitor vc.Visitor, current, oldestInstance vc.Target) (msg string, ver string) {
|
||
if next := visitor.NextMajor(); next.Ok() {
|
||
// at least one stable release exists for the next major version
|
||
log.Debugf("Generating alert msg for next major version. current=%s, next=%s", current.Version(), next.Version())
|
||
return fmt.Sprintf("The next major version of Teleport is %s. Please consider upgrading your Cluster.", next.Major()), next.Version()
|
||
}
|
||
|
||
if nc := visitor.NewestCurrent(); nc.Ok() && nc.NewerThan(current) {
|
||
// newer release of the currently running major version is available
|
||
log.Debugf("Generating alert msg for new minor or patch release. current=%s, newest=%s", current.Version(), nc.Version())
|
||
return fmt.Sprintf("Teleport %s is now available, please consider upgrading your Cluster.", nc.Version()), nc.Version()
|
||
}
|
||
|
||
if oldestInstance.Ok() && current.NewerThan(oldestInstance) {
|
||
// at least one connected instance is older than this auth server
|
||
return "Some Agents within this Cluster are running an older version of Teleport. Please consider upgrading them.", current.Version()
|
||
}
|
||
|
||
return "", ""
|
||
}
|
||
|
||
// updateVersionMetrics leverages the inventory control stream to report the versions of
|
||
// all instances that are connected to a single auth server via prometheus metrics. To
|
||
// get an accurate representation of versions in an entire cluster the metric must be aggregated
|
||
// with all auth instances.
|
||
func (a *Server) updateVersionMetrics() {
|
||
versionCount := make(map[string]int)
|
||
|
||
// record versions for all connected resources
|
||
a.inventory.Iter(func(handle inventory.UpstreamHandle) {
|
||
versionCount[handle.Hello().Version]++
|
||
})
|
||
|
||
// record version for **THIS** auth server
|
||
versionCount[teleport.Version]++
|
||
|
||
// reset the gauges so that any versions that fall off are removed from exported metrics
|
||
registeredAgents.Reset()
|
||
for version, count := range versionCount {
|
||
registeredAgents.WithLabelValues(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, rnd *insecurerand.Rand) {
|
||
remoteClusters, err := a.Services.GetRemoteClusters()
|
||
if err != nil {
|
||
log.WithError(err).Error("Failed to load remote clusters for status refresh")
|
||
return
|
||
}
|
||
|
||
netConfig, err := a.GetClusterNetworkingConfig(ctx)
|
||
if err != nil {
|
||
log.WithError(err).Error("Failed to load networking config for remote cluster status refresh")
|
||
return
|
||
}
|
||
|
||
// randomize the order to optimize for multiple auth servers running in parallel
|
||
rnd.Shuffle(len(remoteClusters), func(i, j int) {
|
||
remoteClusters[i], remoteClusters[j] = remoteClusters[j], remoteClusters[i]
|
||
})
|
||
|
||
// 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
|
||
for _, remoteCluster := range remoteClusters {
|
||
if updated, err := a.updateRemoteClusterStatus(ctx, netConfig, remoteCluster); err != nil {
|
||
log.WithError(err).Error("Failed to perform remote cluster status refresh")
|
||
} 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.bk != nil {
|
||
if err := a.bk.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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// SetEnforcer sets the server's enforce service
|
||
func (a *Server) SetEnforcer(enforcer services.Enforcer) {
|
||
a.Services.Enforcer = enforcer
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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()
|
||
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()
|
||
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, services.HostCertParams{
|
||
CASigner: caSigner,
|
||
PublicHostKey: hostPublicKey,
|
||
HostID: hostID,
|
||
NodeName: nodeName,
|
||
Principals: principals,
|
||
ClusterName: clusterName,
|
||
Role: role,
|
||
TTL: ttl,
|
||
})
|
||
}
|
||
|
||
func (a *Server) generateHostCert(
|
||
ctx context.Context, p services.HostCertParams,
|
||
) ([]byte, error) {
|
||
authPref, err := a.GetAuthPreference(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if p.Role == types.RoleNode {
|
||
if lockErr := a.checkLockInForce(authPref.GetLockingMode(),
|
||
[]types.LockTarget{{Node: p.HostID}, {Node: HostFQDN(p.HostID, p.ClusterName)}},
|
||
); lockErr != nil {
|
||
return nil, trace.Wrap(lockErr)
|
||
}
|
||
}
|
||
return a.Authority.GenerateHostCert(p)
|
||
}
|
||
|
||
// GetKeyStore returns the KeyStore used by the auth server
|
||
func (a *Server) GetKeyStore() *keystore.Manager {
|
||
return a.keyStore
|
||
}
|
||
|
||
type certRequest struct {
|
||
// user is a user to generate certificate for
|
||
user types.User
|
||
// impersonator is a user who generates the certificate,
|
||
// is set when different from the user in the certificate
|
||
impersonator string
|
||
// checker is used to perform RBAC checks.
|
||
checker services.AccessChecker
|
||
// ttl is Duration of the certificate
|
||
ttl time.Duration
|
||
// publicKey is RSA public key in authorized_keys format
|
||
publicKey []byte
|
||
// 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 services.RequestIDs
|
||
// 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
|
||
// 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
|
||
// 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
|
||
// attestationStatement is an attestation statement associated with the given public key.
|
||
attestationStatement *keys.AttestationStatement
|
||
// skipAttestation is a server-side flag which is used to skip the attestation check.
|
||
skipAttestation bool
|
||
// deviceExtensions holds device-aware user certificate extensions.
|
||
deviceExtensions DeviceExtensions
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
type certRequestOption func(*certRequest)
|
||
|
||
func certRequestMFAVerified(mfaID string) certRequestOption {
|
||
return func(r *certRequest) { r.mfaVerified = mfaID }
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
func (a *Server) GenerateOpenSSHCert(ctx context.Context, req *proto.OpenSSHCertRequest) (*proto.OpenSSHCert, error) {
|
||
if req.Username == "" {
|
||
return nil, trace.BadParameter("username is empty")
|
||
}
|
||
if len(req.PublicKey) == 0 {
|
||
return nil, trace.BadParameter("public key is empty")
|
||
}
|
||
if req.TTL == 0 {
|
||
return nil, trace.BadParameter("TTL is not set")
|
||
}
|
||
if req.TTL < 0 {
|
||
return nil, trace.BadParameter("TTL must be positive")
|
||
}
|
||
if req.Cluster == "" {
|
||
return nil, trace.BadParameter("cluster is empty")
|
||
}
|
||
|
||
user, err := a.GetUser(req.Username, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
accessInfo := services.AccessInfoFromUser(user)
|
||
clusterName, err := a.GetClusterName()
|
||
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.generateOpenSSHCert(certRequest{
|
||
user: user,
|
||
publicKey: req.PublicKey,
|
||
compatibility: constants.CertificateFormatStandard,
|
||
checker: checker,
|
||
ttl: time.Duration(req.TTL),
|
||
traits: map[string][]string{
|
||
constants.TraitLogins: {req.Username},
|
||
},
|
||
routeToCluster: req.Cluster,
|
||
disallowReissue: true,
|
||
})
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
return &proto.OpenSSHCert{
|
||
Cert: certs.SSH,
|
||
}, nil
|
||
}
|
||
|
||
// GenerateUserTestCerts is used to generate user certificate, used internally for tests
|
||
func (a *Server) GenerateUserTestCerts(key []byte, username string, ttl time.Duration, compatibility, routeToCluster, pinnedIP string) ([]byte, []byte, error) {
|
||
user, err := a.GetUser(username, false)
|
||
if err != nil {
|
||
return nil, nil, trace.Wrap(err)
|
||
}
|
||
accessInfo := services.AccessInfoFromUser(user)
|
||
clusterName, err := a.GetClusterName()
|
||
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)
|
||
}
|
||
certs, err := a.generateUserCert(certRequest{
|
||
user: user,
|
||
ttl: ttl,
|
||
compatibility: compatibility,
|
||
publicKey: key,
|
||
routeToCluster: routeToCluster,
|
||
checker: checker,
|
||
traits: user.GetTraits(),
|
||
loginIP: pinnedIP,
|
||
pinIP: pinnedIP != "",
|
||
})
|
||
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.
|
||
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
|
||
// 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
|
||
}
|
||
|
||
// GenerateUserAppTestCert generates an application specific certificate, used
|
||
// internally for tests.
|
||
func (a *Server) GenerateUserAppTestCert(req AppTestCertRequest) ([]byte, error) {
|
||
user, err := a.GetUser(req.Username, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
accessInfo := services.AccessInfoFromUser(user)
|
||
clusterName, err := a.GetClusterName()
|
||
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()
|
||
}
|
||
certs, err := a.generateUserCert(certRequest{
|
||
user: user,
|
||
publicKey: req.PublicKey,
|
||
checker: 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: {uuid.New().String()},
|
||
}),
|
||
// Only allow this certificate to be used for applications.
|
||
usage: []string{teleport.UsageAppsOnly},
|
||
// Add in the application routing information.
|
||
appSessionID: sessionID,
|
||
appPublicAddr: req.PublicAddr,
|
||
appClusterName: req.ClusterName,
|
||
awsRoleARN: req.AWSRoleARN,
|
||
azureIdentity: req.AzureIdentity,
|
||
gcpServiceAccount: req.GCPServiceAccount,
|
||
})
|
||
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.
|
||
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) {
|
||
user, err := a.GetUser(req.Username, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
accessInfo := services.AccessInfoFromUser(user)
|
||
clusterName, err := a.GetClusterName()
|
||
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(certRequest{
|
||
user: user,
|
||
publicKey: req.PublicKey,
|
||
loginIP: req.PinnedIP,
|
||
pinIP: req.PinnedIP != "",
|
||
checker: 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,
|
||
})
|
||
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
|
||
// must match their TLS certificate.
|
||
SSHAuthorizedKey []byte
|
||
// 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")
|
||
}
|
||
|
||
// Is at least one extension present?
|
||
// Are the extensions valid?
|
||
identity := authCtx.Identity.GetIdentity()
|
||
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")
|
||
// Do not reissue if device extensions are already present.
|
||
case identity.DeviceExtensions.DeviceID != "",
|
||
identity.DeviceExtensions.AssetTag != "",
|
||
identity.DeviceExtensions.CredentialID != "":
|
||
return nil, trace.BadParameter("device extensions already present")
|
||
}
|
||
|
||
// Fetch user TLS certificate.
|
||
x509Cert, err := authz.UserCertificateFromContext(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
// Sanity check: x509Cert matches identity.
|
||
// Both the TLS certificate and the identity come from the same source, so
|
||
// they are unlikely to mismatch unless Teleport itself mixes it up.
|
||
if x509Cert.Subject.CommonName != identity.Username {
|
||
return nil, trace.BadParameter("identity and x509 user mismatch")
|
||
}
|
||
|
||
// 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 != identity.Username:
|
||
return nil, trace.BadParameter("identity and SSH user mismatch")
|
||
case !slices.Equal(filterAndSortPrincipals(sshCert.ValidPrincipals), filterAndSortPrincipals(identity.Principals)):
|
||
return nil, trace.BadParameter("identity and SSH principals mismatch")
|
||
case !apisshutils.KeysEqual(sshCert.Key, xPubKey):
|
||
return nil, trace.BadParameter("x509 and SSH public key mismatch")
|
||
// 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.
|
||
authPref, err := a.GetAuthPreference(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if err := a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{
|
||
checker: authCtx.Checker,
|
||
defaultMode: authPref.GetLockingMode(),
|
||
username: identity.Username,
|
||
mfaVerified: identity.MFAVerified,
|
||
activeAccessRequests: identity.ActiveRequests,
|
||
deviceID: opts.DeviceExtensions.DeviceID, // Check lock against requested device.
|
||
}); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
// Augment TLS certificate.
|
||
newIdentity := identity
|
||
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)
|
||
}
|
||
newTLSCert, err := tlsCA.GenerateCertificate(tlsca.CertificateRequest{
|
||
Clock: a.clock,
|
||
PublicKey: x509Cert.PublicKey,
|
||
Subject: subj,
|
||
// Use the same expiration as the original cert.
|
||
NotAfter: x509Cert.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(x509Cert.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)
|
||
}
|
||
|
||
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) {
|
||
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 := false
|
||
if _, ok := req.user.GetMetadata().Labels[types.BotLabel]; ok {
|
||
bot = true
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
if err := a.AnonymizeAndSubmit(&usagereporter.UserCertificateIssuedEvent{
|
||
UserName: user,
|
||
Ttl: durationpb.New(req.ttl),
|
||
IsBot: bot,
|
||
UsageDatabase: database,
|
||
UsageApp: app,
|
||
UsageKubernetes: kubernetes,
|
||
UsageDesktop: desktop,
|
||
}); err != nil {
|
||
log.Debugf("Unable to submit certificate issued usage event: %v", err)
|
||
}
|
||
}
|
||
|
||
// generateUserCert generates certificates signed with User CA
|
||
func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
|
||
return generateCert(a, req, types.UserCA)
|
||
}
|
||
|
||
// generateOpenSSHCert generates certificates signed with OpenSSH CA
|
||
func (a *Server) generateOpenSSHCert(req certRequest) (*proto.Certs, error) {
|
||
return generateCert(a, req, types.OpenSSHCA)
|
||
}
|
||
|
||
func generateCert(a *Server, req certRequest, caType types.CertAuthType) (*proto.Certs, error) {
|
||
ctx := context.TODO()
|
||
err := req.check()
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
if len(req.checker.GetAllowedResourceIDs()) > 0 && modules.GetModules().BuildType() != modules.BuildEnterprise {
|
||
return nil, fmt.Errorf("Resource Access Requests: %w", ErrRequiresEnterprise)
|
||
}
|
||
|
||
// Reject the cert request if there is a matching lock in force.
|
||
authPref, err := a.GetAuthPreference(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if err := a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{
|
||
checker: req.checker,
|
||
defaultMode: authPref.GetLockingMode(),
|
||
username: req.user.GetName(),
|
||
mfaVerified: req.mfaVerified,
|
||
activeAccessRequests: req.activeRequests.AccessRequests,
|
||
deviceID: req.deviceExtensions.DeviceID,
|
||
}); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
// reuse the same RSA keys for SSH and TLS keys
|
||
cryptoPubKey, err := sshutils.CryptoPublicKey(req.publicKey)
|
||
if 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)
|
||
}
|
||
if certificateFormat == teleport.CertificateFormatUnspecified {
|
||
certificateFormat = req.checker.CertificateFormat()
|
||
}
|
||
|
||
var sessionTTL time.Duration
|
||
var allowedLogins []string
|
||
|
||
// 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.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 or the session TTL for the role.
|
||
sessionTTL = req.checker.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.CheckLoginDuration(sessionTTL)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
attestedKeyPolicy := keys.PrivateKeyPolicyNone
|
||
requiredKeyPolicy := req.checker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
|
||
if !req.skipAttestation && requiredKeyPolicy != keys.PrivateKeyPolicyNone {
|
||
// verify that the required private key policy for the requesting identity
|
||
// is met by the provided attestation statement.
|
||
attestedKeyPolicy, err = modules.GetModules().AttestHardwareKey(ctx, a, requiredKeyPolicy, req.attestationStatement, cryptoPubKey, sessionTTL)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
clusterName, err := a.GetDomainName()
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if req.routeToCluster == "" {
|
||
req.routeToCluster = clusterName
|
||
}
|
||
if req.routeToCluster != clusterName {
|
||
// Authorize access to a remote cluster.
|
||
rc, err := a.GetRemoteCluster(req.routeToCluster)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if err := req.checker.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)
|
||
|
||
requestedResourcesStr, err := types.ResourceIDsToString(req.checker.GetAllowedResourceIDs())
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
pinnedIP := ""
|
||
if req.checker.PinSourceIP() || req.pinIP {
|
||
if req.loginIP == "" {
|
||
// TODO(anton): make sure all upstream callers provide clientIP and make this into hard error
|
||
// instead of warning, after merging #21080
|
||
log.Warnf("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)
|
||
}
|
||
sshSigner, err := a.keyStore.GetSSHSigner(ctx, ca)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
params := services.UserCertParams{
|
||
CASigner: sshSigner,
|
||
PublicUserKey: req.publicKey,
|
||
Username: req.user.GetName(),
|
||
Impersonator: req.impersonator,
|
||
AllowedLogins: allowedLogins,
|
||
TTL: sessionTTL,
|
||
Roles: req.checker.RoleNames(),
|
||
CertificateFormat: certificateFormat,
|
||
PermitPortForwarding: req.checker.CanPortForward(),
|
||
PermitAgentForwarding: req.checker.CanForwardAgents(),
|
||
PermitX11Forwarding: req.checker.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,
|
||
CertificateExtensions: req.checker.CertificateExtensions(),
|
||
AllowedResourceIDs: requestedResourcesStr,
|
||
ConnectionDiagnosticID: req.connectionDiagnosticID,
|
||
PrivateKeyPolicy: attestedKeyPolicy,
|
||
DeviceID: req.deviceExtensions.DeviceID,
|
||
DeviceAssetTag: req.deviceExtensions.AssetTag,
|
||
DeviceCredentialID: req.deviceExtensions.CredentialID,
|
||
}
|
||
signedSSHCert, err := a.GenerateUserCert(params)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
kubeGroups, kubeUsers, err := req.checker.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)
|
||
}
|
||
// Only validate/default kubernetes cluster name for the current teleport
|
||
// cluster. If this cert is targeting a trusted teleport cluster, leave all
|
||
// the kubernetes cluster validation up to them.
|
||
if req.routeToCluster == clusterName {
|
||
req.kubernetesCluster, err = kubeutils.CheckOrSetKubeCluster(a.closeCtx, a, req.kubernetesCluster, clusterName)
|
||
if err != nil {
|
||
if !trace.IsNotFound(err) {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
log.Debug("Failed setting default kubernetes cluster for user login (user did not provide a cluster); leaving KubernetesCluster extension in the TLS certificate empty")
|
||
}
|
||
}
|
||
|
||
// See which database names and users this user is allowed to use.
|
||
dbNames, dbUsers, err := req.checker.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 := req.checker.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 := req.checker.CheckAzureIdentities(sessionTTL, req.overrideRoleTTL)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
// Enumerate allowed GCP service accounts.
|
||
gcpAccounts, err := req.checker.CheckGCPServiceAccounts(sessionTTL, req.overrideRoleTTL)
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
identity := tlsca.Identity{
|
||
Username: req.user.GetName(),
|
||
Impersonator: req.impersonator,
|
||
Groups: req.checker.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,
|
||
PublicAddr: req.appPublicAddr,
|
||
ClusterName: req.appClusterName,
|
||
Name: req.appName,
|
||
AWSRoleARN: req.awsRoleARN,
|
||
AzureIdentity: req.azureIdentity,
|
||
GCPServiceAccount: req.gcpServiceAccount,
|
||
},
|
||
TeleportCluster: clusterName,
|
||
RouteToDatabase: tlsca.RouteToDatabase{
|
||
ServiceName: req.dbService,
|
||
Protocol: req.dbProtocol,
|
||
Username: req.dbUser,
|
||
Database: req.dbName,
|
||
},
|
||
DatabaseNames: dbNames,
|
||
DatabaseUsers: dbUsers,
|
||
MFAVerified: req.mfaVerified,
|
||
PreviousIdentityExpires: req.previousIdentityExpires,
|
||
LoginIP: req.loginIP,
|
||
PinnedIP: pinnedIP,
|
||
AWSRoleARNs: roleARNs,
|
||
AzureIdentities: azureIdentities,
|
||
GCPServiceAccounts: gcpAccounts,
|
||
ActiveRequests: req.activeRequests.AccessRequests,
|
||
DisallowReissue: req.disallowReissue,
|
||
Renewable: req.renewable,
|
||
Generation: req.generation,
|
||
AllowedResourceIDs: req.checker.GetAllowedResourceIDs(),
|
||
PrivateKeyPolicy: attestedKeyPolicy,
|
||
ConnectionDiagnosticID: req.connectionDiagnosticID,
|
||
DeviceExtensions: tlsca.DeviceExtensions{
|
||
DeviceID: req.deviceExtensions.DeviceID,
|
||
AssetTag: req.deviceExtensions.AssetTag,
|
||
CredentialID: req.deviceExtensions.CredentialID,
|
||
},
|
||
}
|
||
|
||
var signedTLSCert []byte
|
||
notAfter := a.clock.Now().UTC().Add(sessionTTL)
|
||
// generate TLS certificate if the signing CA isn't OpenSSH CA, as
|
||
// OpenSSH CA doesn't have any TLS keypairs
|
||
if caType != types.OpenSSHCA {
|
||
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: cryptoPubKey,
|
||
Subject: subject,
|
||
NotAfter: notAfter,
|
||
}
|
||
signedTLSCert, err = tlsCA.GenerateCertificate(certRequest)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
eventIdentity := identity.GetEventIdentity()
|
||
eventIdentity.Expires = notAfter
|
||
if err := a.emitter.EmitAuditEvent(a.closeCtx, &apievents.CertificateCreate{
|
||
Metadata: apievents.Metadata{
|
||
Type: events.CertificateCreateEvent,
|
||
Code: events.CertificateCreateCode,
|
||
},
|
||
CertificateType: events.CertificateTypeUser,
|
||
Identity: &eventIdentity,
|
||
}); err != nil {
|
||
log.WithError(err).Warn("Failed to emit certificate create event.")
|
||
}
|
||
|
||
// 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)
|
||
|
||
return certs, nil
|
||
}
|
||
|
||
type verifyLocksForUserCertsReq struct {
|
||
checker services.AccessChecker
|
||
|
||
// 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
|
||
}
|
||
|
||
// verifyLocksForUserCerts verifies if any locks are in place before issuing new
|
||
// user certificates.
|
||
func (a *Server) verifyLocksForUserCerts(req verifyLocksForUserCertsReq) error {
|
||
checker := req.checker
|
||
lockingMode := checker.LockingMode(req.defaultMode)
|
||
|
||
lockTargets := []types.LockTarget{
|
||
{User: req.username},
|
||
{MFADevice: req.mfaVerified},
|
||
{Device: req.deviceID},
|
||
}
|
||
lockTargets = append(lockTargets,
|
||
services.RolesToLockTargets(checker.RoleNames())...,
|
||
)
|
||
lockTargets = append(lockTargets,
|
||
services.AccessRequestsToLockTargets(req.activeAccessRequests)...,
|
||
)
|
||
|
||
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
|
||
}
|
||
|
||
// 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(username string, authenticateFn func() error) error {
|
||
user, err := a.Services.GetUser(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.RecoveryAttemptLockExpires.After(a.clock.Now().UTC()) {
|
||
log.Debugf("%v exceeds %v failed account recovery attempts, locked until %v",
|
||
user.GetName(), defaults.MaxAccountRecoveryAttempts, apiutils.HumanTimeFormat(status.RecoveryAttemptLockExpires))
|
||
|
||
err := trace.AccessDenied(MaxFailedAttemptsErrMsg)
|
||
return trace.WithField(err, ErrFieldKeyUserMaxedAttempts, true)
|
||
}
|
||
if status.LockExpires.After(a.clock.Now().UTC()) {
|
||
log.Debugf("%v exceeds %v failed login attempts, locked until %v",
|
||
user.GetName(), defaults.MaxLoginAttempts, apiutils.HumanTimeFormat(status.LockExpires))
|
||
|
||
err := trace.AccessDenied(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 {
|
||
log.Error(trace.DebugReport(err))
|
||
return trace.Wrap(fnErr)
|
||
}
|
||
loginAttempts, err := a.GetUserLoginAttempts(username)
|
||
if err != nil {
|
||
log.Error(trace.DebugReport(err))
|
||
return trace.Wrap(fnErr)
|
||
}
|
||
if !services.LastFailed(defaults.MaxLoginAttempts, loginAttempts) {
|
||
log.Debugf("%v user has less than %v failed login attempts", username, defaults.MaxLoginAttempts)
|
||
return trace.Wrap(fnErr)
|
||
}
|
||
lockUntil := a.clock.Now().UTC().Add(defaults.AccountLockInterval)
|
||
log.Debug(fmt.Sprintf("%v exceeds %v failed login attempts, locked until %v",
|
||
username, defaults.MaxLoginAttempts, apiutils.HumanTimeFormat(lockUntil)))
|
||
user.SetLocked(lockUntil, "user has exceeded maximum failed login attempts")
|
||
err = a.UpsertUser(user)
|
||
if err != nil {
|
||
log.Error(trace.DebugReport(err))
|
||
return trace.Wrap(fnErr)
|
||
}
|
||
|
||
retErr := trace.AccessDenied(MaxFailedAttemptsErrMsg)
|
||
return trace.WithField(retErr, ErrFieldKeyUserMaxedAttempts, true)
|
||
}
|
||
|
||
// PreAuthenticatedSignIn is for MFA authentication methods where the password
|
||
// is already checked before issuing the second factor challenge
|
||
func (a *Server) PreAuthenticatedSignIn(ctx context.Context, user string, identity tlsca.Identity) (types.WebSession, error) {
|
||
accessInfo, err := services.AccessInfoFromLocalIdentity(identity, a)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
sess, err := a.NewWebSession(ctx, types.NewWebSessionRequest{
|
||
User: user,
|
||
Roles: accessInfo.Roles,
|
||
Traits: accessInfo.Traits,
|
||
AccessRequests: identity.ActiveRequests,
|
||
RequestedResourceIDs: accessInfo.AllowedResourceIDs,
|
||
})
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if err := a.upsertWebSession(ctx, user, sess); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return sess.WithoutSecrets(), nil
|
||
}
|
||
|
||
// CreateAuthenticateChallenge implements AuthService.CreateAuthenticateChallenge.
|
||
func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) {
|
||
var username string
|
||
var passwordless bool
|
||
|
||
switch req.GetRequest().(type) {
|
||
case *proto.CreateAuthenticateChallengeRequest_UserCredentials:
|
||
username = req.GetUserCredentials().GetUsername()
|
||
|
||
if err := a.WithUserLock(username, func() error {
|
||
return a.checkPasswordWOToken(username, req.GetUserCredentials().GetPassword())
|
||
}); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
case *proto.CreateAuthenticateChallengeRequest_RecoveryStartTokenID:
|
||
token, err := a.GetUserToken(ctx, req.GetRecoveryStartTokenID())
|
||
if err != nil {
|
||
log.Error(trace.DebugReport(err))
|
||
return nil, trace.AccessDenied("invalid token")
|
||
}
|
||
|
||
if err := a.verifyUserToken(token, UserTokenTypeRecoveryStart); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
username = token.GetUser()
|
||
|
||
case *proto.CreateAuthenticateChallengeRequest_Passwordless:
|
||
passwordless = true // Allows empty username.
|
||
|
||
default: // unset or CreateAuthenticateChallengeRequest_ContextUser.
|
||
var err error
|
||
username, err = authz.GetClientUsername(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
}
|
||
|
||
challenges, err := a.mfaAuthChallenge(ctx, username, passwordless)
|
||
if err != nil {
|
||
log.Error(trace.DebugReport(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) {
|
||
token, err := a.GetUserToken(ctx, req.GetTokenID())
|
||
if err != nil {
|
||
log.Error(trace.DebugReport(err))
|
||
return nil, trace.AccessDenied("invalid token")
|
||
}
|
||
|
||
allowedTokenTypes := []string{
|
||
UserTokenTypePrivilege,
|
||
UserTokenTypePrivilegeException,
|
||
UserTokenTypeResetPassword,
|
||
UserTokenTypeResetPasswordInvite,
|
||
UserTokenTypeRecoveryApproved,
|
||
}
|
||
|
||
if err := a.verifyUserToken(token, allowedTokenTypes...); err != nil {
|
||
return nil, trace.AccessDenied("invalid token")
|
||
}
|
||
|
||
regChal, err := a.createRegisterChallenge(ctx, &newRegisterChallengeRequest{
|
||
username: token.GetUser(),
|
||
token: token,
|
||
deviceType: req.GetDeviceType(),
|
||
deviceUsage: req.GetDeviceUsage(),
|
||
})
|
||
|
||
return regChal, 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:
|
||
otpKey, otpOpts, err := a.newTOTPKey(req.username)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
challenge := &proto.TOTPRegisterChallenge{
|
||
Secret: otpKey.Secret(),
|
||
Issuer: otpKey.Issuer(),
|
||
PeriodSeconds: uint32(otpOpts.Period),
|
||
Algorithm: otpOpts.Algorithm.String(),
|
||
Digits: uint32(otpOpts.Digits.Length()),
|
||
Account: otpKey.AccountName(),
|
||
}
|
||
|
||
if req.token != nil {
|
||
secrets, err := a.createTOTPUserTokenSecrets(ctx, req.token, otpKey)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
challenge.QRCode = secrets.GetQRCode()
|
||
}
|
||
|
||
return &proto.MFARegisterChallenge{Request: &proto.MFARegisterChallenge_TOTP{TOTP: challenge}}, 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: wanlib.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 {
|
||
log.Error(trace.DebugReport(err))
|
||
return nil, trace.AccessDenied("invalid token")
|
||
}
|
||
|
||
if err := a.verifyUserToken(token, 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 {
|
||
token, err := a.GetUserToken(ctx, req.GetTokenID())
|
||
if err != nil {
|
||
log.Error(trace.DebugReport(err))
|
||
return trace.AccessDenied("invalid token")
|
||
}
|
||
|
||
if err := a.verifyUserToken(token, UserTokenTypeRecoveryApproved, UserTokenTypePrivilege); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
_, err = a.deleteMFADeviceSafely(ctx, token.GetUser(), req.GetDeviceName())
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
// deleteMFADeviceSafely deletes the user's mfa device while preventing users from deleting their last device
|
||
// for clusters that require second factors, which prevents users from being locked out of their account.
|
||
func (a *Server) deleteMFADeviceSafely(ctx context.Context, user, deviceName string) (*types.MFADevice, error) {
|
||
devs, err := a.Services.GetMFADevices(ctx, user, true)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
authPref, err := a.GetAuthPreference(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
kindToSF := map[string]constants.SecondFactorType{
|
||
fmt.Sprintf("%T", &types.MFADevice_Totp{}): constants.SecondFactorOTP,
|
||
fmt.Sprintf("%T", &types.MFADevice_U2F{}): constants.SecondFactorWebauthn,
|
||
fmt.Sprintf("%T", &types.MFADevice_Webauthn{}): constants.SecondFactorWebauthn,
|
||
}
|
||
sfToCount := make(map[constants.SecondFactorType]int)
|
||
var knownDevices int
|
||
var deviceToDelete *types.MFADevice
|
||
|
||
// Find the device to delete and count devices.
|
||
for _, d := range devs {
|
||
// Match device by name or ID.
|
||
if d.GetName() == deviceName || d.Id == deviceName {
|
||
deviceToDelete = d
|
||
}
|
||
|
||
sf, ok := kindToSF[fmt.Sprintf("%T", d.Device)]
|
||
switch {
|
||
case !ok && d == deviceToDelete:
|
||
return nil, trace.NotImplemented("cannot delete device of type %T", d.Device)
|
||
case !ok:
|
||
log.Warnf("Ignoring unknown device with type %T in deletion.", d.Device)
|
||
continue
|
||
}
|
||
|
||
sfToCount[sf]++
|
||
knownDevices++
|
||
}
|
||
if deviceToDelete == nil {
|
||
return nil, trace.NotFound("MFA device %q does not exist", deviceName)
|
||
}
|
||
|
||
// Prevent users from deleting their last device for clusters that require second factors.
|
||
const minDevices = 2
|
||
switch sf := authPref.GetSecondFactor(); sf {
|
||
case constants.SecondFactorOff, constants.SecondFactorOptional: // MFA is not required, allow deletion
|
||
case constants.SecondFactorOn:
|
||
if knownDevices < minDevices {
|
||
return nil, trace.BadParameter(
|
||
"cannot delete the last MFA device for this user; add a replacement device first to avoid getting locked out")
|
||
}
|
||
case constants.SecondFactorOTP, constants.SecondFactorWebauthn:
|
||
if sfToCount[sf] < minDevices {
|
||
return nil, trace.BadParameter(
|
||
"cannot delete the last %s device for this user; add a replacement device first to avoid getting locked out", sf)
|
||
}
|
||
default:
|
||
return nil, trace.BadParameter("unexpected second factor type: %s", sf)
|
||
}
|
||
|
||
if err := a.DeleteMFADevice(ctx, user, deviceToDelete.Id); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
// Emit deleted event.
|
||
clusterName, err := a.GetClusterName()
|
||
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),
|
||
}); 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) {
|
||
privilegeToken, err := a.GetUserToken(ctx, req.GetTokenID())
|
||
if err != nil {
|
||
log.Error(trace.DebugReport(err))
|
||
return nil, trace.AccessDenied("invalid token")
|
||
}
|
||
|
||
if err := a.verifyUserToken(privilegeToken, UserTokenTypePrivilege, UserTokenTypePrivilegeException); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
dev, err := a.verifyMFARespAndAddDevice(ctx, &newMFADeviceFields{
|
||
username: privilegeToken.GetUser(),
|
||
newDeviceName: req.GetNewDeviceName(),
|
||
tokenID: privilegeToken.GetName(),
|
||
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 token.
|
||
// It is used as following:
|
||
// - TOTP:
|
||
// - look up TOTP secret stored by token ID
|
||
// - MFA:
|
||
// - look up challenge stored by token ID
|
||
// This field can be empty to use storage overrides.
|
||
tokenID string
|
||
// totpSecret is a secret shared by client and server to generate totp codes.
|
||
// Field can be empty to get secret by "tokenID".
|
||
totpSecret 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.GetSecondFactor() == constants.SecondFactorOff {
|
||
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()
|
||
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),
|
||
}); err != nil {
|
||
log.WithError(err).Warn("Failed to emit add mfa device event.")
|
||
}
|
||
|
||
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")
|
||
}
|
||
|
||
var secret string
|
||
switch {
|
||
case req.tokenID != "":
|
||
secrets, err := a.GetUserTokenSecrets(ctx, req.tokenID)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
secret = secrets.GetOTPKey()
|
||
case req.totpSecret != "":
|
||
secret = req.totpSecret
|
||
default:
|
||
return nil, trace.BadParameter("missing TOTP secret")
|
||
}
|
||
|
||
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: wanlib.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 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.AccessInfoFromLocalIdentity(identity, a)
|
||
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(req.User, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
traits = user.GetTraits()
|
||
|
||
} 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()
|
||
}
|
||
|
||
// Let session expire with the shortest expiry time.
|
||
if expiresAt.After(accessRequest.GetAccessExpiry()) {
|
||
expiresAt = accessRequest.GetAccessExpiry()
|
||
}
|
||
} 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.
|
||
user, err := a.GetUser(req.User, false)
|
||
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(user.GetRoles(), a, user.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 = user.GetRoles()
|
||
accessRequests = nil
|
||
}
|
||
|
||
sessionTTL := utils.ToTTL(a.clock, expiresAt)
|
||
sess, err := a.NewWebSession(ctx, types.NewWebSessionRequest{
|
||
User: req.User,
|
||
Roles: roles,
|
||
Traits: traits,
|
||
SessionTTL: sessionTTL,
|
||
AccessRequests: accessRequests,
|
||
RequestedResourceIDs: allowedResourceIDs,
|
||
})
|
||
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, req.User, sess); err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
return sess, nil
|
||
}
|
||
|
||
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)
|
||
}
|
||
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)
|
||
}
|
||
|
||
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.GetUser(user, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
session, err := a.CreateWebSessionFromReq(ctx, types.NewWebSessionRequest{
|
||
User: user,
|
||
Roles: u.GetRoles(),
|
||
Traits: u.GetTraits(),
|
||
LoginTime: a.clock.Now().UTC(),
|
||
})
|
||
return session, trace.Wrap(err)
|
||
}
|
||
|
||
// GenerateToken generates multi-purpose authentication token.
|
||
// Deprecated: Use CreateToken or UpdateToken.
|
||
// DELETE IN 14.0.0, replaced by methods above (strideynet).
|
||
func (a *Server) GenerateToken(ctx context.Context, req *proto.GenerateTokenRequest) (string, error) {
|
||
ttl := defaults.ProvisioningTokenTTL
|
||
if req.TTL != 0 {
|
||
ttl = req.TTL.Get()
|
||
}
|
||
expires := a.clock.Now().UTC().Add(ttl)
|
||
|
||
if req.Token == "" {
|
||
token, err := utils.CryptoRandomHex(TokenLenBytes)
|
||
if err != nil {
|
||
return "", trace.Wrap(err)
|
||
}
|
||
req.Token = token
|
||
}
|
||
|
||
token, err := types.NewProvisionToken(req.Token, req.Roles, expires)
|
||
if err != nil {
|
||
return "", trace.Wrap(err)
|
||
}
|
||
if len(req.Labels) != 0 {
|
||
meta := token.GetMetadata()
|
||
meta.Labels = req.Labels
|
||
token.SetMetadata(meta)
|
||
}
|
||
|
||
if err := a.UpsertToken(ctx, token); err != nil {
|
||
return "", trace.Wrap(err)
|
||
}
|
||
|
||
userMetadata := authz.ClientUserMetadata(ctx)
|
||
for _, role := range req.Roles {
|
||
if role == types.RoleTrustedCluster {
|
||
if err := a.emitter.EmitAuditEvent(ctx, &apievents.TrustedClusterTokenCreate{
|
||
Metadata: apievents.Metadata{
|
||
Type: events.TrustedClusterTokenCreateEvent,
|
||
Code: events.TrustedClusterTokenCreateCode,
|
||
},
|
||
UserMetadata: userMetadata,
|
||
}); err != nil {
|
||
log.WithError(err).Warn("Failed to emit trusted cluster token create event.")
|
||
}
|
||
}
|
||
}
|
||
|
||
return req.Token, nil
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// HostFQDN consists of host UUID and cluster name joined via .
|
||
func HostFQDN(hostUUID, clusterName string) string {
|
||
return fmt.Sprintf("%v.%v", hostUUID, clusterName)
|
||
}
|
||
|
||
// 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()
|
||
log.Debugf("Node %q [%v] is rate limited: %v.", req.NodeName, req.HostID, 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 generateRequestsLatencies.Observe(time.Since(start).Seconds())
|
||
|
||
generateRequestsCount.Inc()
|
||
generateRequestsCurrent.Inc()
|
||
defer generateRequestsCurrent.Dec()
|
||
|
||
clusterName, err := a.GetClusterName()
|
||
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 := tlsca.ParsePublicKeyPEM(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()) {
|
||
log.Debugf("Client sent rotation state %v, cache state is %v, using state from the DB.", req.Rotation, 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, services.HostCertParams{
|
||
CASigner: caSigner,
|
||
PublicHostKey: req.PublicSSHKey,
|
||
HostID: req.HostID,
|
||
NodeName: req.NodeName,
|
||
ClusterName: clusterName.GetClusterName(),
|
||
Role: 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: 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: append([]string{}, 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.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
|
||
}
|
||
|
||
// UnstableAssertSystemRole is not a stable part of the public API. Used by older
|
||
// instances to prove that they hold a given system role.
|
||
// DELETE IN: 12.0 (deprecated in v11, but required for back-compat with v10 clients)
|
||
func (a *Server) UnstableAssertSystemRole(ctx context.Context, req proto.UnstableSystemRoleAssertion) error {
|
||
return trace.Wrap(a.Unstable.AssertSystemRole(ctx, req))
|
||
}
|
||
|
||
func (a *Server) UnstableGetSystemRoleAssertions(ctx context.Context, serverID string, assertionID string) (proto.UnstableSystemRoleAssertionSet, 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,
|
||
}
|
||
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 {
|
||
var rsp proto.InventoryStatusSummary
|
||
if req.Connected {
|
||
a.inventory.Iter(func(handle inventory.UpstreamHandle) {
|
||
rsp.Connected = append(rsp.Connected, handle.Hello())
|
||
})
|
||
}
|
||
return rsp
|
||
}
|
||
|
||
func (a *Server) PingInventory(ctx context.Context, req proto.InventoryPingRequest) (proto.InventoryPingResponse, error) {
|
||
const pingAttempt = "ping-attempt"
|
||
const pingSuccess = "ping-success"
|
||
const maxAttempts = 16
|
||
stream, ok := a.inventory.GetControlStream(req.ServerID)
|
||
if !ok {
|
||
return proto.InventoryPingResponse{}, trace.NotFound("no control stream found for server %q", req.ServerID)
|
||
}
|
||
|
||
id := insecurerand.Uint64()
|
||
|
||
if !req.ControlLog {
|
||
// this ping doesn't pass through the control log, so just execute it immediately.
|
||
d, err := stream.Ping(ctx, id)
|
||
return proto.InventoryPingResponse{
|
||
Duration: d,
|
||
}, trace.Wrap(err)
|
||
}
|
||
|
||
// matchEntry is used to check if our log entry has been included
|
||
// in the control log.
|
||
matchEntry := func(entry types.InstanceControlLogEntry) bool {
|
||
return entry.Type == pingAttempt && entry.ID == id
|
||
}
|
||
|
||
var included bool
|
||
for i := 1; i <= maxAttempts; i++ {
|
||
stream.VisitInstanceState(func(ref inventory.InstanceStateRef) (update inventory.InstanceStateUpdate) {
|
||
// check if we've already successfully included the ping entry
|
||
if ref.LastHeartbeat != nil {
|
||
if slices.IndexFunc(ref.LastHeartbeat.GetControlLog(), matchEntry) >= 0 {
|
||
included = true
|
||
return
|
||
}
|
||
}
|
||
|
||
// if the entry pending already, we just need to wait
|
||
if slices.IndexFunc(ref.QualifiedPendingControlLog, matchEntry) >= 0 {
|
||
return
|
||
}
|
||
|
||
// either this is the first iteration, or the pending control log was reset.
|
||
update.QualifiedPendingControlLog = append(update.QualifiedPendingControlLog, types.InstanceControlLogEntry{
|
||
Type: pingAttempt,
|
||
ID: id,
|
||
Time: time.Now(),
|
||
})
|
||
stream.HeartbeatInstance()
|
||
return
|
||
})
|
||
|
||
if included {
|
||
// entry appeared in control log
|
||
break
|
||
}
|
||
|
||
// pause briefly, then re-sync our state. note that this strategy is not scalable. control log usage is intended only
|
||
// for periodic operations. control-log based pings are a mechanism for testing/debugging only, hence the use of a
|
||
// simple sleep loop.
|
||
select {
|
||
case <-time.After(time.Millisecond * 100 * time.Duration(i)):
|
||
case <-stream.Done():
|
||
return proto.InventoryPingResponse{}, trace.Errorf("control stream closed during ping attempt")
|
||
case <-ctx.Done():
|
||
return proto.InventoryPingResponse{}, trace.Wrap(ctx.Err())
|
||
}
|
||
}
|
||
|
||
if !included {
|
||
return proto.InventoryPingResponse{}, trace.LimitExceeded("failed to include ping %d in control log for instance %q (max attempts exceeded)", id, req.ServerID)
|
||
}
|
||
|
||
d, err := stream.Ping(ctx, id)
|
||
if err != nil {
|
||
return proto.InventoryPingResponse{}, trace.Wrap(err)
|
||
}
|
||
|
||
stream.VisitInstanceState(func(_ inventory.InstanceStateRef) (update inventory.InstanceStateUpdate) {
|
||
update.UnqualifiedPendingControlLog = append(update.UnqualifiedPendingControlLog, types.InstanceControlLogEntry{
|
||
Type: pingSuccess,
|
||
ID: id,
|
||
Labels: map[string]string{
|
||
"duration": d.String(),
|
||
},
|
||
})
|
||
return
|
||
})
|
||
stream.HeartbeatInstance()
|
||
|
||
return proto.InventoryPingResponse{
|
||
Duration: d,
|
||
}, nil
|
||
}
|
||
|
||
// 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()
|
||
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(TokenExpiredOrNotFound)
|
||
}
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if !a.checkTokenTTL(tok) {
|
||
return nil, trace.AccessDenied(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) {
|
||
log.Warnf("Unable to delete token from backend: %v.", err)
|
||
}
|
||
}
|
||
}()
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (a *Server) DeleteToken(ctx context.Context, token string) (err error) {
|
||
tkns, err := a.GetStaticTokens()
|
||
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
|
||
func (a *Server) GetTokens(ctx context.Context, opts ...services.MarshalOption) (tokens []types.ProvisionToken, err error) {
|
||
// get node tokens:
|
||
tokens, err = a.Services.GetTokens(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
// get static tokens:
|
||
tkns, err := a.GetStaticTokens()
|
||
if err != nil && !trace.IsNotFound(err) {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
if err == nil {
|
||
tokens = append(tokens, tkns.GetStaticTokens()...)
|
||
}
|
||
// get user tokens:
|
||
userTokens, err := a.GetUserTokens(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
// 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
|
||
}
|
||
|
||
// NewWebSession creates and returns a new web session for the specified request
|
||
func (a *Server) NewWebSession(ctx context.Context, req types.NewWebSessionRequest) (types.WebSession, error) {
|
||
user, err := a.GetUser(req.User, false)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
clusterName, err := a.GetClusterName()
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
checker, err := services.NewAccessChecker(&services.AccessInfo{
|
||
Roles: req.Roles,
|
||
Traits: req.Traits,
|
||
AllowedResourceIDs: req.RequestedResourceIDs,
|
||
}, clusterName.GetClusterName(), a)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
netCfg, err := a.GetClusterNetworkingConfig(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
|
||
priv, pub, err := native.GenerateKeyPair()
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
sessionTTL := req.SessionTTL
|
||
if sessionTTL == 0 {
|
||
sessionTTL = checker.AdjustSessionTTL(apidefaults.CertDuration)
|
||
}
|
||
certs, err := a.generateUserCert(certRequest{
|
||
user: user,
|
||
ttl: sessionTTL,
|
||
publicKey: pub,
|
||
checker: checker,
|
||
traits: req.Traits,
|
||
activeRequests: services.RequestIDs{AccessRequests: req.AccessRequests},
|
||
})
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
token, err := utils.CryptoRandomHex(SessionTokenBytes)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
bearerToken, err := utils.CryptoRandomHex(SessionTokenBytes)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
bearerTokenTTL := utils.MinTTL(sessionTTL, BearerTokenTTL)
|
||
|
||
startTime := a.clock.Now()
|
||
if !req.LoginTime.IsZero() {
|
||
startTime = req.LoginTime
|
||
}
|
||
|
||
sessionSpec := types.WebSessionSpecV2{
|
||
User: req.User,
|
||
Priv: priv,
|
||
Pub: certs.SSH,
|
||
TLSCert: certs.TLS,
|
||
Expires: startTime.UTC().Add(sessionTTL),
|
||
BearerToken: bearerToken,
|
||
BearerTokenExpires: startTime.UTC().Add(bearerTokenTTL),
|
||
LoginTime: req.LoginTime,
|
||
IdleTimeout: types.Duration(netCfg.GetWebIdleTimeout()),
|
||
}
|
||
UserLoginCount.Inc()
|
||
|
||
sess, err := types.NewWebSession(token, types.KindWebSession, sessionSpec)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return sess, 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
|
||
}
|
||
|
||
func (a *Server) DeleteNamespace(namespace string) error {
|
||
ctx := context.TODO()
|
||
if namespace == apidefaults.Namespace {
|
||
return trace.AccessDenied("can't delete default namespace")
|
||
}
|
||
nodes, err := a.GetNodes(ctx, namespace)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if len(nodes) != 0 {
|
||
return trace.BadParameter("can't delete namespace %v that has %v registered nodes", namespace, len(nodes))
|
||
}
|
||
return a.Services.DeleteNamespace(namespace)
|
||
}
|
||
|
||
func (a *Server) CreateAccessRequest(ctx context.Context, req types.AccessRequest, identity tlsca.Identity) error {
|
||
now := a.clock.Now().UTC()
|
||
|
||
req.SetCreationTime(now)
|
||
|
||
// Always perform variable expansion on creation only, this ensures the
|
||
// access request that is reviewed is the same that is approved.
|
||
expandOpts := services.ExpandVars(true)
|
||
if err := services.ValidateAccessRequestForUser(ctx, a.clock, a, req, identity, expandOpts); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if req.GetDryRun() {
|
||
// Made it this far with no errors, return before creating the request
|
||
// if this is a dry run.
|
||
return nil
|
||
}
|
||
|
||
log.Debugf("Creating Access Request %v with expiry %v.", req.GetName(), req.Expiry())
|
||
|
||
if err := a.Services.CreateAccessRequest(ctx, req); err != nil {
|
||
return 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()),
|
||
RequestID: req.GetName(),
|
||
RequestState: req.GetState().String(),
|
||
Reason: req.GetRequestReason(),
|
||
})
|
||
if err != nil {
|
||
log.WithError(err).Warn("Failed to emit access request create event.")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit access request delete event.")
|
||
}
|
||
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,
|
||
}
|
||
|
||
if delegator := apiutils.GetDelegator(ctx); delegator != "" {
|
||
event.Delegator = delegator
|
||
}
|
||
|
||
if len(params.Annotations) > 0 {
|
||
annotations, err := apievents.EncodeMapStrings(params.Annotations)
|
||
if err != nil {
|
||
log.WithError(err).Debugf("Failed to encode access request annotations.")
|
||
} else {
|
||
event.Annotations = annotations
|
||
}
|
||
}
|
||
err = a.emitter.EmitAuditEvent(a.closeCtx, event)
|
||
if err != nil {
|
||
log.WithError(err).Warn("Failed to emit access request update event.")
|
||
}
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
func (a *Server) SubmitAccessReview(ctx context.Context, params types.AccessReviewSubmission) (types.AccessRequest, error) {
|
||
clusterName, err := a.GetClusterName()
|
||
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)
|
||
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,
|
||
}
|
||
|
||
if len(params.Review.Annotations) > 0 {
|
||
annotations, err := apievents.EncodeMapStrings(params.Review.Annotations)
|
||
if err != nil {
|
||
log.WithError(err).Debugf("Failed to encode access request annotations.")
|
||
} else {
|
||
event.Annotations = annotations
|
||
}
|
||
}
|
||
if err := a.emitter.EmitAuditEvent(a.closeCtx, event); err != nil {
|
||
log.WithError(err).Warn("Failed to emit access request update event.")
|
||
}
|
||
|
||
return req, nil
|
||
}
|
||
|
||
func (a *Server) GetAccessCapabilities(ctx context.Context, req types.AccessCapabilitiesRequest) (*types.AccessCapabilities, error) {
|
||
caps, err := services.CalculateAccessCapabilities(ctx, a.clock, a, req)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return caps, 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
|
||
}
|
||
|
||
// GenerateCertAuthorityCRL generates an empty CRL for the local CA of a given type.
|
||
func (a *Server) GenerateCertAuthorityCRL(ctx context.Context, caType types.CertAuthType) ([]byte, error) {
|
||
// Generate a CRL for the current cluster CA.
|
||
clusterName, err := a.GetClusterName()
|
||
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)
|
||
}
|
||
|
||
// TODO(awly): this will only create a CRL for an active signer.
|
||
// If there are multiple signers (multiple HSMs), we won't have the full CRL coverage.
|
||
// Generate a CRL per signer and return all of them separately.
|
||
|
||
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)
|
||
}
|
||
// Empty CRL valid for 1yr.
|
||
template := &x509.RevocationList{
|
||
Number: big.NewInt(1),
|
||
ThisUpdate: time.Now().Add(-1 * time.Minute), // 1 min in the past to account for clock skew.
|
||
NextUpdate: time.Now().Add(365 * 24 * time.Hour),
|
||
}
|
||
crl, err := x509.CreateRevocationList(rand.Reader, template, 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
|
||
}
|
||
}
|
||
|
||
// CreateAuditStream creates audit event stream
|
||
func (a *Server) CreateAuditStream(ctx context.Context, sid session.ID) (apievents.Stream, error) {
|
||
streamer, err := a.modeStreamer(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return streamer.CreateAuditStream(ctx, sid)
|
||
}
|
||
|
||
// ResumeAuditStream resumes the stream that has been created
|
||
func (a *Server) ResumeAuditStream(ctx context.Context, sid session.ID, uploadID string) (apievents.Stream, error) {
|
||
streamer, err := a.modeStreamer(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return streamer.ResumeAuditStream(ctx, sid, uploadID)
|
||
}
|
||
|
||
// modeStreamer creates streamer based on the event mode
|
||
func (a *Server) modeStreamer(ctx context.Context) (events.Streamer, error) {
|
||
recConfig, err := a.GetSessionRecordingConfig(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
// In sync mode, auth server forwards session control to the event log
|
||
// in addition to sending them and data events to the record storage.
|
||
if services.IsRecordSync(recConfig.GetMode()) {
|
||
return events.NewTeeStreamer(a.streamer, a.emitter), nil
|
||
}
|
||
// In async mode, clients submit session control events
|
||
// during the session in addition to writing a local
|
||
// session recording to be uploaded at the end of the session,
|
||
// so forwarding events here will result in duplicate events.
|
||
return a.streamer, nil
|
||
}
|
||
|
||
// 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 {
|
||
log.WithError(err).Warn("Failed to emit app create event.")
|
||
}
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit app update event.")
|
||
}
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit app delete event.")
|
||
}
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit database create event.")
|
||
}
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit database update event.")
|
||
}
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit database delete event.")
|
||
}
|
||
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 := 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 {
|
||
log.WithError(err).Warn("Failed to emit kube cluster create event.")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// UpdateKubernetesCluster updates an existing kubernetes cluster resource.
|
||
func (a *Server) UpdateKubernetesCluster(ctx context.Context, kubeCluster types.KubeCluster) error {
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit kube cluster update event.")
|
||
}
|
||
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 {
|
||
log.WithError(err).Warn("Failed to emit kube cluster delete event.")
|
||
}
|
||
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)
|
||
}
|
||
|
||
event, err := usagereporter.ConvertUsageEvent(req.GetEvent(), username)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
if err := a.AnonymizeAndSubmit(event); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (a *Server) isMFARequired(ctx context.Context, checker services.AccessChecker, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) {
|
||
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{Required: true}, nil
|
||
case services.MFARequiredNever:
|
||
return &proto.IsMFARequiredResponse{Required: false}, nil
|
||
}
|
||
|
||
var noMFAAccessErr, notFoundErr 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")
|
||
}
|
||
// Find the target node and check whether MFA is required.
|
||
nodes, err := a.GetNodes(ctx, apidefaults.Namespace)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
var matches []types.Server
|
||
for _, n := range nodes {
|
||
// Get the server address without port number.
|
||
addr, _, err := net.SplitHostPort(n.GetAddr())
|
||
if err != nil {
|
||
addr = n.GetAddr()
|
||
}
|
||
// Match NodeName to UUID, hostname or self-reported server address.
|
||
if n.GetName() == t.Node.Node || n.GetHostname() == t.Node.Node || addr == t.Node.Node {
|
||
matches = append(matches, n)
|
||
}
|
||
}
|
||
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{Required: false}, 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 {
|
||
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:
|
||
notFoundErr = trace.NotFound("kubernetes cluster %q not found", t.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.Wrap(notFoundErr)
|
||
}
|
||
|
||
noMFAAccessErr = checker.CheckAccess(cluster, services.AccessState{})
|
||
|
||
case *proto.IsMFARequiredRequest_Database:
|
||
notFoundErr = trace.NotFound("database service %q not found", t.Database.ServiceName)
|
||
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.Wrap(notFoundErr)
|
||
}
|
||
|
||
dbRoleMatchers := role.DatabaseRoleMatchers(
|
||
db,
|
||
t.Database.Username,
|
||
t.Database.GetDatabase(),
|
||
)
|
||
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()))
|
||
|
||
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{Required: false}, nil
|
||
}
|
||
// Errors other than ErrSessionMFARequired mean something else is wrong,
|
||
// most likely access denied.
|
||
if !errors.Is(noMFAAccessErr, services.ErrSessionMFARequired) {
|
||
if !trace.IsAccessDenied(noMFAAccessErr) {
|
||
log.WithError(noMFAAccessErr).Warn("Could not determine MFA access")
|
||
}
|
||
|
||
// 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{Required: false}, nil
|
||
}
|
||
// If we reach here, the error from AccessChecker was
|
||
// ErrSessionMFARequired.
|
||
|
||
return &proto.IsMFARequiredResponse{Required: true}, nil
|
||
}
|
||
|
||
// mfaAuthChallenge constructs an MFAAuthenticateChallenge for all MFA devices
|
||
// registered by the user.
|
||
func (a *Server) mfaAuthChallenge(ctx context.Context, user string, passwordless bool) (*proto.MFAAuthenticateChallenge, error) {
|
||
// 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()
|
||
|
||
// 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 passwordless {
|
||
if !enableWebauthn {
|
||
return nil, trace.BadParameter("passwordless requires WebAuthn")
|
||
}
|
||
webLogin := &wanlib.PasswordlessFlow{
|
||
Webauthn: webConfig,
|
||
Identity: a.Services,
|
||
}
|
||
assertion, err := webLogin.Begin(ctx)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
return &proto.MFAAuthenticateChallenge{
|
||
WebauthnChallenge: wanlib.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, enableWebauthn)
|
||
challenge := &proto.MFAAuthenticateChallenge{}
|
||
|
||
// TOTP challenge.
|
||
if enableTOTP && groupedDevs.TOTP {
|
||
challenge.TOTP = &proto.TOTPChallenge{}
|
||
}
|
||
|
||
// WebAuthn challenge.
|
||
if len(groupedDevs.Webauthn) > 0 {
|
||
webLogin := &wanlib.LoginFlow{
|
||
U2F: u2fPref,
|
||
Webauthn: webConfig,
|
||
Identity: wanlib.WithDevices(a.Services, groupedDevs.Webauthn),
|
||
}
|
||
assertion, err := webLogin.Begin(ctx, user)
|
||
if err != nil {
|
||
return nil, trace.Wrap(err)
|
||
}
|
||
challenge.WebauthnChallenge = wanlib.CredentialAssertionToProto(assertion)
|
||
}
|
||
|
||
return challenge, nil
|
||
}
|
||
|
||
type devicesByType struct {
|
||
TOTP bool
|
||
Webauthn []*types.MFADevice
|
||
}
|
||
|
||
func groupByDeviceType(devs []*types.MFADevice, groupWebauthn bool) devicesByType {
|
||
res := devicesByType{}
|
||
for _, dev := range devs {
|
||
switch dev.Device.(type) {
|
||
case *types.MFADevice_Totp:
|
||
res.TOTP = true
|
||
case *types.MFADevice_U2F:
|
||
if groupWebauthn {
|
||
res.Webauthn = append(res.Webauthn, dev)
|
||
}
|
||
case *types.MFADevice_Webauthn:
|
||
if groupWebauthn {
|
||
res.Webauthn = append(res.Webauthn, dev)
|
||
}
|
||
default:
|
||
log.Warningf("Skipping MFA device of unknown type %T.", dev.Device)
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
|
||
// validateMFAAuthResponse validates an MFA or passwordless challenge.
|
||
// Returns the device used to solve the challenge (if applicable) and the
|
||
// username.
|
||
func (a *Server) validateMFAAuthResponse(
|
||
ctx context.Context,
|
||
resp *proto.MFAAuthenticateResponse, user string, passwordless bool,
|
||
) (*types.MFADevice, string, error) {
|
||
// Sanity check user/passwordless.
|
||
if user == "" && !passwordless {
|
||
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 := wanlib.CredentialAssertionResponseFromProto(res.Webauthn)
|
||
var dev *types.MFADevice
|
||
if passwordless {
|
||
webLogin := &wanlib.PasswordlessFlow{
|
||
Webauthn: webConfig,
|
||
Identity: a.Services,
|
||
}
|
||
dev, user, err = webLogin.Finish(ctx, assertionResp)
|
||
} else {
|
||
webLogin := &wanlib.LoginFlow{
|
||
U2F: u2f,
|
||
Webauthn: webConfig,
|
||
Identity: a.Services,
|
||
}
|
||
dev, err = webLogin.Finish(ctx, user, wanlib.CredentialAssertionResponseFromProto(res.Webauthn))
|
||
}
|
||
if err != nil {
|
||
return nil, "", trace.AccessDenied("MFA response validation failed: %v", err)
|
||
}
|
||
return dev, user, nil
|
||
|
||
case *proto.MFAAuthenticateResponse_TOTP:
|
||
dev, err := a.checkOTP(user, res.TOTP.Code)
|
||
return dev, user, trace.Wrap(err)
|
||
|
||
default:
|
||
return nil, "", trace.BadParameter("unknown or missing MFAAuthenticateResponse type %T", resp.Response)
|
||
}
|
||
}
|
||
|
||
func (a *Server) upsertWebSession(ctx context.Context, user string, session types.WebSession) error {
|
||
if err := a.WebSessions().Upsert(ctx, session); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
token, err := types.NewWebToken(session.GetBearerTokenExpiryTime(), types.WebTokenSpecV3{
|
||
User: session.GetUser(),
|
||
Token: session.GetBearerToken(),
|
||
})
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err := a.WebTokens().Upsert(ctx, token); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
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 CompareAndSwap to update
|
||
// the given CA with newKeys added to the AdditionalTrustedKeys
|
||
func (a *Server) addAdditionalTrustedKeysAtomic(
|
||
ctx context.Context,
|
||
currentCA types.CertAuthority,
|
||
newKeys types.CAKeySet,
|
||
needsUpdate func(types.CertAuthority) (bool, error),
|
||
) error {
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return trace.Wrap(ctx.Err())
|
||
default:
|
||
}
|
||
updateRequired, err := needsUpdate(currentCA)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if !updateRequired {
|
||
return nil
|
||
}
|
||
|
||
newCA := currentCA.Clone()
|
||
currentKeySet := newCA.GetAdditionalTrustedKeys()
|
||
mergedKeySet := mergeKeySets(currentKeySet, newKeys)
|
||
if err := newCA.SetAdditionalTrustedKeys(mergedKeySet); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
err = a.CompareAndSwapCertAuthority(newCA, currentCA)
|
||
if err != nil && !trace.IsCompareFailed(err) {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err == nil {
|
||
// success!
|
||
return nil
|
||
}
|
||
// else trace.IsCompareFailed(err) == true (CA was concurrently updated)
|
||
|
||
currentCA, err = a.Services.GetCertAuthority(ctx, currentCA.GetID(), true)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// newKeySet generates a new sets of keys for a given CA type.
|
||
// Keep this function in sync with lib/service/suite/suite.go:NewTestCAWithConfig().
|
||
func newKeySet(ctx context.Context, keyStore *keystore.Manager, caID types.CertAuthID) (types.CAKeySet, error) {
|
||
var keySet types.CAKeySet
|
||
switch caID.Type {
|
||
case types.UserCA, types.HostCA:
|
||
sshKeyPair, err := keyStore.NewSSHKeyPair(ctx)
|
||
if err != nil {
|
||
return keySet, trace.Wrap(err)
|
||
}
|
||
tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName)
|
||
if err != nil {
|
||
return keySet, trace.Wrap(err)
|
||
}
|
||
keySet.SSH = append(keySet.SSH, sshKeyPair)
|
||
keySet.TLS = append(keySet.TLS, tlsKeyPair)
|
||
case types.DatabaseCA:
|
||
// Database CA only contains TLS cert.
|
||
tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName)
|
||
if err != nil {
|
||
return keySet, trace.Wrap(err)
|
||
}
|
||
keySet.TLS = append(keySet.TLS, tlsKeyPair)
|
||
case types.OpenSSHCA:
|
||
// OpenSSH CA only contains a SSH key pair.
|
||
sshKeyPair, err := keyStore.NewSSHKeyPair(ctx)
|
||
if err != nil {
|
||
return keySet, trace.Wrap(err)
|
||
}
|
||
keySet.SSH = append(keySet.SSH, sshKeyPair)
|
||
case types.JWTSigner:
|
||
jwtKeyPair, err := keyStore.NewJWTKeyPair(ctx)
|
||
if err != nil {
|
||
return keySet, trace.Wrap(err)
|
||
}
|
||
keySet.JWT = append(keySet.JWT, jwtKeyPair)
|
||
case types.SAMLIDPCA:
|
||
// SAML IDP CA only contains TLS certs.
|
||
tlsKeyPair, err := keyStore.NewTLSKeyPair(ctx, caID.DomainName)
|
||
if err != nil {
|
||
return keySet, trace.Wrap(err)
|
||
}
|
||
keySet.TLS = append(keySet.TLS, tlsKeyPair)
|
||
default:
|
||
return keySet, trace.BadParameter("unknown ca type: %s", caID.Type)
|
||
}
|
||
return keySet, nil
|
||
}
|
||
|
||
// 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 {
|
||
hasUsableKeys, err := a.keyStore.HasUsableAdditionalKeys(ctx, ca)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if hasUsableKeys {
|
||
// 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 keystore does not have any usable
|
||
// keys in the CA.
|
||
needsUpdate := func(ca types.CertAuthority) (bool, error) {
|
||
hasUsableKeys, err := a.keyStore.HasUsableAdditionalKeys(ctx, ca)
|
||
return !hasUsableKeys, trace.Wrap(err)
|
||
}
|
||
err = a.addAdditionalTrustedKeysAtomic(ctx, ca, newKeySet, needsUpdate)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
log.Infof("Successfully added locally usable additional trusted keys to %s CA.", ca.GetType())
|
||
return nil
|
||
}
|
||
|
||
// createSelfSignedCA creates a new self-signed CA and writes it to the
|
||
// backend, with the type and clusterName given by the argument caID.
|
||
func (a *Server) createSelfSignedCA(ctx context.Context, caID types.CertAuthID) error {
|
||
keySet, err := newKeySet(ctx, a.keyStore, caID)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
|
||
Type: caID.Type,
|
||
ClusterName: caID.DomainName,
|
||
ActiveKeys: keySet,
|
||
})
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
if err := a.CreateCertAuthority(ctx, ca); err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// deleteUnusedKeys deletes all teleport keys held in a connected HSM for this
|
||
// auth server which are not currently used in any CAs.
|
||
func (a *Server) deleteUnusedKeys(ctx context.Context) error {
|
||
clusterName, err := a.Services.GetClusterName()
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
|
||
var usedKeys [][]byte
|
||
for _, caType := range types.CertAuthTypes {
|
||
caID := types.CertAuthID{Type: caType, DomainName: clusterName.GetClusterName()}
|
||
ca, err := a.Services.GetCertAuthority(ctx, caID, true)
|
||
if err != nil {
|
||
return trace.Wrap(err)
|
||
}
|
||
for _, keySet := range []types.CAKeySet{ca.GetActiveKeys(), ca.GetAdditionalTrustedKeys()} {
|
||
for _, sshKeyPair := range keySet.SSH {
|
||
usedKeys = append(usedKeys, sshKeyPair.PrivateKey)
|
||
}
|
||
for _, tlsKeyPair := range keySet.TLS {
|
||
usedKeys = append(usedKeys, tlsKeyPair.Key)
|
||
}
|
||
for _, jwtKeyPair := range keySet.JWT {
|
||
usedKeys = append(usedKeys, jwtKeyPair.PrivateKey)
|
||
}
|
||
}
|
||
}
|
||
return trace.Wrap(a.keyStore.DeleteUnusedKeys(ctx, usedKeys))
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
const (
|
||
// BearerTokenTTL specifies standard bearer token to exist before
|
||
// it has to be renewed by the client
|
||
BearerTokenTTL = 10 * time.Minute
|
||
|
||
// TokenLenBytes is len in bytes of the invite token
|
||
TokenLenBytes = 16
|
||
|
||
// RecoveryTokenLenBytes is len in bytes of a user token for recovery.
|
||
RecoveryTokenLenBytes = 32
|
||
|
||
// SessionTokenBytes is the number of bytes of a web or application session.
|
||
SessionTokenBytes = 32
|
||
)
|
||
|
||
// githubClient is internal structure that stores Github OAuth 2client and its config
|
||
type githubClient struct {
|
||
client *oauth2.Client
|
||
config oauth2.Config
|
||
}
|
||
|
||
// oauth2ConfigsEqual returns true if the provided OAuth2 configs are equal
|
||
func oauth2ConfigsEqual(a, b oauth2.Config) bool {
|
||
if a.Credentials.ID != b.Credentials.ID {
|
||
return false
|
||
}
|
||
if a.Credentials.Secret != b.Credentials.Secret {
|
||
return false
|
||
}
|
||
if a.RedirectURL != b.RedirectURL {
|
||
return false
|
||
}
|
||
if len(a.Scope) != len(b.Scope) {
|
||
return false
|
||
}
|
||
for i := range a.Scope {
|
||
if a.Scope[i] != b.Scope[i] {
|
||
return false
|
||
}
|
||
}
|
||
if a.AuthURL != b.AuthURL {
|
||
return false
|
||
}
|
||
if a.TokenURL != b.TokenURL {
|
||
return false
|
||
}
|
||
if a.AuthMethod != b.AuthMethod {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// WithClusterCAs returns a TLS hello callback that returns a copy of the provided
|
||
// TLS config with client CAs pool of the specified cluster.
|
||
func WithClusterCAs(tlsConfig *tls.Config, ap AccessCache, currentClusterName string, log logrus.FieldLogger) func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||
return func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||
var clusterName string
|
||
var err error
|
||
if info.ServerName != "" {
|
||
// Newer clients will set SNI that encodes the cluster name.
|
||
clusterName, err = apiutils.DecodeClusterName(info.ServerName)
|
||
if err != nil {
|
||
if !trace.IsNotFound(err) {
|
||
log.Debugf("Ignoring unsupported cluster name name %q.", info.ServerName)
|
||
clusterName = ""
|
||
}
|
||
}
|
||
}
|
||
pool, totalSubjectsLen, err := DefaultClientCertPool(ap, clusterName)
|
||
if err != nil {
|
||
log.WithError(err).Errorf("Failed to retrieve client pool for %q.", clusterName)
|
||
// this falls back to the default config
|
||
return nil, nil
|
||
}
|
||
|
||
// Per https://tools.ietf.org/html/rfc5246#section-7.4.4 the total size of
|
||
// the known CA subjects sent to the client can't exceed 2^16-1 (due to
|
||
// 2-byte length encoding). The crypto/tls stack will panic if this
|
||
// happens.
|
||
//
|
||
// This usually happens on the root cluster with a very large (>500) number
|
||
// of leaf clusters. In these cases, the client cert will be signed by the
|
||
// current (root) cluster.
|
||
//
|
||
// If the number of CAs turns out too large for the handshake, drop all but
|
||
// the current cluster CA. In the unlikely case where it's wrong, the
|
||
// client will be rejected.
|
||
if totalSubjectsLen >= int64(math.MaxUint16) {
|
||
log.Debugf("Number of CAs in client cert pool is too large and cannot be encoded in a TLS handshake; this is due to a large number of trusted clusters; will use only the CA of the current cluster to validate.")
|
||
|
||
pool, _, err = DefaultClientCertPool(ap, currentClusterName)
|
||
if err != nil {
|
||
log.WithError(err).Errorf("Failed to retrieve client pool for %q.", currentClusterName)
|
||
// this falls back to the default config
|
||
return nil, nil
|
||
}
|
||
}
|
||
tlsCopy := tlsConfig.Clone()
|
||
tlsCopy.ClientCAs = pool
|
||
return tlsCopy, 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) {
|
||
return []string{
|
||
"*." + constants.APIDomain,
|
||
constants.APIDomain,
|
||
}
|
||
}
|
||
return nil
|
||
}
|