Skip to content

Commit

Permalink
Merge pull request kubernetes#22208 from deads2k/cert-dynamic-reload
Browse files Browse the repository at this point in the history
UPSTREAM: 00000: add dynamic certificate reloading

Origin-commit: aec40a4ced4ec3e881ffc6f27f16e1e6d39bbedc
  • Loading branch information
k8s-publishing-bot committed Mar 4, 2019
2 parents 332e75d + 8b2d967 commit 6ff2356
Show file tree
Hide file tree
Showing 61 changed files with 1,659 additions and 792 deletions.
12 changes: 4 additions & 8 deletions staging/src/k8s.io/apiserver/pkg/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package server

import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
Expand Down Expand Up @@ -206,16 +205,13 @@ type SecureServingInfo struct {
// Listener is the secure server network listener.
Listener net.Listener

// Cert is the main server cert which is used if SNI does not match. Cert must be non-nil and is
// allowed to be in SNICerts.
Cert *tls.Certificate

// SNICerts are the TLS certificates by name used for SNI.
SNICerts map[string]*tls.Certificate

// ClientCA is the certificate bundle for all the signers that you'll recognize for incoming client certificates
ClientCA *x509.CertPool

// DynamicCertificates is an alternative to Cert and SNICerts that is limited to files, but those file will
// be monitored for changes every minute and allows the certificates to be updated dynamically.
DynamicCertificates *DynamicCertificateConfig

// MinTLSVersion optionally overrides the minimum TLS version supported.
// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants).
MinTLSVersion uint16
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
const LoopbackClientServerNameOverride = "apiserver-loopback-client"

func (s *SecureServingInfo) NewClientConfig(caCert []byte) (*restclient.Config, error) {
if s == nil || (s.Cert == nil && len(s.SNICerts) == 0) {
if s == nil || (s.DynamicCertificates == nil) {
return nil, nil
}

Expand Down
228 changes: 228 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/server/dynamic_certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package server

import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"reflect"
"strings"
"sync/atomic"
"time"

"github.com/golang/glog"

utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
)

// DynamicCertificateConfig provides a way to have dynamic https configuration for a fixed set of files.
// You need to remember to call CheckCerts before usage and start the go routine for Run.
type DynamicCertificateConfig struct {
CurrentValue atomic.Value

// LoopbackCert holds the special certificate that we create for loopback connections
LoopbackCert *tls.Certificate

CurrentContent DynamicCertificateContent

CertificateReferences DynamicCertificateReferences
}

type DynamicCertificateReferences struct {
DefaultCertificate CertKeyFileReference
NameToCertificate map[string]*CertKeyFileReference
}

type CertKeyFileReference struct {
Cert string
Key string
}

type DynamicCertificateContent struct {
DefaultCertificate CertKeyFileContent
NameToCertificate map[string]*CertKeyFileContent
}

type CertKeyFileContent struct {
Cert []byte
Key []byte
}

type RuntimeDynamicCertificateConfig struct {
Certificates []tls.Certificate
NameToCertificate map[string]*tls.Certificate
}

func (c *DynamicCertificateConfig) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
uncastObj := c.CurrentValue.Load()
if uncastObj == nil {
return nil, errors.New("tls: configuration not ready")
}
runtimeConfig, ok := uncastObj.(*RuntimeDynamicCertificateConfig)
if !ok {
return nil, errors.New("tls: unexpected config type")
}
return runtimeConfig.GetCertificate(clientHello)
}

func (c *DynamicCertificateConfig) Run(stopCh <-chan struct{}) {
glog.Infof("Starting DynamicCertificateConfig")
defer glog.Infof("Shutting down DynamicCertificateConfig")

go wait.Until(func() {
err := c.CheckCerts()
if err != nil {
utilruntime.HandleError(err)
}
}, 1*time.Minute, stopCh)

<-stopCh
}

func (c *DynamicCertificateConfig) CheckCerts() error {
newContent := DynamicCertificateContent{
NameToCertificate: map[string]*CertKeyFileContent{},
}

certBytes, err := ioutil.ReadFile(c.CertificateReferences.DefaultCertificate.Cert)
if err != nil {
return err
}
keyBytes, err := ioutil.ReadFile(c.CertificateReferences.DefaultCertificate.Key)
if err != nil {
return err
}
newContent.DefaultCertificate = CertKeyFileContent{Cert: certBytes, Key: keyBytes}

for key, currRef := range c.CertificateReferences.NameToCertificate {
certBytes, err := ioutil.ReadFile(currRef.Cert)
if err != nil {
return err
}
keyBytes, err := ioutil.ReadFile(currRef.Key)
if err != nil {
return err
}
newContent.NameToCertificate[key] = &CertKeyFileContent{Cert: certBytes, Key: keyBytes}
}

if newContent.Equals(&c.CurrentContent) {
return nil
}

newRuntimeConfig, err := newContent.ToRuntimeConfig()
if err != nil {
return err
}
if c.LoopbackCert != nil {
newRuntimeConfig.NameToCertificate[LoopbackClientServerNameOverride] = c.LoopbackCert
}
c.CurrentValue.Store(newRuntimeConfig)
c.CurrentContent = newContent // this is single threaded, so we have no locking issue

return nil
}

func (c *DynamicCertificateContent) Equals(rhs *DynamicCertificateContent) bool {
if c == nil && rhs == nil {
return true
}
if c == nil && rhs != nil {
return false
}
if c != nil && rhs == nil {
return false
}
cKeys := sets.StringKeySet(c.NameToCertificate)
rhsKeys := sets.StringKeySet(rhs.NameToCertificate)
if !cKeys.Equal(rhsKeys) {
return false
}

if !c.DefaultCertificate.Equals(&rhs.DefaultCertificate) {
return false
}
for _, key := range cKeys.UnsortedList() {
if !c.NameToCertificate[key].Equals(rhs.NameToCertificate[key]) {
return false
}
}

return true
}

func (c *DynamicCertificateContent) ToRuntimeConfig() (*RuntimeDynamicCertificateConfig, error) {
ret := &RuntimeDynamicCertificateConfig{
NameToCertificate: map[string]*tls.Certificate{},
}

// load main cert
if len(c.DefaultCertificate.Cert) != 0 || len(c.DefaultCertificate.Key) != 0 {
tlsCert, err := tls.X509KeyPair(c.DefaultCertificate.Cert, c.DefaultCertificate.Key)
if err != nil {
return nil, fmt.Errorf("unable to load server certificate: %v", err)
}
ret.Certificates = []tls.Certificate{tlsCert}
}

// load SNI certs
for name, nck := range c.NameToCertificate {
tlsCert, err := tls.X509KeyPair(nck.Cert, nck.Key)
if err != nil {
return nil, fmt.Errorf("failed to load SNI cert and key: %v", err)
}
ret.NameToCertificate[name] = &tlsCert
}

return ret, nil
}

func (c *CertKeyFileContent) Equals(rhs *CertKeyFileContent) bool {
if c == nil && rhs == nil {
return true
}
if c == nil && rhs != nil {
return false
}
if c != nil && rhs == nil {
return false
}
return reflect.DeepEqual(c.Key, rhs.Key) && reflect.DeepEqual(c.Cert, rhs.Cert)
}

// GetCertificate copied from tls.getCertificate
func (c *RuntimeDynamicCertificateConfig) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if len(c.Certificates) == 0 {
return nil, errors.New("tls: no certificates configured")
}

if c.NameToCertificate == nil {
// There's only one choice, so no point doing any work.
return &c.Certificates[0], nil
}

name := strings.ToLower(clientHello.ServerName)
for len(name) > 0 && name[len(name)-1] == '.' {
name = name[:len(name)-1]
}

if cert, ok := c.NameToCertificate[name]; ok {
return cert, nil
}

// try replacing labels in the name with wildcards until we get a
// match.
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
if cert, ok := c.NameToCertificate[candidate]; ok {
return cert, nil
}
}

