diff --git a/lib/auth/join/join.go b/lib/auth/join/join.go index d5d6b8f3b96c..f29d3a635472 100644 --- a/lib/auth/join/join.go +++ b/lib/auth/join/join.go @@ -74,8 +74,10 @@ type RegisterParams struct { // ID is identity ID ID state.IdentityID // AuthServers is a list of auth servers to dial + // Ignored if AuthClient is provided. AuthServers []utils.NetAddr // ProxyServer is a proxy server to dial + // Ignored if AuthClient is provided. ProxyServer utils.NetAddr // AdditionalPrincipals is a list of additional principals to dial AdditionalPrincipals []string @@ -86,12 +88,16 @@ type RegisterParams struct { // PublicSSHKey is a server's public SSH key to sign PublicSSHKey []byte // CipherSuites is a list of cipher suites to use for TLS client connection + // Ignored if AuthClient is provided. CipherSuites []uint16 // CAPins are the SKPI hashes of the CAs used to verify the Auth Server. + // Ignored if AuthClient is provided. CAPins []string // CAPath is the path to the CA file. + // Ignored if AuthClient is provided. CAPath string // GetHostCredentials is a client that can fetch host credentials. + // Ignored if AuthClient is provided. GetHostCredentials HostCredentials // Clock specifies the time provider. Will be used to override the time anchor // for TLS certificate verification. @@ -105,8 +111,10 @@ type RegisterParams struct { // AzureParams is the parameters specific to the azure join method. AzureParams AzureParams // CircuitBreakerConfig defines how the circuit breaker should behave. + // Ignored if AuthClient is provided. CircuitBreakerConfig breaker.Config // FIPS means FedRAMP/FIPS 140-2 compliant configuration was requested. + // Ignored if AuthClient is provided. FIPS bool // IDToken is a token retrieved from a workload identity provider for // certain join types e.g GitHub, Google. @@ -116,7 +124,13 @@ type RegisterParams struct { // It should not be specified for non-bot registrations. Expires *time.Time // Insecure trusts the certificates from the Auth Server or Proxy during registration without verification. + // Ignored if AuthClient is provided. Insecure bool + // AuthClient allows an existing client with a connection to the auth + // server to be used for the registration process. If specified, then the + // Register method will not attempt to dial, and many other parameters + // may be ignored. + AuthClient AuthJoinClient } func (r *RegisterParams) checkAndSetDefaults() error { @@ -132,6 +146,11 @@ func (r *RegisterParams) checkAndSetDefaults() error { } func (r *RegisterParams) verifyAuthOrProxyAddress() error { + // If AuthClient is provided we do not need addresses to dial with. + if r.AuthClient != nil { + return nil + } + haveAuthServers := len(r.AuthServers) > 0 haveProxyServer := !r.ProxyServer.IsEmpty() @@ -210,6 +229,19 @@ func Register(ctx context.Context, params RegisterParams) (certs *proto.Certs, e } } + // If an explicit AuthClient has been provided, we want to go straight to + // using that rather than trying both proxy and auth dialing. + if params.AuthClient != nil { + log.Info("Attempting registration with existing auth client.") + certs, err := registerThroughAuthClient(ctx, token, params, params.AuthClient) + if err != nil { + log.WithError(err).Error("Registration with existing auth client failed.") + return nil, trace.Wrap(err) + } + log.Info("Successfully registered with existing auth client.") + return certs, nil + } + type registerMethod struct { call func(ctx context.Context, token string, params RegisterParams) (*proto.Certs, error) desc string @@ -372,6 +404,26 @@ func registerThroughAuth( } defer client.Close() + certs, err = registerThroughAuthClient(ctx, token, params, client) + if err != nil { + return nil, trace.Wrap(err) + } + return certs, nil +} + +// AuthJoinClient is a client that allows access to the Auth Servers join +// service and RegisterUsingToken method for the purposes of joining. +type AuthJoinClient interface { + joinServiceClient + RegisterUsingToken(ctx context.Context, req *types.RegisterUsingTokenRequest) (*proto.Certs, error) +} + +func registerThroughAuthClient( + ctx context.Context, + token string, + params RegisterParams, + client AuthJoinClient, +) (certs *proto.Certs, err error) { switch params.JoinMethod { // IAM and Azure methods use unique gRPC endpoints case types.JoinMethodIAM: diff --git a/lib/auth/join/join_test.go b/lib/auth/join/join_test.go index 8ccbdaba79c2..185caa69d0a6 100644 --- a/lib/auth/join/join_test.go +++ b/lib/auth/join/join_test.go @@ -24,11 +24,14 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh" + "github.com/gravitational/teleport/api/client/proto" headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" "github.com/gravitational/teleport/api/types" @@ -279,3 +282,48 @@ func newBotToken(t *testing.T, tokenName, botName string, role types.SystemRole, require.NoError(t, err, "could not create bot token") return token } + +type authJoinClientMock struct { + AuthJoinClient + registerUsingToken func(ctx context.Context, req *types.RegisterUsingTokenRequest) (*proto.Certs, error) +} + +func (a *authJoinClientMock) RegisterUsingToken(ctx context.Context, req *types.RegisterUsingTokenRequest) (*proto.Certs, error) { + return a.registerUsingToken(ctx, req) +} + +// TestRegisterWithAuthClient is a unit test to validate joining using a +// auth client supplied via RegisterParams +func TestRegisterWithAuthClient(t *testing.T) { + ctx := context.Background() + expectedCerts := &proto.Certs{ + SSH: []byte("ssh-cert"), + } + expectedToken := "test-token" + expectedRole := types.RoleBot + called := false + m := &authJoinClientMock{ + registerUsingToken: func(ctx context.Context, req *types.RegisterUsingTokenRequest) (*proto.Certs, error) { + assert.Empty(t, cmp.Diff( + req, + &types.RegisterUsingTokenRequest{ + Token: expectedToken, + Role: expectedRole, + }, + )) + called = true + return expectedCerts, nil + }, + } + + gotCerts, gotErr := Register(ctx, RegisterParams{ + Token: expectedToken, + ID: state.IdentityID{ + Role: expectedRole, + }, + AuthClient: m, + }) + require.NoError(t, gotErr) + assert.True(t, called) + assert.Equal(t, expectedCerts, gotCerts) +} diff --git a/lib/tbot/bot/destination.go b/lib/tbot/bot/destination.go index a52a96b08ef9..ab1d6b431010 100644 --- a/lib/tbot/bot/destination.go +++ b/lib/tbot/bot/destination.go @@ -57,6 +57,11 @@ type Destination interface { // as YAML including the type header. MarshalYAML() (interface{}, error) + // IsPersistent indicates whether this destination is persistent. + // This is true for most production destinations, but will be false for + // Nop or Memory destinations. + IsPersistent() bool + // Stringer so that Destination's implements fmt.Stringer which allows for // better logging. fmt.Stringer diff --git a/lib/tbot/config/config.go b/lib/tbot/config/config.go index 6ffae7a328c0..49d70b02dc18 100644 --- a/lib/tbot/config/config.go +++ b/lib/tbot/config/config.go @@ -250,12 +250,6 @@ func (conf *OnboardingConfig) HasToken() bool { return conf.TokenValue != "" } -// RenewableJoinMethod indicates that certificate renewal should be used with -// this join method rather than rejoining each time. -func (conf *OnboardingConfig) RenewableJoinMethod() bool { - return conf.JoinMethod == types.JoinMethodToken -} - // SetToken stores the value for --token or auth_token in the config // // In the case of the token value pointing to a file, this allows us to diff --git a/lib/tbot/config/destination_directory.go b/lib/tbot/config/destination_directory.go index feb5b6d7830c..df1fae33fba1 100644 --- a/lib/tbot/config/destination_directory.go +++ b/lib/tbot/config/destination_directory.go @@ -257,3 +257,7 @@ func (dm *DestinationDirectory) MarshalYAML() (interface{}, error) { type raw DestinationDirectory return withTypeHeader((*raw)(dm), DestinationDirectoryType) } + +func (dd *DestinationDirectory) IsPersistent() bool { + return true +} diff --git a/lib/tbot/config/destination_kubernetes_secret.go b/lib/tbot/config/destination_kubernetes_secret.go index e0a7740944cb..57eb78fc78cc 100644 --- a/lib/tbot/config/destination_kubernetes_secret.go +++ b/lib/tbot/config/destination_kubernetes_secret.go @@ -287,3 +287,7 @@ func (dks *DestinationKubernetesSecret) MarshalYAML() (interface{}, error) { type raw DestinationKubernetesSecret return withTypeHeader((*raw)(dks), DestinationKubernetesSecretType) } + +func (dks *DestinationKubernetesSecret) IsPersistent() bool { + return true +} diff --git a/lib/tbot/config/destination_memory.go b/lib/tbot/config/destination_memory.go index c357e98b9998..ce4f3a9151e9 100644 --- a/lib/tbot/config/destination_memory.go +++ b/lib/tbot/config/destination_memory.go @@ -120,3 +120,7 @@ func (dm *DestinationMemory) MarshalYAML() (interface{}, error) { type raw DestinationMemory return withTypeHeader((*raw)(dm), DestinationMemoryType) } + +func (dm *DestinationMemory) IsPersistent() bool { + return false +} diff --git a/lib/tbot/config/destination_nop.go b/lib/tbot/config/destination_nop.go index 7b7eb10d5002..e5682ab4e6a7 100644 --- a/lib/tbot/config/destination_nop.go +++ b/lib/tbot/config/destination_nop.go @@ -79,3 +79,7 @@ func (dm *DestinationNop) MarshalYAML() (interface{}, error) { type raw DestinationNop return withTypeHeader((*raw)(dm), DestinationNopType) } + +func (dm *DestinationNop) IsPersistent() bool { + return false +} diff --git a/lib/tbot/identity/identity.go b/lib/tbot/identity/identity.go index 82f0daaf83fd..bfcea0c13b50 100644 --- a/lib/tbot/identity/identity.go +++ b/lib/tbot/identity/identity.go @@ -106,6 +106,8 @@ type Identity struct { // ClusterName is a name of host's cluster determined from the // x509 certificate. ClusterName string + // TLSIdentity is the parsed TLS identity based on the X509 certificate. + TLSIdentity *tlsca.Identity } // LoadIdentityParams contains parameters beyond proto.Certs needed to load a @@ -171,7 +173,7 @@ func ReadIdentityFromStore(params *LoadIdentityParams, certs *proto.Certs) (*Ide return nil, trace.Wrap(err, "parsing ssh identity") } - clusterName, x509Cert, tlsCert, tlsCAPool, err := ParseTLSIdentity( + clusterName, tlsIdent, x509Cert, tlsCert, tlsCAPool, err := ParseTLSIdentity( params.PrivateKeyBytes, certs.TLS, certs.TLSCACerts, ) if err != nil { @@ -196,41 +198,54 @@ func ReadIdentityFromStore(params *LoadIdentityParams, certs *proto.Certs) (*Ide X509Cert: x509Cert, TLSCert: tlsCert, TLSCAPool: tlsCAPool, + TLSIdentity: tlsIdent, }, nil } // ParseTLSIdentity reads TLS identity from key pair func ParseTLSIdentity( keyBytes []byte, certBytes []byte, caCertsBytes [][]byte, -) (clusterName string, x509Cert *x509.Certificate, tlsCert *tls.Certificate, certPool *x509.CertPool, err error) { +) ( + clusterName string, + tlsIdentity *tlsca.Identity, + x509Cert *x509.Certificate, + tlsCert *tls.Certificate, + certPool *x509.CertPool, + err error, +) { x509Cert, err = tlsca.ParseCertificatePEM(certBytes) if err != nil { - return "", nil, nil, nil, trace.Wrap(err, "parsing certificate") + return "", nil, nil, nil, nil, trace.Wrap(err, "parsing certificate") } if len(x509Cert.Issuer.Organization) == 0 { - return "", nil, nil, nil, trace.BadParameter("certificate missing CA organization") + return "", nil, nil, nil, nil, trace.BadParameter("certificate missing CA organization") } clusterName = x509Cert.Issuer.Organization[0] if clusterName == "" { - return "", nil, nil, nil, trace.BadParameter("certificate missing cluster name") + return "", nil, nil, nil, nil, trace.BadParameter("certificate missing cluster name") } certPool = x509.NewCertPool() for j := range caCertsBytes { parsedCert, err := tlsca.ParseCertificatePEM(caCertsBytes[j]) if err != nil { - return "", nil, nil, nil, trace.Wrap(err, "parsing CA certificate") + return "", nil, nil, nil, nil, trace.Wrap(err, "parsing CA certificate") } certPool.AddCert(parsedCert) } cert, err := keys.X509KeyPair(certBytes, keyBytes) if err != nil { - return "", nil, nil, nil, trace.Wrap(err, "parse private key") + return "", nil, nil, nil, nil, trace.Wrap(err, "parse private key") } - return clusterName, x509Cert, &cert, certPool, nil + tlsIdent, err := tlsca.FromSubject(x509Cert.Subject, x509Cert.NotAfter) + if err != nil { + return "", nil, nil, nil, nil, trace.Wrap(err, "parse tls identity") + } + + return clusterName, tlsIdent, x509Cert, &cert, certPool, nil } // parseSSHIdentity reads identity from initialized keypair diff --git a/lib/tbot/service_bot_identity.go b/lib/tbot/service_bot_identity.go index 55286b805d7d..28e909f5d947 100644 --- a/lib/tbot/service_bot_identity.go +++ b/lib/tbot/service_bot_identity.go @@ -94,20 +94,27 @@ func hasTokenChanged(configTokenBytes, identityBytes []byte) bool { } // loadIdentityFromStore attempts to load a persisted identity from a store. -// It checks this loaded identity against the configured onboarding profile -// and ignores the loaded identity if there has been a configuration change. -func (s *identityService) loadIdentityFromStore(ctx context.Context, store bot.Destination) (*identity.Identity, error) { +// It then checks: +// - This identity against the configured onboarding profile. +// - This identity is not expired +// If any checks fail, it will not return the loaded identity. +func (s *identityService) loadIdentityFromStore(ctx context.Context, store bot.Destination) *identity.Identity { ctx, span := tracer.Start(ctx, "identityService/loadIdentityFromStore") defer span.End() - s.log.InfoContext(ctx, "Loading existing bot identity from store.", "store", store) + s.log.InfoContext(ctx, "Loading existing bot identity from store", "store", store) loadedIdent, err := identity.LoadIdentity(ctx, store, identity.BotKinds()...) if err != nil { if trace.IsNotFound(err) { - s.log.InfoContext(ctx, "No existing bot identity found in store. Bot will join using configured token.") - return nil, nil + s.log.InfoContext(ctx, "No existing bot identity found in store") + return nil } else { - return nil, trace.Wrap(err) + s.log.WarnContext( + ctx, + "Failed to load existing bot identity from store", + "error", err, + ) + return nil } } @@ -118,77 +125,99 @@ func (s *identityService) loadIdentityFromStore(ctx context.Context, store bot.D sha := sha256.Sum256([]byte(token)) configTokenHashBytes := []byte(hex.EncodeToString(sha[:])) if hasTokenChanged(loadedIdent.TokenHashBytes, configTokenHashBytes) { - s.log.InfoContext(ctx, "Bot identity loaded from store does not match configured token. Bot will fetch identity using configured token.") + s.log.InfoContext(ctx, "Bot identity loaded from store does not match configured token") // If the token has changed, do not return the loaded // identity. - return nil, nil + return nil } } else { // we failed to get the newly configured token to compare to, // we'll assume the last good credentials written to disk should // still be used. - s.log.ErrorContext(ctx, "There was an error loading the configured token. Bot identity loaded from store will be tried.", "error", err) + s.log.WarnContext( + ctx, + "There was an error loading the configured token to compare to existing identity. Identity loaded from store will be tried", + "error", err, + ) } } - s.log.InfoContext(ctx, "Loaded existing bot identity from store.", "identity", describeTLSIdentity(ctx, s.log, loadedIdent)) - return loadedIdent, nil + s.log.InfoContext( + ctx, + "Loaded existing bot identity from store", + "identity", describeTLSIdentity(ctx, s.log, loadedIdent), + ) + + now := time.Now().UTC() + if now.After(loadedIdent.X509Cert.NotAfter) { + s.log.WarnContext( + ctx, + "Identity loaded from store is expired, it will not be used", + "not_after", loadedIdent.X509Cert.NotAfter.Format(time.RFC3339), + "current_time", now.Format(time.RFC3339), + ) + return nil + } else if now.Before(loadedIdent.X509Cert.NotBefore) { + s.log.WarnContext( + ctx, + "Identity loaded from store is not yet valid, it will not be used. Confirm that the system time is correct", + "not_before", loadedIdent.X509Cert.NotBefore.Format(time.RFC3339), + "current_time", now.Format(time.RFC3339), + ) + return nil + } + + return loadedIdent } -// Initialize attempts to load an existing identity from the bot's storage. -// If an identity is found, it is checked against the configured onboarding -// settings. It is then renewed and persisted. +// Initialize sets up the bot identity at startup. This process has a few +// steps to it. +// +// First, we attempt to load an existing identity from the configured storage. +// This is ignored if we know that the onboarding settings have changed. +// +// If the identity is found, and seems valid, we attempt to renew using this +// identity to give us a fresh set of certificates. // -// If no identity is found, or the identity is no longer valid, a new identity -// is requested using the configured onboarding settings. +// If there is no identity, or the identity is invalid, we'll join using the +// configured onboarding settings. func (s *identityService) Initialize(ctx context.Context) error { ctx, span := tracer.Start(ctx, "identityService/Initialize") defer span.End() - s.log.InfoContext(ctx, "Initializing bot identity.") - var loadedIdent *identity.Identity - var err error - if s.cfg.Onboarding.RenewableJoinMethod() { - // Nil, nil will be returned if no identity can be found in store or - // the identity in the store is no longer relevant. - loadedIdent, err = s.loadIdentityFromStore(ctx, s.cfg.Storage.Destination) - if err != nil { - return trace.Wrap(err) + s.log.InfoContext(ctx, "Initializing bot identity") + // nil will be returned if no identity can be found in store or + // the identity in the store is no longer relevant or valid. + loadedIdent := s.loadIdentityFromStore(ctx, s.cfg.Storage.Destination) + if loadedIdent == nil { + if !s.cfg.Onboarding.HasToken() { + // There's no loaded identity to work with, and they've not + // configured a token to use to request an identity :( + return trace.BadParameter( + "no existing identity found on disk or join token configured", + ) } + s.log.InfoContext( + ctx, + "Bot was unable to load a valid existing identity from the store, will attempt to join using configured token", + ) } + var err error var newIdentity *identity.Identity - if s.cfg.Onboarding.RenewableJoinMethod() && loadedIdent != nil { - // If using a renewable join method and we loaded an identity, let's - // immediately renew it so we know that after initialisation we have the - // full certificate TTL. - if err := checkIdentity(ctx, s.log, loadedIdent); err != nil { - return trace.Wrap(err) - } - facade := identity.NewFacade(s.cfg.FIPS, s.cfg.Insecure, loadedIdent) - authClient, err := clientForFacade(ctx, s.log, s.cfg, facade, s.resolver) - if err != nil { - return trace.Wrap(err) - } - defer authClient.Close() - newIdentity, err = botIdentityFromAuth( - ctx, s.log, loadedIdent, authClient, s.cfg.CertificateTTL, - ) + if loadedIdent != nil { + newIdentity, err = renewIdentity(ctx, s.log, s.cfg, s.resolver, loadedIdent) if err != nil { - return trace.Wrap(err) + return trace.Wrap(err, "renewing identity using loaded identity") } - } else if s.cfg.Onboarding.HasToken() { - // If using a non-renewable join method, or we weren't able to load an - // identity from the store, let's get a new identity using the - // configured token. - newIdentity, err = botIdentityFromToken(ctx, s.log, s.cfg) + } else { + // TODO(noah): If the above renewal fails, do we want to try joining + // instead? Is there a sane amount of times to try renewing before + // giving up and rejoining? + newIdentity, err = botIdentityFromToken(ctx, s.log, s.cfg, nil) if err != nil { - return trace.Wrap(err) + return trace.Wrap(err, "joining with token") } - } else { - // There's no loaded identity to work with, and they've not configured - // a token to use to request an identity :( - return trace.BadParameter("no existing identity found on disk or join token configured") } s.log.InfoContext(ctx, "Fetched new bot identity", "identity", describeTLSIdentity(ctx, s.log, newIdentity)) @@ -264,32 +293,9 @@ func (s *identityService) renew( return trace.Wrap(err, "Cannot write to destination %s, aborting.", botDestination) } - var newIdentity *identity.Identity - var err error - if s.cfg.Onboarding.RenewableJoinMethod() { - // When using a renewable join method, we use GenerateUserCerts to - // request a new certificate using our current identity. - // We explicitly create a new client here to ensure that the latest - // identity is being used! - facade := identity.NewFacade(s.cfg.FIPS, s.cfg.Insecure, currentIdentity) - authClient, err := clientForFacade(ctx, s.log, s.cfg, facade, s.resolver) - if err != nil { - return trace.Wrap(err, "creating auth client") - } - defer authClient.Close() - newIdentity, err = botIdentityFromAuth( - ctx, s.log, currentIdentity, authClient, s.cfg.CertificateTTL, - ) - if err != nil { - return trace.Wrap(err, "renewing identity with existing identity") - } - } else { - // When using the non-renewable join methods, we rejoin each time rather - // than using certificate renewal. - newIdentity, err = botIdentityFromToken(ctx, s.log, s.cfg) - if err != nil { - return trace.Wrap(err, "renewing identity with token") - } + newIdentity, err := renewIdentity(ctx, s.log, s.cfg, s.resolver, currentIdentity) + if err != nil { + return trace.Wrap(err, "renewing identity") } s.log.InfoContext(ctx, "Fetched new bot identity", "identity", describeTLSIdentity(ctx, s.log, newIdentity)) @@ -303,6 +309,44 @@ func (s *identityService) renew( return nil } +func renewIdentity( + ctx context.Context, + log *slog.Logger, + botCfg *config.BotConfig, + resolver reversetunnelclient.Resolver, + oldIdentity *identity.Identity, +) (*identity.Identity, error) { + ctx, span := tracer.Start(ctx, "renewIdentity") + defer span.End() + // Explicitly create a new client - this guarantees that requests will be + // made with the most recent identity and that a connection associated with + // an old identity will not be used. + facade := identity.NewFacade(botCfg.FIPS, botCfg.Insecure, oldIdentity) + authClient, err := clientForFacade(ctx, log, botCfg, facade, resolver) + if err != nil { + return nil, trace.Wrap(err, "creating auth client") + } + defer authClient.Close() + + if oldIdentity.TLSIdentity.Renewable { + // When using a renewable join method, we use GenerateUserCerts to + // request a new certificate using our current identity. + newIdentity, err := botIdentityFromAuth( + ctx, log, oldIdentity, authClient, botCfg.CertificateTTL, + ) + if err != nil { + return nil, trace.Wrap(err, "renewing identity using GenerateUserCert") + } + return newIdentity, nil + } + + newIdentity, err := botIdentityFromToken(ctx, log, botCfg, authClient) + if err != nil { + return nil, trace.Wrap(err, "renewing identity using Register") + } + return newIdentity, nil +} + // botIdentityFromAuth uses an existing identity to request a new from the auth // server using GenerateUserCerts. This only works for renewable join types. func botIdentityFromAuth( @@ -356,7 +400,16 @@ func botIdentityFromAuth( // botIdentityFromToken uses a join token to request a bot identity from an auth // server using auth.Register. -func botIdentityFromToken(ctx context.Context, log *slog.Logger, cfg *config.BotConfig) (*identity.Identity, error) { +// +// The authClient parameter is optional - if provided - this will be used for +// the request. This saves the overhead of trying to create a new client as +// part of the join process and allows us to preserve the bot instance id. +func botIdentityFromToken( + ctx context.Context, + log *slog.Logger, + cfg *config.BotConfig, + authClient *authclient.Client, +) (*identity.Identity, error) { _, span := tracer.Start(ctx, "botIdentityFromToken") defer span.End() @@ -378,16 +431,21 @@ func botIdentityFromToken(ctx context.Context, log *slog.Logger, cfg *config.Bot ID: state.IdentityID{ Role: types.RoleBot, }, - PublicTLSKey: tlsPublicKey, - PublicSSHKey: sshPublicKey, + PublicTLSKey: tlsPublicKey, + PublicSSHKey: sshPublicKey, + JoinMethod: cfg.Onboarding.JoinMethod, + Expires: &expires, + + // Below options are effectively ignored if AuthClient is not-nil + Insecure: cfg.Insecure, CAPins: cfg.Onboarding.CAPins, CAPath: cfg.Onboarding.CAPath, - GetHostCredentials: client.HostCredentials, - JoinMethod: cfg.Onboarding.JoinMethod, - Expires: &expires, FIPS: cfg.FIPS, + GetHostCredentials: client.HostCredentials, CipherSuites: cfg.CipherSuites(), - Insecure: cfg.Insecure, + } + if authClient != nil { + params.AuthClient = authClient } addr, addrKind := cfg.Address() diff --git a/lib/tbot/tbot.go b/lib/tbot/tbot.go index 152a233d0736..a13b9ef5abc2 100644 --- a/lib/tbot/tbot.go +++ b/lib/tbot/tbot.go @@ -548,6 +548,13 @@ func (b *Bot) preRunChecks(ctx context.Context) (_ func() error, err error) { return unlock, trace.Wrap(err) } + if !store.IsPersistent() { + b.log.WarnContext( + ctx, + "Bot is configured with a non-persistent storage destination. If the bot is running in a non-ephemeral environment, this will impact the ability to provide a long-lived bot instance identity", + ) + } + return unlock, nil } @@ -576,42 +583,6 @@ func checkDestinations(ctx context.Context, cfg *config.BotConfig) error { return nil } -// checkIdentity performs basic startup checks on an identity and loudly warns -// end users if it is unlikely to work. -func checkIdentity(ctx context.Context, log *slog.Logger, ident *identity.Identity) error { - var validAfter time.Time - var validBefore time.Time - - if ident.X509Cert != nil { - validAfter = ident.X509Cert.NotBefore - validBefore = ident.X509Cert.NotAfter - } else if ident.SSHCert != nil { - validAfter = time.Unix(int64(ident.SSHCert.ValidAfter), 0) - validBefore = time.Unix(int64(ident.SSHCert.ValidBefore), 0) - } else { - return trace.BadParameter("identity is invalid and contains no certificates") - } - - now := time.Now().UTC() - if now.After(validBefore) { - log.WarnContext( - ctx, - "Identity has expired. The renewal is likely to fail", - "expires", validBefore.Format(time.RFC3339), - "current_time", now.Format(time.RFC3339), - ) - } else if now.Before(validAfter) { - log.WarnContext( - ctx, - "Identity is not yet valid. Confirm that the system time is correct", - "valid_after", validAfter.Format(time.RFC3339), - "current_time", now.Format(time.RFC3339), - ) - } - - return nil -} - // clientForFacade creates a new auth client from the given // facade. Note that depending on the connection address given, this may // attempt to connect via the proxy and therefore requires both SSH and TLS diff --git a/lib/tbot/tbot_test.go b/lib/tbot/tbot_test.go index 52e074fbf73d..ebe10213c5aa 100644 --- a/lib/tbot/tbot_test.go +++ b/lib/tbot/tbot_test.go @@ -498,11 +498,7 @@ func tlsIdentFromDest(ctx context.Context, t *testing.T, dest bot.Destination) * require.NoError(t, err) hostCABytes, err := dest.Read(ctx, config.HostCAPath) require.NoError(t, err) - _, x509Cert, _, _, err := identity.ParseTLSIdentity(keyBytes, certBytes, [][]byte{hostCABytes}) - require.NoError(t, err) - tlsIdent, err := tlsca.FromSubject( - x509Cert.Subject, x509Cert.NotAfter, - ) + _, tlsIdent, _, _, _, err := identity.ParseTLSIdentity(keyBytes, certBytes, [][]byte{hostCABytes}) require.NoError(t, err) return tlsIdent }