mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 05:04:33 +08:00
* Add file based locking to tbot * Add test for destination locking and clarify lock error * Fix import weirdness * Rename Lock -> TryLock * Add warning on DestinationDirectory.TryLock re: ACLs
473 lines
12 KiB
Go
473 lines
12 KiB
Go
/*
|
|
Copyright 2022 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 tbot
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/client/webclient"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/lib/auth"
|
|
"github.com/gravitational/teleport/lib/tbot/config"
|
|
"github.com/gravitational/teleport/lib/tbot/identity"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
type Bot struct {
|
|
cfg *config.BotConfig
|
|
log logrus.FieldLogger
|
|
reloadChan chan struct{}
|
|
|
|
// These are protected by getter/setters with mutex locks
|
|
mu sync.Mutex
|
|
_client auth.ClientI
|
|
_ident *identity.Identity
|
|
_authPong *proto.PingResponse
|
|
_proxyPong *webclient.PingResponse
|
|
_cas map[types.CertAuthType][]types.CertAuthority
|
|
started bool
|
|
}
|
|
|
|
func New(cfg *config.BotConfig, log logrus.FieldLogger, reloadChan chan struct{}) *Bot {
|
|
if log == nil {
|
|
log = utils.NewLogger()
|
|
}
|
|
|
|
return &Bot{
|
|
cfg: cfg,
|
|
log: log,
|
|
reloadChan: reloadChan,
|
|
|
|
_cas: map[types.CertAuthType][]types.CertAuthority{},
|
|
}
|
|
}
|
|
|
|
// Config returns the current bot config
|
|
func (b *Bot) Config() *config.BotConfig {
|
|
return b.cfg
|
|
}
|
|
|
|
// Client retrieves the current auth client.
|
|
func (b *Bot) Client() auth.ClientI {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
return b._client
|
|
}
|
|
|
|
func (b *Bot) setClient(client auth.ClientI) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
// Make sure the previous client is closed.
|
|
if b._client != nil {
|
|
_ = b._client.Close()
|
|
}
|
|
|
|
b._client = client
|
|
}
|
|
|
|
func (b *Bot) ident() *identity.Identity {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
return b._ident
|
|
}
|
|
|
|
func (b *Bot) setIdent(ident *identity.Identity) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
b._ident = ident
|
|
}
|
|
|
|
func (b *Bot) markStarted() error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
if b.started {
|
|
return trace.BadParameter("bot has already been started")
|
|
}
|
|
b.started = true
|
|
|
|
return nil
|
|
}
|
|
|
|
// certAuthorities returns cached CAs of the given type.
|
|
func (b *Bot) certAuthorities(caType types.CertAuthType) []types.CertAuthority {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
return b._cas[caType]
|
|
}
|
|
|
|
// clearCertAuthorities purges the CA cache. This should be run at least as
|
|
// frequently as CAs are rotated.
|
|
func (b *Bot) clearCertAuthorities() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
b._cas = map[types.CertAuthType][]types.CertAuthority{}
|
|
}
|
|
|
|
// GetCertAuthorities returns the possibly cached CAs of the given type and
|
|
// requests them from the server if unavailable.
|
|
func (b *Bot) GetCertAuthorities(ctx context.Context, caType types.CertAuthType) ([]types.CertAuthority, error) {
|
|
if cas := b.certAuthorities(caType); len(cas) > 0 {
|
|
return cas, nil
|
|
}
|
|
|
|
cas, err := b.Client().GetCertAuthorities(ctx, caType, false)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
b._cas[caType] = cas
|
|
return cas, nil
|
|
}
|
|
|
|
// authPong returns the last ping response from the auth server. It may be nil
|
|
// if no ping has succeeded.
|
|
func (b *Bot) authPong() *proto.PingResponse {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
return b._authPong
|
|
}
|
|
|
|
// AuthPing pings the auth server and returns the (possibly cached) response.
|
|
func (b *Bot) AuthPing(ctx context.Context) (*proto.PingResponse, error) {
|
|
if authPong := b.authPong(); authPong != nil {
|
|
return authPong, nil
|
|
}
|
|
|
|
pong, err := b.Client().Ping(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b._authPong = &pong
|
|
|
|
return &pong, nil
|
|
}
|
|
|
|
// proxyPong returns the last proxy ping response. It may be nil if no proxy
|
|
// ping has succeeded.
|
|
func (b *Bot) proxyPong() *webclient.PingResponse {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
return b._proxyPong
|
|
}
|
|
|
|
// ProxyPing returns a (possibly cached) ping response from the Teleport proxy.
|
|
// Note that it relies on the auth server being configured with a sane proxy
|
|
// public address.
|
|
func (b *Bot) ProxyPing(ctx context.Context) (*webclient.PingResponse, error) {
|
|
if proxyPong := b.proxyPong(); proxyPong != nil {
|
|
return proxyPong, nil
|
|
}
|
|
|
|
// Note: this relies on the auth server's proxy address. We could
|
|
// potentially support some manual parameter here in the future if desired.
|
|
authPong, err := b.AuthPing(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
proxyPong, err := webclient.Ping(&webclient.Config{
|
|
Context: ctx,
|
|
ProxyAddr: authPong.ProxyPublicAddr,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b._proxyPong = proxyPong
|
|
|
|
return proxyPong, nil
|
|
}
|
|
|
|
func (b *Bot) Run(ctx context.Context) error {
|
|
if err := b.markStarted(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
unlock, err := b.initialize(ctx)
|
|
defer func() {
|
|
if unlock != nil {
|
|
if err := unlock(); err != nil {
|
|
b.log.WithError(err).Warn("Failed to release lock. Future starts of tbot may fail.")
|
|
}
|
|
}
|
|
}()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Maintain a context that we can cancel if the bot is running in one shot.
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
eg, egCtx := errgroup.WithContext(ctx)
|
|
eg.Go(func() error {
|
|
return trace.Wrap(b.caRotationLoop(egCtx))
|
|
})
|
|
eg.Go(func() error {
|
|
err := b.renewLoop(egCtx)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// If `renewLoop` exits with nil, the bot is running in "one-shot", so
|
|
// we should indicate to other long-running processes that they can
|
|
// finish up.
|
|
cancel()
|
|
return nil
|
|
})
|
|
|
|
return eg.Wait()
|
|
}
|
|
|
|
// initialize returns an unlock function which must be deferred.
|
|
func (b *Bot) initialize(ctx context.Context) (func() error, error) {
|
|
if b.cfg.AuthServer == "" {
|
|
return nil, trace.BadParameter(
|
|
"an auth or proxy server must be set via --auth-server or configuration",
|
|
)
|
|
}
|
|
|
|
// First, try to make sure all destinations are usable.
|
|
if err := checkDestinations(b.cfg); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Start by loading the bot's primary destination.
|
|
dest, err := b.cfg.Storage.GetDestination()
|
|
if err != nil {
|
|
return nil, trace.Wrap(
|
|
err, "could not read bot storage destination from config",
|
|
)
|
|
}
|
|
|
|
// Now attempt to lock the destination so we have sole use of it
|
|
unlock, err := dest.TryLock()
|
|
if err != nil {
|
|
if errors.Is(err, utils.ErrUnsuccessfulLockTry) {
|
|
return unlock, trace.WrapWithMessage(err, "Failed to acquire exclusive lock for tbot destination directory - is tbot already running?")
|
|
}
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
|
|
var authClient auth.ClientI
|
|
|
|
fetchNewIdentity := true
|
|
// First, attempt to load an identity from storage.
|
|
ident, err := identity.LoadIdentity(dest, identity.BotKinds()...)
|
|
if err == nil {
|
|
if b.cfg.Onboarding != nil && b.cfg.Onboarding.HasToken() {
|
|
// try to grab the token to see if it's changed, as we'll need to fetch a new identity if it has
|
|
if token, err := b.cfg.Onboarding.Token(); err == nil {
|
|
sha := sha256.Sum256([]byte(token))
|
|
configTokenHashBytes := []byte(hex.EncodeToString(sha[:]))
|
|
|
|
fetchNewIdentity = hasTokenChanged(ident.TokenHashBytes, configTokenHashBytes)
|
|
} else {
|
|
// we failed to get the token, we'll continue on trying to use the existing identity
|
|
b.log.WithError(err).Error("There was an error loading the token")
|
|
|
|
fetchNewIdentity = false
|
|
|
|
b.log.Info("Using the last good identity")
|
|
}
|
|
}
|
|
|
|
if !fetchNewIdentity {
|
|
identStr, err := describeTLSIdentity(ident)
|
|
if err != nil {
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
|
|
b.log.Infof("Successfully loaded bot identity, %s", identStr)
|
|
|
|
if err := b.checkIdentity(ident); err != nil {
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
|
|
if b.cfg.Onboarding != nil {
|
|
b.log.Warn("Note: onboarding config ignored as identity was loaded from persistent storage")
|
|
}
|
|
|
|
authClient, err = b.AuthenticatedUserClientFromIdentity(ctx, ident)
|
|
if err != nil {
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if fetchNewIdentity {
|
|
if ident != nil {
|
|
// If ident is set here, we detected a token change above.
|
|
b.log.Warnf("Detected a token change, will attempt to fetch a new identity.")
|
|
} else if trace.IsNotFound(err) {
|
|
// This is _probably_ a fresh start, so we'll log the true error
|
|
// and try to fetch a fresh identity.
|
|
b.log.Debugf("Identity %s is not found or empty and could not be loaded, will start from scratch: %+v", dest, err)
|
|
} else {
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
|
|
// Verify we can write to the destination.
|
|
if err := identity.VerifyWrite(dest); err != nil {
|
|
return unlock, trace.Wrap(
|
|
err, "Could not write to destination %s, aborting.", dest,
|
|
)
|
|
}
|
|
|
|
// Get first identity
|
|
ident, err = b.getIdentityFromToken()
|
|
if err != nil {
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
|
|
b.log.Debug("Attempting first connection using initial auth client")
|
|
authClient, err = b.AuthenticatedUserClientFromIdentity(ctx, ident)
|
|
if err != nil {
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
|
|
// Attempt a request to make sure our client works.
|
|
if _, err := authClient.Ping(ctx); err != nil {
|
|
return unlock, trace.Wrap(err, "unable to communicate with auth server")
|
|
}
|
|
|
|
identStr, err := describeTLSIdentity(ident)
|
|
if err != nil {
|
|
return unlock, trace.Wrap(err)
|
|
}
|
|
b.log.Infof("Successfully generated new bot identity, %s", identStr)
|
|
|
|
b.log.Debugf("Storing new bot identity to %s", dest)
|
|
if err := identity.SaveIdentity(ident, dest, identity.BotKinds()...); err != nil {
|
|
return unlock, trace.Wrap(
|
|
err, "unable to save generated identity back to destination",
|
|
)
|
|
}
|
|
}
|
|
|
|
b.setClient(authClient)
|
|
b.setIdent(ident)
|
|
|
|
return unlock, nil
|
|
}
|
|
|
|
func hasTokenChanged(configTokenBytes, identityBytes []byte) bool {
|
|
if len(configTokenBytes) == 0 || len(identityBytes) == 0 {
|
|
return false
|
|
}
|
|
|
|
return !bytes.Equal(identityBytes, configTokenBytes)
|
|
}
|
|
|
|
// checkDestinations checks all destinations and tries to create any that
|
|
// don't already exist.
|
|
func checkDestinations(cfg *config.BotConfig) error {
|
|
// Note: This is vaguely problematic as we don't recommend that users
|
|
// store renewable certs under the same user as end-user certs. That said,
|
|
// - if the destination was properly created via tbot init this is a no-op
|
|
// - if users intend to follow that advice but miss a step, it should fail
|
|
// due to lack of permissions
|
|
storage, err := cfg.Storage.GetDestination()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// TODO: consider warning if ownership of all destintions is not expected.
|
|
|
|
// Note: no subdirs to init for bot's internal storage.
|
|
if err := storage.Init([]string{}); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
for _, dest := range cfg.Destinations {
|
|
destImpl, err := dest.GetDestination()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
subdirs, err := dest.ListSubdirectories()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if err := destImpl.Init(subdirs); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkIdentity performs basic startup checks on an identity and loudly warns
|
|
// end users if it is unlikely to work.
|
|
func (b *Bot) checkIdentity(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) {
|
|
b.log.Errorf(
|
|
"Identity has expired. The renewal is likely to fail. (expires: %s, current time: %s)",
|
|
validBefore.Format(time.RFC3339),
|
|
now.Format(time.RFC3339),
|
|
)
|
|
} else if now.Before(validAfter) {
|
|
b.log.Warnf(
|
|
"Identity is not yet valid. Confirm that the system time is correct. (valid after: %s, current time: %s)",
|
|
validAfter.Format(time.RFC3339),
|
|
now.Format(time.RFC3339),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|