// If nothing matches, return the first certificate.
return &c.Certificates[0], nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package server
24 changes: 15 additions & 9 deletions staging/src/k8s.io/apiserver/pkg/server/options/serving.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,14 @@ func (s *SecureServingOptions) ApplyTo(config **server.SecureServingInfo) error
}
c := *config

serverCertFile, serverKeyFile := s.ServerCert.CertKey.CertFile, s.ServerCert.CertKey.KeyFile
// load main cert
if len(serverCertFile) != 0 || len(serverKeyFile) != 0 {
tlsCert, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile)
if err != nil {
return fmt.Errorf("unable to load server certificate: %v", err)
}
c.Cert = &tlsCert
c.DynamicCertificates = &server.DynamicCertificateConfig{
CertificateReferences: server.DynamicCertificateReferences{
DefaultCertificate: server.CertKeyFileReference{
Cert: s.ServerCert.CertKey.CertFile,
Key: s.ServerCert.CertKey.KeyFile,
},
NameToCertificate: map[string]*server.CertKeyFileReference{},
},
}

if len(s.CipherSuites) != 0 {
Expand All @@ -236,18 +236,24 @@ func (s *SecureServingOptions) ApplyTo(config **server.SecureServingInfo) error
}

// load SNI certs
// holds the original filenames of the certificates. Because allow implicit specification of names, we have to read
// everything to get this to be able to drive the dynamicCertificateConfig
namedTLSCerts := make([]server.NamedTLSCert, 0, len(s.SNICertKeys))
for _, nck := range s.SNICertKeys {
tlsCert, err := tls.LoadX509KeyPair(nck.CertFile, nck.KeyFile)
namedTLSCerts = append(namedTLSCerts, server.NamedTLSCert{
OriginalFileName: &server.CertKeyFileReference{
Cert: nck.CertFile,
Key: nck.KeyFile,
},
TLSCert: tlsCert,
Names: nck.Names,
})
if err != nil {
return fmt.Errorf("failed to load SNI cert and key: %v", err)
}
}
c.SNICerts, err = server.GetNamedCertificateMap(namedTLSCerts)
_, c.DynamicCertificates.CertificateReferences.NameToCertificate, err = server.GetNamedCertificateMap(namedTLSCerts)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 6ff2356

Please sign in to comment.