Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func NewCommand(ctx context.Context) *cobra.Command {
}

// Create a new TLS provider for the serving certificate and private key.
tls, err := tls.NewProvider(opts.Logr, cm, opts.TLS, cm)
tls, err := tls.NewProvider(opts.Logr, cm, opts.TLS, cm, cl)
if err != nil {
return fmt.Errorf("failed to create tls provider: %w", err)
}
Expand Down
18 changes: 16 additions & 2 deletions cmd/app/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,10 @@ func (o *Options) Complete() error {
}

// Ensure there is at least one DNS name to set in the serving certificate
// to ensure clients can properly verify the serving certificate
if len(o.TLS.ServingCertificateDNSNames) == 0 {
// to ensure clients can properly verify the serving certificate.
// Not required when loading a pre-provisioned Secret whose cert already
// embeds the desired DNS SANs.
if len(o.TLS.ServingCertificateDNSNames) == 0 && o.TLS.ServingCertificateSecretName == "" {
return fmt.Errorf("the list of DNS names to add to the serving certificate is empty")
}

Expand Down Expand Up @@ -261,6 +263,18 @@ func (o *Options) addTLSFlags(fs *pflag.FlagSet) {
"serving-signature-algorithm", "RSA",
"The type of signature algorithm to use when generating private keys. "+
"Currently only RSA and ECDSA are supported. By default RSA is used.")

fs.StringVar(&o.TLS.ServingCertificateSecretName,
"serving-certificate-secret-name", "",
"Name of a pre-provisioned Secret containing a cert-manager-issued TLS certificate "+
"and key for the gRPC serving endpoint. When set, istio-csr loads its serving cert "+
"from this Secret (tls.crt, tls.key, ca.crt) instead of requesting a CertificateRequest. "+
"Use this to supply a SPIFFE URI SAN that cert-manager Certificates support but "+
"the built-in CertificateRequest path does not.")

fs.StringVar(&o.TLS.ServingCertificateSecretNamespace,
"serving-certificate-secret-namespace", "",
"Namespace of the Secret named by --serving-certificate-secret-name.")
}

