blob: a4a66d5dd6fa2b3ea2420b2f1c6c07f88600574b [file] [log] [blame]
// Copyright Istio Authors
//
// 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 ca
import (
"bytes"
"fmt"
"math/rand"
"time"
)
import (
"istio.io/pkg/log"
v1 "k8s.io/api/core/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
import (
"github.com/apache/dubbo-go-pixiu/security/pkg/k8s/controller"
"github.com/apache/dubbo-go-pixiu/security/pkg/pki/util"
certutil "github.com/apache/dubbo-go-pixiu/security/pkg/util"
)
var rootCertRotatorLog = log.RegisterScope("rootcertrotator", "Self-signed CA root cert rotator log", 0)
type SelfSignedCARootCertRotatorConfig struct {
certInspector certutil.CertUtil
caStorageNamespace string
org string
rootCertFile string
client corev1.CoreV1Interface
CheckInterval time.Duration
caCertTTL time.Duration
retryInterval time.Duration
retryMax time.Duration
dualUse bool
enableJitter bool
}
// SelfSignedCARootCertRotator automatically checks self-signed signing root
// certificate and rotates root certificate if it is going to expire.
type SelfSignedCARootCertRotator struct {
caSecretController *controller.CaSecretController
config *SelfSignedCARootCertRotatorConfig
backOffTime time.Duration
ca *IstioCA
}
// NewSelfSignedCARootCertRotator returns a new root cert rotator instance that
// rotates self-signed root cert periodically.
func NewSelfSignedCARootCertRotator(config *SelfSignedCARootCertRotatorConfig,
ca *IstioCA) *SelfSignedCARootCertRotator {
rotator := &SelfSignedCARootCertRotator{
caSecretController: controller.NewCaSecretController(config.client),
config: config,
ca: ca,
}
if config.enableJitter {
// Select a back off time in seconds, which is in the range of [0, rotator.config.CheckInterval).
randSource := rand.NewSource(time.Now().UnixNano())
randBackOff := rand.New(randSource)
backOffSeconds := int(time.Duration(randBackOff.Int63n(int64(rotator.config.CheckInterval))).Seconds())
rotator.backOffTime = time.Duration(backOffSeconds) * time.Second
rootCertRotatorLog.Infof("Set up back off time %s to start rotator.", rotator.backOffTime.String())
} else {
rotator.backOffTime = time.Duration(0)
}
return rotator
}
// Run refreshes root certs and updates config map accordingly.
func (rotator *SelfSignedCARootCertRotator) Run(stopCh chan struct{}) {
if rotator.config.enableJitter {
rootCertRotatorLog.Infof("Jitter is enabled, wait %s before "+
"starting root cert rotator.", rotator.backOffTime.String())
select {
case <-time.After(rotator.backOffTime):
rootCertRotatorLog.Infof("Jitter complete, start rotator.")
case <-stopCh:
rootCertRotatorLog.Info("Received stop signal, so stop the root cert rotator.")
return
}
}
ticker := time.NewTicker(rotator.config.CheckInterval)
for {
select {
case <-ticker.C:
rootCertRotatorLog.Info("Check and rotate root cert.")
rotator.checkAndRotateRootCert()
case _, ok := <-stopCh:
if !ok {
rootCertRotatorLog.Info("Received stop signal, so stop the root cert rotator.")
if ticker != nil {
ticker.Stop()
}
return
}
}
}
}
// checkAndRotateRootCert decides whether root cert should be refreshed, and rotates
// root cert for self-signed Citadel.
func (rotator *SelfSignedCARootCertRotator) checkAndRotateRootCert() {
caSecret, scrtErr := rotator.caSecretController.LoadCASecretWithRetry(CASecret,
rotator.config.caStorageNamespace, rotator.config.retryInterval, rotator.config.retryMax)
if scrtErr != nil {
rootCertRotatorLog.Errorf("Fail to load CA secret %s:%s (error: %s), skip cert rotation job",
rotator.config.caStorageNamespace, CASecret, scrtErr.Error())
} else {
rotator.checkAndRotateRootCertForSigningCertCitadel(caSecret)
}
}
// checkAndRotateRootCertForSigningCertCitadel checks root cert secret and rotates
// root cert if the current one is about to expire. The rotation uses existing
// root private key to generate a new root cert, and updates root cert secret.
func (rotator *SelfSignedCARootCertRotator) checkAndRotateRootCertForSigningCertCitadel(
caSecret *v1.Secret) {
if caSecret == nil {
rootCertRotatorLog.Errorf("root cert secret %s is nil, skip cert rotation job",
CASecret)
return
}
// Check root certificate expiration time in CA secret
waitTime, err := rotator.config.certInspector.GetWaitTime(caSecret.Data[CACertFile], time.Now(), time.Duration(0))
if err == nil && waitTime > 0 {
rootCertRotatorLog.Info("Root cert is not about to expire, skipping root cert rotation.")
caCertInMem, _, _, _ := rotator.ca.GetCAKeyCertBundle().GetAllPem()
// If CA certificate is different from the CA certificate in local key
// cert bundle, it implies that other Citadels have updated istio-ca-secret.
// Reload root certificate into key cert bundle.
if !bytes.Equal(caCertInMem, caSecret.Data[CACertFile]) {
rootCertRotatorLog.Warn("CA cert in KeyCertBundle does not match CA cert in " +
"istio-ca-secret. Start to reload root cert into KeyCertBundle")
rootCerts, err := util.AppendRootCerts(caSecret.Data[CACertFile], rotator.config.rootCertFile)
if err != nil {
rootCertRotatorLog.Errorf("failed to append root certificates from file: %s", err.Error())
return
}
if err := rotator.ca.GetCAKeyCertBundle().VerifyAndSetAll(caSecret.Data[CACertFile],
caSecret.Data[CAPrivateKeyFile], nil, rootCerts); err != nil {
rootCertRotatorLog.Errorf("failed to reload root cert into KeyCertBundle (%v)", err)
} else {
rootCertRotatorLog.Info("Successfully reloaded root cert into KeyCertBundle.")
}
}
return
}
rootCertRotatorLog.Infof("Refresh root certificate, root cert is about to expire: %s", err.Error())
oldCertOptions, err := util.GetCertOptionsFromExistingCert(caSecret.Data[CACertFile])
if err != nil {
rootCertRotatorLog.Warnf("Failed to generate cert options from existing root certificate (%v), "+
"new root certificate may not match old root certificate", err)
}
options := util.CertOptions{
TTL: rotator.config.caCertTTL,
SignerPrivPem: caSecret.Data[CAPrivateKeyFile],
Org: rotator.config.org,
IsCA: true,
IsSelfSigned: true,
RSAKeySize: rotator.ca.caRSAKeySize,
IsDualUse: rotator.config.dualUse,
}
// options should be consistent with the one used in NewSelfSignedIstioCAOptions().
// This is to make sure when rotate the root cert, we don't make unnecessary changes
// to the certificate or add extra fields to the certificate.
options = util.MergeCertOptions(options, oldCertOptions)
pemCert, pemKey, ckErr := util.GenRootCertFromExistingKey(options)
if ckErr != nil {
rootCertRotatorLog.Errorf("unable to generate CA cert and key for self-signed CA: %s", ckErr.Error())
return
}
pemRootCerts, err := util.AppendRootCerts(pemCert, rotator.config.rootCertFile)
if err != nil {
rootCertRotatorLog.Errorf("failed to append root certificates: %s", err.Error())
return
}
oldCaCert := caSecret.Data[CACertFile]
oldCaPrivateKey := caSecret.Data[CAPrivateKeyFile]
oldRootCerts := rotator.ca.GetCAKeyCertBundle().GetRootCertPem()
if rollback, err := rotator.updateRootCertificate(caSecret, true, pemCert, pemKey, pemRootCerts); err != nil {
if !rollback {
rootCertRotatorLog.Errorf("Failed to roll forward root certificate (error: %s). "+
"Abort new root certificate", err.Error())
return
}
// caSecret is out-of-date. Need to load the latest istio-ca-secret to roll back root certificate.
_, err = rotator.updateRootCertificate(nil, false, oldCaCert, oldCaPrivateKey, oldRootCerts)
if err != nil {
rootCertRotatorLog.Errorf("Failed to roll backward root certificate (error: %s).", err.Error())
}
return
}
rootCertRotatorLog.Info("Root certificate rotation is completed successfully.")
}
// updateRootCertificate updates root certificate in istio-ca-secret, keycertbundle and configmap. It takes a scrt
// object, cert, and key, and a flag rollForward indicating whether this update is to roll forward root certificate or
// to roll backward.
// updateRootCertificate returns error when any step is failed, and a flag indicating whether a rollback is required.
// Only when rollForward is true and failure happens, the returned rollback flag is true.
func (rotator *SelfSignedCARootCertRotator) updateRootCertificate(caSecret *v1.Secret, rollForward bool, cert, key, rootCert []byte) (bool, error) {
var err error
if caSecret == nil {
caSecret, err = rotator.caSecretController.LoadCASecretWithRetry(CASecret,
rotator.config.caStorageNamespace, rotator.config.retryInterval, rotator.config.retryMax)
if err != nil {
return false, fmt.Errorf("failed to load CA secret %s:%s (error: %s)", rotator.config.caStorageNamespace, CASecret,
err.Error())
}
}
caSecret.Data[CACertFile] = cert
caSecret.Data[CAPrivateKeyFile] = key
if err = rotator.caSecretController.UpdateCASecretWithRetry(caSecret, rotator.config.retryInterval, rotator.config.retryMax); err != nil {
return false, fmt.Errorf("failed to update CA secret (error: %s)", err.Error())
}
rootCertRotatorLog.Infof("Root certificate is written into CA secret: %v", string(cert))
if err := rotator.ca.GetCAKeyCertBundle().VerifyAndSetAll(cert, key, nil, rootCert); err != nil {
if rollForward {
// Rolling forward root certificate fails at keycertbundle update, notify caller to rollback.
return true, fmt.Errorf("failed to update CA KeyCertBundle (error: %s)", err.Error())
}
return false, fmt.Errorf("failed to update CA KeyCertBundle (error: %s)", err.Error())
}
rootCertRotatorLog.Infof("Root certificate is updated in CA KeyCertBundle: %v", string(cert))
return false, nil
}