proxy-pool/internal/controlplane/tlsreload/certificate.go

62 lines
1.8 KiB
Go

// Package tlsreload provides last-known-good leaf certificate reloads for the
// low-frequency control-plane TLS handshakes.
package tlsreload
import (
"crypto/tls"
"errors"
"sync"
)
var ErrInvalidCertificatePaths = errors.New("invalid TLS certificate paths")
// CertificateProvider reloads a certificate/key pair for each new handshake.
// Kubernetes projected volumes update file pairs atomically, but retaining the
// last valid pair also bridges short writer or filesystem visibility gaps.
type CertificateProvider struct {
certificateFile string
keyFile string
mu sync.RWMutex
last tls.Certificate
}
func NewCertificateProvider(certificateFile, keyFile string) (*CertificateProvider, error) {
if certificateFile == "" || keyFile == "" {
return nil, ErrInvalidCertificatePaths
}
certificate, err := tls.LoadX509KeyPair(certificateFile, keyFile)
if err != nil {
return nil, err
}
return &CertificateProvider{certificateFile: certificateFile, keyFile: keyFile, last: certificate}, nil
}
func (provider *CertificateProvider) ServerCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return provider.certificate()
}
func (provider *CertificateProvider) ClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
return provider.certificate()
}
func (provider *CertificateProvider) certificate() (*tls.Certificate, error) {
if provider == nil {
return nil, ErrInvalidCertificatePaths
}
certificate, err := tls.LoadX509KeyPair(provider.certificateFile, provider.keyFile)
if err == nil {
provider.mu.Lock()
provider.last = certificate
provider.mu.Unlock()
return &certificate, nil
}
provider.mu.RLock()
fallback := provider.last
provider.mu.RUnlock()
if len(fallback.Certificate) == 0 {
return nil, err
}
return &fallback, nil
}