Files
teleport/lib/service/certreloader.go
T
Zac Bergquist 2ae9d770fc Refactor tctl's dependencies (#22693)
* Move configuration from lib/service to lib/service/servicecfg

The new servicecfg package will hold only configuration for services.
This will allow other packages (like tctl and tsh) to depend on
servicecfg without pulling in all of lib/service (which has a number
of platform-specific details).

This is the first step towards being able to build tctl for Windows.

* Move PAM and BPF config into servicecfg

This breaks a compile-time dependency on BPF/PAM for tctl.
2023-03-09 17:48:36 +00:00

160 lines
5.0 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 service
import (
"context"
"crypto/tls"
"crypto/x509"
"sync"
"time"
"github.com/gravitational/trace"
log "github.com/sirupsen/logrus"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/service/servicecfg"
)
// CertReloaderConfig contains the configuration of the certificate reloader.
type CertReloaderConfig struct {
// KeyPairs are the key and certificate pairs that the proxy will load.
KeyPairs []servicecfg.KeyPairPath
// KeyPairsReloadInterval is the interval between attempts to reload
// x509 key pairs. If set to 0, then periodic reloading is disabled.
KeyPairsReloadInterval time.Duration
}
// CertReloader periodically reloads a list of cert key-pair paths.
// This allows new certificates to be used without a full reload of Teleport.
type CertReloader struct {
*log.Entry
// cfg is the certificate reloader configuration.
cfg CertReloaderConfig
// certificates is the list of certificates loaded.
certificates []tls.Certificate
// mu protects the list of certificates.
mu sync.RWMutex
}
// NewCertReloader initializes a new certificate reloader.
func NewCertReloader(cfg CertReloaderConfig) *CertReloader {
return &CertReloader{
Entry: log.WithFields(log.Fields{
trace.Component: teleport.Component(teleport.ComponentProxy, "certreloader"),
}),
cfg: cfg,
}
}
// Run tries to load certificates and then spawns the certificate reloader.
func (c *CertReloader) Run(ctx context.Context) error {
// Synchronously load initially configured certificates.
if err := c.loadCertificates(); err != nil {
return trace.Wrap(err)
}
// Do not reload certificates if the interval is 0 (the default).
if c.cfg.KeyPairsReloadInterval == 0 {
return nil
}
// Spawn the certificate reloader.
go func() {
c.Infof("Starting periodic reloading of certificate key pairs every %s.", c.cfg.KeyPairsReloadInterval)
defer func() {
c.Info("Stopped periodic reloading of certificate key pairs.")
}()
t := time.NewTicker(c.cfg.KeyPairsReloadInterval)
defer t.Stop()
for {
select {
case <-t.C:
if err := c.loadCertificates(); err != nil {
c.WithError(err).Warn("Failed to load certificates")
}
case <-ctx.Done():
return
}
}
}()
return nil
}
// loadCertificates loads certificate keys pairs.
// It returns an error if any of the certificate key pairs fails to load.
// If any of the key pairs fails to load, none of the certificates are updated.
func (c *CertReloader) loadCertificates() error {
certs := make([]tls.Certificate, 0, len(c.cfg.KeyPairs))
for _, pair := range c.cfg.KeyPairs {
c.Debugf("Loading TLS certificate %v and key %v.", pair.Certificate, pair.PrivateKey)
certificate, err := tls.LoadX509KeyPair(pair.Certificate, pair.PrivateKey)
if err != nil {
// If one certificate fails to load, then no certificate is updated.
return trace.WrapWithMessage(err, "TLS certificate %v and key %v failed to load", pair.Certificate, pair.PrivateKey)
}
// Parse the end entity cert and add it to certificate.Leaf.
// With this, the SupportsCertificate call doesn't have to
// parse it on every GetCertificate call.
leaf, err := x509.ParseCertificate(certificate.Certificate[0])
if err != nil {
// If one certificate fails to load, then no certificate is updated.
return trace.WrapWithMessage(err, "TLS certificate %v and key %v failed to be parsed", pair.Certificate, pair.PrivateKey)
}
certificate.Leaf = leaf
certs = append(certs, certificate)
}
c.mu.Lock()
defer c.mu.Unlock()
c.certificates = certs
return nil
}
// GetCertificate is compatible with tls.Config.GetCertificate, allowing
// the CertReloader to be a source of certificates for a TLS listener.
// Certificate selection logic is the same as getCertificate in crypto/tls:
// https://github.com/golang/go/tree/f64c2a2ce5dc859315047184e310879dcf747d53/src/crypto/tls/common.go#L1075-L1117
func (c *CertReloader) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.certificates) == 0 {
return nil, trace.NotFound("found no certificates in cert reloader")
}
if len(c.certificates) == 1 {
// There's only one choice, so no point doing any work.
return &c.certificates[0], nil
}
// If there's more than one choice, select the first certificate that matches.
for _, cert := range c.certificates {
if err := clientHello.SupportsCertificate(&cert); err == nil {
return &cert, nil
}
}
// If nothing matches, return the first certificate.
return &c.certificates[0], nil
}