blob: 6eda90b56aa5f1b3905f2da36bf2244f2c09c8d8 [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 bootstrap
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"strings"
"time"
)
import (
"istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/features"
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
"github.com/apache/dubbo-go-pixiu/pkg/security"
"github.com/apache/dubbo-go-pixiu/security/pkg/k8s/chiron"
)
const (
// defaultCertGracePeriodRatio is the default length of certificate rotation grace period,
// configured as the ratio of the certificate TTL.
defaultCertGracePeriodRatio = 0.5
// defaultMinCertGracePeriod is the default minimum grace period for workload cert rotation.
defaultMinCertGracePeriod = 10 * time.Minute
// the interval polling root cert and re sign istiod cert when it changes.
rootCertPollingInterval = 60 * time.Second
// Default CA certificate path
// Currently, custom CA path is not supported; no API to get custom CA cert yet.
defaultCACertPath = "./var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)
// CertController can create certificates signed by K8S server.
func (s *Server) initCertController(args *PilotArgs) error {
var err error
var secretNames, dnsNames []string
meshConfig := s.environment.Mesh()
if meshConfig.GetCertificates() == nil || len(meshConfig.GetCertificates()) == 0 {
// TODO: if the provider is set to Citadel, use that instead of k8s so the API is still preserved.
log.Info("No certificates specified, skipping K8S DNS certificate controller")
return nil
}
k8sClient := s.kubeClient
for _, c := range meshConfig.GetCertificates() {
name := strings.Join(c.GetDnsNames(), ",")
if len(name) == 0 { // must have a DNS name
continue
}
if len(c.GetSecretName()) > 0 {
// Chiron will generate the key and certificate and save them in a secret
secretNames = append(secretNames, c.GetSecretName())
dnsNames = append(dnsNames, name)
}
}
// Provision and manage the certificates for non-Pilot services.
// If services are empty, the certificate controller will do nothing.
s.certController, err = chiron.NewWebhookController(defaultCertGracePeriodRatio, defaultMinCertGracePeriod,
k8sClient, defaultCACertPath, secretNames, dnsNames, args.Namespace, "")
if err != nil {
return fmt.Errorf("failed to create certificate controller: %v", err)
}
s.addStartFunc(func(stop <-chan struct{}) error {
go func() {
// Run Chiron to manage the lifecycles of certificates
s.certController.Run(stop)
}()
return nil
})
return nil
}
// initDNSCerts will create the certificates to be used by Istiod GRPC server and webhooks.
// If the certificate creation fails - for example no support in K8S - returns an error.
// Will use the mesh.yaml DiscoveryAddress to find the default expected address of the control plane,
// with an environment variable allowing override.
//
// Controlled by features.IstiodService env variable, which defines the name of the service to use in the DNS
// cert, or empty for disabling this feature.
//
// TODO: If the discovery address in mesh.yaml is set to port 15012 (XDS-with-DNS-certs) and the name
// matches the k8s namespace, failure to start DNS server is a fatal error.
func (s *Server) initDNSCerts(hostname, namespace string) error {
// Name in the Istiod cert - support the old service names as well.
// validate hostname contains namespace
parts := strings.Split(hostname, ".")
hostnamePrefix := parts[0]
var certChain, keyPEM, caBundle []byte
var err error
pilotCertProviderName := features.PilotCertProvider
if strings.HasPrefix(pilotCertProviderName, constants.CertProviderKubernetesSignerPrefix) && s.RA != nil {
signerName := strings.TrimPrefix(pilotCertProviderName, constants.CertProviderKubernetesSignerPrefix)
log.Infof("Generating K8S-signed cert for %v using signer %v", s.dnsNames, signerName)
certChain, keyPEM, _, err = chiron.GenKeyCertK8sCA(s.kubeClient,
strings.Join(s.dnsNames, ","), hostnamePrefix+".csr.secret", namespace, "", signerName, true, SelfSignedCACertTTL.Get())
if err != nil {
return fmt.Errorf("failed generating key and cert by kubernetes: %v", err)
}
caBundle, err = s.RA.GetRootCertFromMeshConfig(signerName)
if err != nil {
return err
}
// MeshConfig:Add callback for mesh config update
s.environment.AddMeshHandler(func() {
newCaBundle, _ := s.RA.GetRootCertFromMeshConfig(signerName)
if newCaBundle != nil && !bytes.Equal(newCaBundle, s.istiodCertBundleWatcher.GetKeyCertBundle().CABundle) {
newCertChain, newKeyPEM, _, err := chiron.GenKeyCertK8sCA(s.kubeClient,
strings.Join(s.dnsNames, ","), hostnamePrefix+".csr.secret", namespace, "", signerName, true, SelfSignedCACertTTL.Get())
if err != nil {
log.Fatalf("failed regenerating key and cert for istiod by kubernetes: %v", err)
}
s.istiodCertBundleWatcher.SetAndNotify(newKeyPEM, newCertChain, newCaBundle)
}
})
} else if pilotCertProviderName == constants.CertProviderKubernetes {
log.Infof("Generating K8S-signed cert for %v", s.dnsNames)
certChain, keyPEM, _, err = chiron.GenKeyCertK8sCA(s.kubeClient,
strings.Join(s.dnsNames, ","), hostnamePrefix+".csr.secret", namespace, defaultCACertPath, "", true, SelfSignedCACertTTL.Get())
if err != nil {
return fmt.Errorf("failed generating key and cert by kubernetes: %v", err)
}
caBundle, err = os.ReadFile(defaultCACertPath)
if err != nil {
return fmt.Errorf("failed reading %s: %v", defaultCACertPath, err)
}
} else if pilotCertProviderName == constants.CertProviderIstiod {
certChain, keyPEM, err = s.CA.GenKeyCert(s.dnsNames, SelfSignedCACertTTL.Get(), false)
if err != nil {
return fmt.Errorf("failed generating istiod key cert %v", err)
}
log.Infof("Generating istiod-signed cert for %v:\n %s", s.dnsNames, certChain)
fileBundle, err := detectSigningCABundle()
if err != nil {
return fmt.Errorf("unable to determine signing file format %v", err)
}
// check if signing key file exists the cert dir
if _, err := os.Stat(fileBundle.SigningKeyFile); err != nil {
log.Infof("No plugged-in cert at %v; self-signed cert is used", fileBundle.SigningKeyFile)
caBundle = s.CA.GetCAKeyCertBundle().GetRootCertPem()
s.addStartFunc(func(stop <-chan struct{}) error {
go func() {
// regenerate istiod key cert when root cert changes.
s.watchRootCertAndGenKeyCert(stop)
}()
return nil
})
} else {
log.Infof("Use plugged-in cert at %v", fileBundle.SigningKeyFile)
caBundle, err = os.ReadFile(fileBundle.RootCertFile)
if err != nil {
return fmt.Errorf("failed reading %s: %v", fileBundle.RootCertFile, err)
}
}
} else {
customCACertPath := security.DefaultRootCertFilePath
log.Infof("User specified cert provider: %v, mounted in a well known location %v",
features.PilotCertProvider, customCACertPath)
caBundle, err = os.ReadFile(customCACertPath)
if err != nil {
return fmt.Errorf("failed reading %s: %v", customCACertPath, err)
}
}
s.istiodCertBundleWatcher.SetAndNotify(keyPEM, certChain, caBundle)
return nil
}
// TODO(hzxuzonghu): support async notification instead of polling the CA root cert.
func (s *Server) watchRootCertAndGenKeyCert(stop <-chan struct{}) {
caBundle := s.CA.GetCAKeyCertBundle().GetRootCertPem()
for {
select {
case <-stop:
return
case <-time.After(rootCertPollingInterval):
newRootCert := s.CA.GetCAKeyCertBundle().GetRootCertPem()
if !bytes.Equal(caBundle, newRootCert) {
caBundle = newRootCert
certChain, keyPEM, err := s.CA.GenKeyCert(s.dnsNames, SelfSignedCACertTTL.Get(), false)
if err != nil {
log.Errorf("failed generating istiod key cert %v", err)
} else {
s.istiodCertBundleWatcher.SetAndNotify(keyPEM, certChain, caBundle)
log.Infof("regenerated istiod dns cert: %s", certChain)
}
}
}
}
}
// updatePluggedinRootCertAndGenKeyCert when intermediate CA is updated, it generates new dns certs and notifies keycertbundle about the changes
func (s *Server) updatePluggedinRootCertAndGenKeyCert() error {
caBundle := s.CA.GetCAKeyCertBundle().GetRootCertPem()
certChain, keyPEM, err := s.CA.GenKeyCert(s.dnsNames, SelfSignedCACertTTL.Get(), false)
if err != nil {
return err
}
s.istiodCertBundleWatcher.SetAndNotify(keyPEM, certChain, caBundle)
return nil
}
// initCertificateWatches sets up watches for the plugin dns certs.
func (s *Server) initCertificateWatches(tlsOptions TLSOptions) error {
if err := s.istiodCertBundleWatcher.SetFromFilesAndNotify(tlsOptions.KeyFile, tlsOptions.CertFile, tlsOptions.CaCertFile); err != nil {
return fmt.Errorf("set keyCertBundle failed: %v", err)
}
// TODO: Setup watcher for root and restart server if it changes.
for _, file := range []string{tlsOptions.CertFile, tlsOptions.KeyFile} {
log.Infof("adding watcher for certificate %s", file)
if err := s.fileWatcher.Add(file); err != nil {
return fmt.Errorf("could not watch %v: %v", file, err)
}
}
s.addStartFunc(func(stop <-chan struct{}) error {
go func() {
var keyCertTimerC <-chan time.Time
for {
select {
case <-keyCertTimerC:
keyCertTimerC = nil
if err := s.istiodCertBundleWatcher.SetFromFilesAndNotify(tlsOptions.KeyFile, tlsOptions.CertFile, tlsOptions.CaCertFile); err != nil {
log.Errorf("Setting keyCertBundle failed: %v", err)
}
case <-s.fileWatcher.Events(tlsOptions.CertFile):
if keyCertTimerC == nil {
keyCertTimerC = time.After(watchDebounceDelay)
}
case <-s.fileWatcher.Events(tlsOptions.KeyFile):
if keyCertTimerC == nil {
keyCertTimerC = time.After(watchDebounceDelay)
}
case err := <-s.fileWatcher.Errors(tlsOptions.CertFile):
log.Errorf("error watching %v: %v", tlsOptions.CertFile, err)
case err := <-s.fileWatcher.Errors(tlsOptions.KeyFile):
log.Errorf("error watching %v: %v", tlsOptions.KeyFile, err)
case <-stop:
return
}
}
}()
return nil
})
return nil
}
func (s *Server) reloadIstiodCert(watchCh <-chan struct{}, stopCh <-chan struct{}) {
for {
select {
case <-stopCh:
return
case <-watchCh:
if err := s.loadIstiodCert(); err != nil {
log.Errorf("reload istiod cert failed: %v", err)
}
}
}
}
// loadIstiodCert load IstiodCert received from watchCh once
func (s *Server) loadIstiodCert() error {
keyCertBundle := s.istiodCertBundleWatcher.GetKeyCertBundle()
keyPair, err := tls.X509KeyPair(keyCertBundle.CertPem, keyCertBundle.KeyPem)
if err != nil {
return fmt.Errorf("istiod loading x509 key pairs failed: %v", err)
}
for _, c := range keyPair.Certificate {
x509Cert, err := x509.ParseCertificates(c)
if err != nil {
// This can rarely happen, just in case.
return fmt.Errorf("x509 cert - ParseCertificates() error: %v", err)
}
for _, c := range x509Cert {
log.Infof("x509 cert - Issuer: %q, Subject: %q, SN: %x, NotBefore: %q, NotAfter: %q",
c.Issuer, c.Subject, c.SerialNumber,
c.NotBefore.Format(time.RFC3339), c.NotAfter.Format(time.RFC3339))
}
}
log.Info("Istiod certificates are reloaded")
s.certMu.Lock()
s.istiodCert = &keyPair
s.certMu.Unlock()
return nil
}