mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-25 00:21:10 +02:00
186 lines
5.3 KiB
Go
186 lines
5.3 KiB
Go
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package tls
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"io/ioutil"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/talos-systems/talos/internal/pkg/constants"
|
|
"github.com/talos-systems/talos/internal/pkg/grpc/gen"
|
|
"github.com/talos-systems/talos/pkg/userdata"
|
|
)
|
|
|
|
// CertificateProvider describes an interface by which TLS certificates may be managed
|
|
type CertificateProvider interface {
|
|
|
|
// GetCertificate returns the current certificate matching the given client request
|
|
GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error)
|
|
|
|
// UpdateCertificate updates the stored certificate for the given client request
|
|
UpdateCertificate(h *tls.ClientHelloInfo, cert *tls.Certificate) error
|
|
}
|
|
|
|
type singleCertificateProvider struct {
|
|
sync.RWMutex
|
|
cert *tls.Certificate
|
|
|
|
updateHooks []func(newCert *tls.Certificate)
|
|
}
|
|
|
|
func (p *singleCertificateProvider) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
if p == nil {
|
|
return nil, errors.New("no provider")
|
|
}
|
|
|
|
p.RLock()
|
|
defer p.RUnlock()
|
|
return p.cert, nil
|
|
}
|
|
|
|
func (p *singleCertificateProvider) UpdateCertificate(h *tls.ClientHelloInfo, cert *tls.Certificate) error {
|
|
p.Lock()
|
|
p.cert = cert
|
|
p.Unlock()
|
|
|
|
for _, f := range p.updateHooks {
|
|
f(cert)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type userDataCertificateProvider struct {
|
|
data *userdata.OSSecurity
|
|
}
|
|
|
|
func (p *userDataCertificateProvider) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
cert, err := tls.X509KeyPair(p.data.Identity.Crt, p.data.Identity.Key)
|
|
return &cert, err
|
|
}
|
|
|
|
func (p *userDataCertificateProvider) UpdateCertificate(h *tls.ClientHelloInfo, cert *tls.Certificate) error {
|
|
// No-op
|
|
return nil
|
|
}
|
|
|
|
type renewingFileCertificateProvider struct {
|
|
singleCertificateProvider
|
|
|
|
certFile string
|
|
|
|
// For now, this is using the complete userdata object. It should probably
|
|
// be pared down to just what is necessary, later. However, the current TLS
|
|
// generator code requires the whole thing... so we do, too.
|
|
data *userdata.UserData
|
|
|
|
g *gen.Generator
|
|
}
|
|
|
|
// NewRenewingFileCertificateProvider returns a new CertificateProvider which
|
|
// manages and updates its certificates from trustd, storing a cache file copy.
|
|
//
|
|
// TODO: the flow here is a bit wonky, but it should be fixable after we change
|
|
// to have the default be ephemeral node certificates. Until then, however, we
|
|
// are doing a dance between the userdata-stored cert, the filesystem-cached
|
|
// cert, and the memory-cached cert.
|
|
func NewRenewingFileCertificateProvider(ctx context.Context, data *userdata.UserData) (CertificateProvider, error) {
|
|
g, err := gen.NewGenerator(data, constants.TrustdPort)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to create TLS generator")
|
|
}
|
|
|
|
p := &renewingFileCertificateProvider{
|
|
g: g,
|
|
data: data,
|
|
certFile: constants.NodeCertFile,
|
|
}
|
|
|
|
if err = p.loadInitialCert(); err != nil {
|
|
return nil, errors.Wrap(err, "failed to load initial certificate")
|
|
}
|
|
|
|
go p.manageUpdates(ctx)
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func (p *renewingFileCertificateProvider) loadInitialCert() error {
|
|
|
|
// TODO: eventually, we will reverse this priority, and have the override
|
|
// come from the userdata. For now, however, we use the local file to
|
|
// override the userdata, because we _always_ have userdata certs, and the
|
|
// userdata is intended to be immutable.
|
|
|
|
data, err := ioutil.ReadFile(p.certFile)
|
|
if err != nil || len(data) == 0 {
|
|
// If we cannot read the cert from the file, then we use the userdata-supplied one
|
|
data = p.data.Security.OS.Identity.Crt
|
|
}
|
|
|
|
cert, err := tls.X509KeyPair(data, p.data.Security.OS.Identity.Key)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse cert and key into a TLS Certificate")
|
|
}
|
|
|
|
return p.UpdateCertificate(nil, &cert)
|
|
}
|
|
|
|
func (p *renewingFileCertificateProvider) manageUpdates(ctx context.Context) {
|
|
var nextRenewal = constants.NodeCertRenewalInterval
|
|
|
|
for ctx.Err() == nil {
|
|
if c, _ := p.GetCertificate(nil); c != nil { // nolint: errcheck
|
|
if len(c.Certificate) > 0 {
|
|
cert, err := x509.ParseCertificate(c.Certificate[0])
|
|
if err == nil {
|
|
nextRenewal = time.Until(cert.NotAfter) / 2
|
|
} else {
|
|
log.Println("failed to parse current leaf certificate")
|
|
}
|
|
} else {
|
|
log.Println("current leaf certificate not found")
|
|
}
|
|
} else {
|
|
log.Println("certificate not found")
|
|
}
|
|
|
|
if nextRenewal > constants.NodeCertRenewalInterval {
|
|
nextRenewal = constants.NodeCertRenewalInterval
|
|
}
|
|
|
|
select {
|
|
case <-time.After(nextRenewal):
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
if err := p.renewCert(); err != nil {
|
|
log.Println("failed to renew certificate:", err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *renewingFileCertificateProvider) renewCert() error {
|
|
if err := p.g.Identity(p.data); err != nil {
|
|
return errors.Wrap(err, "failed to renew certificate")
|
|
}
|
|
|
|
// TODO: updating the cert using the generator automatically stores the new
|
|
// cert to userdata. Therefore, we need to pull that cert out in order to
|
|
// update the CertificateProvider's cache of it
|
|
cert, err := tls.X509KeyPair(p.data.Security.OS.Identity.Crt, p.data.Security.OS.Identity.Key)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to parse cert and key into a TLS Certificate")
|
|
}
|
|
|
|
return p.UpdateCertificate(nil, &cert)
|
|
}
|