func (o *Options) addCertManagerFlags(fs *pflag.FlagSet) {
Expand Down
112 changes: 107 additions & 5 deletions pkg/tls/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"istio.io/istio/pkg/spiffe"
pkiutil "istio.io/istio/security/pkg/pki/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/metrics"

Expand Down Expand Up @@ -101,6 +103,18 @@ type Options struct {
// ServingSignatureAlgorithm is the type of key of serving signature algorithm
// used, RSA or ECDSA, The default is RSA.
ServingSignatureAlgorithm string

// ServingCertificateSecretName is the name of a pre-provisioned Secret
// (in ServingCertificateSecretNamespace) containing a cert-manager-issued
// TLS certificate for the gRPC serving endpoint. When set, istio-csr loads
// its serving cert from this Secret instead of creating a CertificateRequest.
// The Secret must contain tls.crt, tls.key, and (if no --root-ca-file is set)
// ca.crt, following the standard cert-manager Secret format.
ServingCertificateSecretName string

// ServingCertificateSecretNamespace is the namespace of the Secret named by
// ServingCertificateSecretName.
ServingCertificateSecretNamespace string
}

// Provider is used to provide a tls config containing an automatically renewed
Expand All @@ -114,7 +128,8 @@ type Provider struct {

rootCAs rootca.RootCAs

cm certmanager.Signer
cm certmanager.Signer
k8sClient kubernetes.Interface

lock sync.RWMutex
tlsConfig *tls.Config
Expand All @@ -124,11 +139,12 @@ type Provider struct {
}

// NewProvider will return a new provider where a TLS config is ready to be fetched.
func NewProvider(log logr.Logger, cm certmanager.Signer, opts Options, issuerChangeNotifier certmanager.IssuerChangeNotifier) (*Provider, error) {
func NewProvider(log logr.Logger, cm certmanager.Signer, opts Options, issuerChangeNotifier certmanager.IssuerChangeNotifier, k8sClient kubernetes.Interface) (*Provider, error) {
return &Provider{
opts: opts,
log: log.WithName("tls-provider"),
cm: cm,
opts: opts,
log: log.WithName("tls-provider"),
cm: cm,
k8sClient: k8sClient,

issuerChangeNotifier: issuerChangeNotifier,
}, nil
Expand Down Expand Up @@ -346,6 +362,10 @@ func (p *Provider) RootCAs(ctx context.Context) *rootca.RootCAs {
// fails, returns error.
// Returns the NotAfter timestamp that the new signed certificate expires.
func (p *Provider) fetchCertificate(ctx context.Context) (time.Time, error) {
if p.opts.ServingCertificateSecretName != "" {
return p.loadFromSecret(ctx)
}

// Increment certificate request metric by 1. Success label is 0 unless there
// is no error where it is changed to 1.
success := "0"
Expand Down Expand Up @@ -449,6 +469,88 @@ func (p *Provider) fetchCertificate(ctx context.Context) (time.Time, error) {
return leafCert.NotAfter, nil
}

// loadFromSecret loads the serving certificate and private key from the
// pre-provisioned Kubernetes Secret named by opts.ServingCertificateSecretName.
// It builds a tls.Config identical to the one produced by fetchCertificate.
// cert-manager rotates the Secret contents; each renewal tick re-reads it.
func (p *Provider) loadFromSecret(ctx context.Context) (time.Time, error) {
success := "0"
defer func() { metricCertRequest.With(prometheus.Labels{"success": success}).Inc() }()

secret, err := p.k8sClient.CoreV1().Secrets(p.opts.ServingCertificateSecretNamespace).Get(
ctx, p.opts.ServingCertificateSecretName, metav1.GetOptions{})
if err != nil {
return time.Time{}, fmt.Errorf("failed to get serving certificate Secret %s/%s: %w",
p.opts.ServingCertificateSecretNamespace, p.opts.ServingCertificateSecretName, err)
}

certPEM := secret.Data["tls.crt"]
keyPEM := secret.Data["tls.key"]

if len(certPEM) == 0 {
return time.Time{}, fmt.Errorf("Secret %s/%s missing tls.crt",
p.opts.ServingCertificateSecretNamespace, p.opts.ServingCertificateSecretName)
}
if len(keyPEM) == 0 {
return time.Time{}, fmt.Errorf("Secret %s/%s missing tls.key",
p.opts.ServingCertificateSecretNamespace, p.opts.ServingCertificateSecretName)
}

if len(p.opts.RootCAsCertFile) == 0 {
caPEM := secret.Data["ca.crt"]
if len(caPEM) == 0 {
return time.Time{}, fmt.Errorf("Secret %s/%s missing ca.crt",
p.opts.ServingCertificateSecretNamespace, p.opts.ServingCertificateSecretName)
}
if err := p.loadCAsRoot(caPEM); err != nil {
return time.Time{}, fmt.Errorf("failed to load CA from Secret: %w", err)
}
}

p.lock.Lock()
defer p.lock.Unlock()

if len(p.rootCAs.PEM) == 0 || p.rootCAs.CertPool == nil {
return time.Time{}, errors.New("root CA certificate is not defined")
}

peerCertVerifier := spiffe.NewPeerCertVerifier()
if err := peerCertVerifier.AddMappingFromPEM(p.opts.TrustDomain, p.rootCAs.PEM); err != nil {
return time.Time{}, fmt.Errorf("failed to add root CAs to SPIFFE peer certificate verifier: %w", err)
}

tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse serving certificate from Secret: %w", err)
}

leafCert, err := pki.DecodeX509CertificateBytes(certPEM)
if err != nil {
return time.Time{}, fmt.Errorf("failed to parse signed certificate: %w", err)
}

p.tlsConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{tlsCert},
NextProtos: []string{"h2"},
ClientAuth: tls.VerifyClientCertIfGiven,
ClientCAs: peerCertVerifier.GetGeneralCertPool(),
SessionTicketsDisabled: true,
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
err := peerCertVerifier.VerifyPeerCert(rawCerts, verifiedChains)
if err != nil {
p.log.Error(err, "could not verify certificate")
}
return err
},
}

p.log.Info("serving certificate loaded from Secret",
"secret", p.opts.ServingCertificateSecretNamespace+"/"+p.opts.ServingCertificateSecretName)
success = "1"
return leafCert.NotAfter, nil
}

func (p *Provider) TrustDomain() string {
return p.opts.TrustDomain
}
Expand Down