Skip to content
Merged
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
33 changes: 29 additions & 4 deletions cmd/crossplane/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ type startCommand struct {
TLSClientSecretName string `env:"TLS_CLIENT_SECRET_NAME" help:"The name of the TLS Secret that will be store Crossplane's client certificate."`
TLSClientCertsDir string `env:"TLS_CLIENT_CERTS_DIR" help:"The path of the folder which will store TLS client certificate of Crossplane."`

TLSClientCACertFileName string `env:"TLS_CLIENT_CA_CERT_FILENAME" help:"The filename of the CA certificate in the TLS client certs directory."`
TLSClientCertFileName string `env:"TLS_CLIENT_CERT_FILENAME" help:"The filename of the client certificate in the TLS client certs directory."`
TLSClientKeyFileName string `env:"TLS_CLIENT_KEY_FILENAME" help:"The filename of the client private key in the TLS client certs directory."`

TLSServerCertFileName string `env:"TLS_SERVER_CERT_FILENAME" help:"The filename of the server certificate in the TLS server certs directory."`
TLSServerKeyFileName string `env:"TLS_SERVER_KEY_FILENAME" help:"The filename of the server private key in the TLS server certs directory."`

EnableDependencyVersionUpgrades bool `group:"Alpha Features:" help:"Enable support for upgrading dependency versions when the parent package is updated."`
EnableDependencyVersionDowngrades bool `group:"Alpha Features:" help:"Enable support for upgrading and downgrading dependency versions when a dependent package is updated."`
EnableSignatureVerification bool `group:"Alpha Features:" help:"Enable support for package signature verification via ImageConfig API."`
Expand Down Expand Up @@ -160,6 +167,22 @@ type startCommand struct {

// Run core Crossplane controllers.
func (c *startCommand) Run(s *runtime.Scheme, log logging.Logger) error { //nolint:gocognit // Only slightly over.
if c.TLSClientCACertFileName == "" {
c.TLSClientCACertFileName = initializer.SecretKeyCACert
}
if c.TLSClientCertFileName == "" {
c.TLSClientCertFileName = corev1.TLSCertKey
}
if c.TLSClientKeyFileName == "" {
c.TLSClientKeyFileName = corev1.TLSPrivateKeyKey
}
if c.TLSServerCertFileName == "" {
c.TLSServerCertFileName = corev1.TLSCertKey
}
if c.TLSServerKeyFileName == "" {
c.TLSServerKeyFileName = corev1.TLSPrivateKeyKey
}

if c.EnableCompositionWebhookSchemaValidation {
//nolint:revive // This is long and easier to read with punctuation.
return errors.New("Crossplane now uses CEL to validate Compositions. The --enable-composition-webhook-schema-validation flag will be removed in a future release.")
Expand Down Expand Up @@ -207,7 +230,9 @@ func (c *startCommand) Run(s *runtime.Scheme, log logging.Logger) error { //noli
Scheme: s,
Cache: cacheOptions,
WebhookServer: webhook.NewServer(webhook.Options{
CertDir: c.TLSServerCertsDir,
CertDir: c.TLSServerCertsDir,
CertName: c.TLSServerCertFileName,
KeyName: c.TLSServerKeyFileName,
TLSOpts: []func(*tls.Config){
func(t *tls.Config) {
t.MinVersion = tls.VersionTLS13
Expand Down Expand Up @@ -273,9 +298,9 @@ func (c *startCommand) Run(s *runtime.Scheme, log logging.Logger) error { //noli
}

clienttls, err := certificates.LoadMTLSConfig(
filepath.Join(c.TLSClientCertsDir, initializer.SecretKeyCACert),
filepath.Join(c.TLSClientCertsDir, corev1.TLSCertKey),
filepath.Join(c.TLSClientCertsDir, corev1.TLSPrivateKeyKey),
filepath.Join(c.TLSClientCertsDir, c.TLSClientCACertFileName),
filepath.Join(c.TLSClientCertsDir, c.TLSClientCertFileName),
filepath.Join(c.TLSClientCertsDir, c.TLSClientKeyFileName),
false)
if err != nil {
return errors.Wrap(err, "cannot load client TLS certificates")
Expand Down
32 changes: 25 additions & 7 deletions cmd/crossplane/core/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package core
import (
"context"
"fmt"
"path/filepath"

admv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -53,6 +54,9 @@ type initCommand struct {
TLSCASecretName string `env:"TLS_CA_SECRET_NAME" help:"The name of the Secret that the initializer will fill with TLS CA certificate."`
TLSServerSecretName string `env:"TLS_SERVER_SECRET_NAME" help:"The name of the Secret that the initializer will fill with TLS server certificates."`
TLSClientSecretName string `env:"TLS_CLIENT_SECRET_NAME" help:"The name of the Secret that the initializer will fill with TLS client certificates."`

WebhookTLSCertDir string `env:"WEBHOOK_TLS_CERT_DIR" help:"Directory containing TLS certificates for webhooks. When set, certificates are read from files instead of Secrets."`
WebhookTLSCACert string `env:"WEBHOOK_TLS_CA_CERT" help:"Filename of the CA certificate within the TLS cert directory."`
}

// Run starts the initialization process.
Expand All @@ -67,6 +71,10 @@ func (c *initCommand) Run(s *runtime.Scheme, log logging.Logger) error {
return errors.Wrap(err, "cannot create new kubernetes client")
}

if c.WebhookTLSCACert == "" {
c.WebhookTLSCACert = initializer.SecretKeyCACert
}

var steps []initializer.Step

tlsGeneratorOpts := []initializer.TLSCertificateGeneratorOption{
Expand All @@ -91,18 +99,28 @@ func (c *initCommand) Run(s *runtime.Scheme, log logging.Logger) error {
),
)

nn := types.NamespacedName{
Name: c.TLSServerSecretName,
Namespace: c.Namespace,
}
svc := admv1.ServiceReference{
Name: c.WebhookServiceName,
Namespace: c.WebhookServiceNamespace,
Port: &c.WebhookServicePort,
}
steps = append(steps,
initializer.NewCoreCRDs(c.CRDsPath, s, initializer.WithWebhookTLSSecretRef(nn)),
initializer.NewWebhookConfigurations(c.WebhookConfigurationsPath, s, nn, svc))

var caProvider initializer.WebhookCAProvider
if c.WebhookTLSCertDir != "" {
caProvider = &initializer.FileCAProvider{Path: filepath.Join(c.WebhookTLSCertDir, c.WebhookTLSCACert)}
steps = append(steps,
initializer.NewCoreCRDs(c.CRDsPath, s),
initializer.NewWebhookConfigurations(c.WebhookConfigurationsPath, s, caProvider, svc))
} else {
nn := types.NamespacedName{
Name: c.TLSServerSecretName,
Namespace: c.Namespace,
}
caProvider = &initializer.SecretCAProvider{SecretRef: nn}
steps = append(steps,
initializer.NewCoreCRDs(c.CRDsPath, s, initializer.WithWebhookTLSSecretRef(nn)),
initializer.NewWebhookConfigurations(c.WebhookConfigurationsPath, s, caProvider, svc))
}
} else {
log.Info("Warning: Webhooks are disabled, so deprecated ValidatingWebhookConfigurations will not be automatically deleted.")
steps = append(steps,
Expand Down
8 changes: 8 additions & 0 deletions internal/initializer/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,24 @@ func TLSCertificateGeneratorWithOwner(owner []metav1.OwnerReference) TLSCertific
}

// TLSCertificateGeneratorWithServerSecretName returns an TLSCertificateGeneratorOption that sets server secret name.
// If the secret name is empty, the option is a no-op.
func TLSCertificateGeneratorWithServerSecretName(s string, dnsNames []string) TLSCertificateGeneratorOption {
return func(g *TLSCertificateGenerator) {
if s == "" {
return
}
g.tlsServerSecretName = &s
g.tlsServerDNSNames = dnsNames
}
}

// TLSCertificateGeneratorWithClientSecretName returns an TLSCertificateGeneratorOption that sets client secret name.
// If the secret name is empty, the option is a no-op.
func TLSCertificateGeneratorWithClientSecretName(s string, subjects []string) TLSCertificateGeneratorOption {
return func(g *TLSCertificateGenerator) {
if s == "" {
return
}
g.tlsClientSecretName = &s
g.tlsClientDNSNames = subjects
}
Expand Down
31 changes: 31 additions & 0 deletions internal/initializer/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,37 @@ func TestTLSCertificateGeneratorRun(t *testing.T) {
},
want: want{err: nil},
},
"EmptyServerSecretNameIsNoOp": {
reason: "Passing an empty server secret name should not set tlsServerSecretName, so Run returns nil immediately.",
args: args{
kube: &test.MockClient{},
opts: []TLSCertificateGeneratorOption{
TLSCertificateGeneratorWithServerSecretName("", []string{subject}),
},
},
want: want{err: nil},
},
"EmptyClientSecretNameIsNoOp": {
reason: "Passing an empty client secret name should not set tlsClientSecretName, so Run returns nil immediately.",
args: args{
kube: &test.MockClient{},
opts: []TLSCertificateGeneratorOption{
TLSCertificateGeneratorWithClientSecretName("", []string{subject}),
},
},
want: want{err: nil},
},
"EmptyBothSecretNamesIsNoOp": {
reason: "Passing empty secret names for both server and client should result in no work done.",
args: args{
kube: &test.MockClient{},
opts: []TLSCertificateGeneratorOption{
TLSCertificateGeneratorWithServerSecretName("", []string{subject}),
TLSCertificateGeneratorWithClientSecretName("", []string{subject}),
},
},
want: want{err: nil},
},
"OnlyClientCertificateSuccessfulGeneratedClientCert": {
reason: "It should be successful if the client certificate is generated and put into the Secret.",
args: args{
Expand Down
71 changes: 59 additions & 12 deletions internal/initializer/webhook_configurations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package initializer

import (
"context"
"os"

"github.com/spf13/afero"
admv1 "k8s.io/api/admissionregistration/v1"
Expand All @@ -35,8 +36,53 @@ import (
const (
errApplyWebhookConfiguration = "cannot apply webhook configuration"
errGetWebhookSecret = "cannot get webhook secret"
errReadCABundleFile = "cannot read CA bundle file"
errEmptyCABundleFile = "CA bundle file is empty"
)

// WebhookCAProvider provides a CA bundle for webhook configurations.
type WebhookCAProvider interface {
GetCABundle(ctx context.Context, kube client.Client) ([]byte, error)
}

// SecretCAProvider loads the CA bundle from a Kubernetes Secret.
type SecretCAProvider struct {
SecretRef types.NamespacedName
}

// GetCABundle returns the CA bundle from a Kubernetes Secret.
func (p *SecretCAProvider) GetCABundle(ctx context.Context, kube client.Client) ([]byte, error) {
s := &corev1.Secret{}
if err := kube.Get(ctx, p.SecretRef, s); err != nil {
return nil, errors.Wrap(err, errGetWebhookSecret)
}

if len(s.Data["tls.crt"]) == 0 {
return nil, errors.Errorf(errFmtNoTLSCrtInSecret, p.SecretRef.String())
}

return s.Data["tls.crt"], nil
}

// FileCAProvider loads the CA bundle from a file.
type FileCAProvider struct {
Path string
}

// GetCABundle returns the CA bundle from a file on disk.
func (p *FileCAProvider) GetCABundle(_ context.Context, _ client.Client) ([]byte, error) {
data, err := os.ReadFile(p.Path)
if err != nil {
return nil, errors.Wrap(err, errReadCABundleFile)
}

if len(data) == 0 {
return nil, errors.New(errEmptyCABundleFile)
}

return data, nil
}

// WithWebhookConfigurationsFs is used to configure the filesystem the CRDs will
// be read from. Its default is afero.OsFs.
func WithWebhookConfigurationsFs(fs afero.Fs) WebhookConfigurationsOption {
Expand All @@ -45,15 +91,22 @@ func WithWebhookConfigurationsFs(fs afero.Fs) WebhookConfigurationsOption {
}
}

// WithWebhookCAProvider sets the CA provider for webhook configurations.
func WithWebhookCAProvider(provider WebhookCAProvider) WebhookConfigurationsOption {
return func(c *WebhookConfigurations) {
c.caProvider = provider
}
}

// WebhookConfigurationsOption configures WebhookConfigurations step.
type WebhookConfigurationsOption func(*WebhookConfigurations)

// NewWebhookConfigurations returns a new *WebhookConfigurations.
func NewWebhookConfigurations(path string, s *runtime.Scheme, tlsSecretRef types.NamespacedName, svc admv1.ServiceReference, opts ...WebhookConfigurationsOption) *WebhookConfigurations {
func NewWebhookConfigurations(path string, s *runtime.Scheme, caProvider WebhookCAProvider, svc admv1.ServiceReference, opts ...WebhookConfigurationsOption) *WebhookConfigurations {
c := &WebhookConfigurations{
Path: path,
Scheme: s,
TLSSecretRef: tlsSecretRef,
caProvider: caProvider,
ServiceReference: svc,
fs: afero.NewOsFs(),
}
Expand All @@ -69,7 +122,7 @@ func NewWebhookConfigurations(path string, s *runtime.Scheme, tlsSecretRef types
type WebhookConfigurations struct {
Path string
Scheme *runtime.Scheme
TLSSecretRef types.NamespacedName
caProvider WebhookCAProvider
ServiceReference admv1.ServiceReference

fs afero.Fs
Expand All @@ -78,17 +131,11 @@ type WebhookConfigurations struct {
// Run applies all webhook ValidatingWebhookConfigurations and
// MutatingWebhookConfiguration in the given directory.
func (c *WebhookConfigurations) Run(ctx context.Context, kube client.Client) error {
s := &corev1.Secret{}
if err := kube.Get(ctx, c.TLSSecretRef, s); err != nil {
return errors.Wrap(err, errGetWebhookSecret)
}

if len(s.Data["tls.crt"]) == 0 {
return errors.Errorf(errFmtNoTLSCrtInSecret, c.TLSSecretRef.String())
caBundle, err := c.caProvider.GetCABundle(ctx, kube)
if err != nil {
return err
}

caBundle := s.Data["tls.crt"]

r, err := parser.NewFsBackend(c.fs,
parser.FsDir(c.Path),
parser.FsFilters(
Expand Down
Loading