Skip to content

Commit

Permalink
Allow CA dirs to be specified beyond /custom/ca/
Browse files Browse the repository at this point in the history
Signed-off-by: Joel Smith <joelsmith@redhat.com>
  • Loading branch information
joelsmith committed Jun 4, 2024
1 parent 25b46f6 commit 8312549
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
- **General**: Declarative parsing of scaler config ([#5037](https://github.com/kedacore/keda/issues/5037)|[#5797](https://github.com/kedacore/keda/issues/5797))
- **General**: Support for Kubernetes v1.30 ([#5828](https://github.com/kedacore/keda/issues/5828))
- **General**: Add --ca-dir flag to KEDA operator to specify directories with CA certificates for scalers to authenticate TLS connections (defaults to /custom/ca) ([#5859](https://github.com/kedacore/keda/pull/5859))

#### Experimental

Expand Down
2 changes: 2 additions & 0 deletions cmd/operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func main() {
var k8sClusterDomain string
var enableCertRotation bool
var validatingWebhookName string
var caDir []string
pflag.BoolVar(&enablePrometheusMetrics, "enable-prometheus-metrics", true, "Enable the prometheus metric of keda-operator.")
pflag.BoolVar(&enableOpenTelemetryMetrics, "enable-opentelemetry-metrics", false, "Enable the opentelemetry metric of keda-operator.")
pflag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the prometheus metric endpoint binds to.")
Expand All @@ -106,6 +107,7 @@ func main() {
pflag.StringVar(&k8sClusterDomain, "k8s-cluster-domain", "cluster.local", "Kubernetes cluster domain. Defaults to cluster.local")
pflag.BoolVar(&enableCertRotation, "enable-cert-rotation", false, "enable automatic generation and rotation of TLS certificates/keys")
pflag.StringVar(&validatingWebhookName, "validating-webhook-name", "keda-admission", "ValidatingWebhookConfiguration name. Defaults to keda-admission")
pflag.StringArrayVar(&caDir, "ca-dir", []string{"/custom/ca"}, "Directory with CA certificates for scalers to authenticate TLS connections. Can be specified multiple times. Defaults to /custom/ca")
opts := zap.Options{}
opts.BindFlags(flag.CommandLine)
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
Expand Down
64 changes: 36 additions & 28 deletions pkg/util/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,21 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log"
)

const customCAPath = "/custom/ca"
const defaultCustomCAPath = "/custom/ca"

var logger = logf.Log.WithName("certificates")

var (
rootCAs *x509.CertPool
rootCAsLock sync.Mutex
rootCAs *x509.CertPool
rootCAsLock sync.Mutex
customCAPaths []string = []string{defaultCustomCAPath}
)

func setCACertDirs(caCertDirs []string) {
customCAPaths = caCertDirs
rootCAs = nil // force a reload on the next call to getRootCAs()
}

func getRootCAs() *x509.CertPool {
rootCAsLock.Lock()
defer rootCAsLock.Unlock()
Expand All @@ -56,37 +62,39 @@ func getRootCAs() *x509.CertPool {
logger.V(1).Info("system cert pool not available, using new cert pool instead")
}
}
if _, err := os.Stat(customCAPath); errors.Is(err, fs.ErrNotExist) {
logger.V(1).Info(fmt.Sprintf("the path %s doesn't exist, skipping custom CA registrations", customCAPath))
return rootCAs
}

files, err := os.ReadDir(customCAPath)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to read %s", customCAPath))
return rootCAs
}

for _, file := range files {
filename := file.Name()
if file.IsDir() || strings.HasPrefix(filename, "..") {
logger.V(1).Info(fmt.Sprintf("%s isn't a valid certificate", filename))
continue // Skip directories and special files
for _, customCAPath := range customCAPaths {
if _, err := os.Stat(customCAPath); errors.Is(err, fs.ErrNotExist) {
logger.V(1).Info(fmt.Sprintf("the path %s doesn't exist, skipping custom CA registrations", customCAPath))
return rootCAs
}

filePath := filepath.Join(customCAPath, filename)
certs, err := os.ReadFile(filePath)
files, err := os.ReadDir(customCAPath)
if err != nil {
logger.Error(err, fmt.Sprintf("error reading %q", filename))
continue
logger.Error(err, fmt.Sprintf("unable to read %s", customCAPath))
return rootCAs
}

if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
logger.Error(fmt.Errorf("no certs appended"), "filename", filename)
continue
for _, file := range files {
filename := file.Name()
if file.IsDir() || strings.HasPrefix(filename, "..") {
logger.V(1).Info(fmt.Sprintf("%s isn't a valid certificate", filename))
continue // Skip directories and special files
}

filePath := filepath.Join(customCAPath, filename)
certs, err := os.ReadFile(filePath)
if err != nil {
logger.Error(err, fmt.Sprintf("error reading %q", filename))
continue
}

if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
logger.Error(fmt.Errorf("no certs appended"), "filename", filename)
continue
}
logger.V(1).Info(fmt.Sprintf("the certificate %s has been added to the pool", filename))
}
logger.V(1).Info(fmt.Sprintf("the certificate %s has been added to the pool", filename))
}

}
return rootCAs
}
55 changes: 47 additions & 8 deletions pkg/util/certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"io/ioutil"
"math/big"
"os"
"path"
Expand All @@ -34,19 +35,24 @@ import (
)

var (
caCrtPath = path.Join(customCAPath, "ca.crt")
certCommonName = "test-cert"
certCommonName = "test-cert"
certCommonName2 = "test-cert2"
)

func TestCustomCAsAreRegistered(t *testing.T) {
defer os.Remove(caCrtPath)
generateCA(t)
customCAPath, err := ioutil.TempDir("", "test-ca-certdir")
require.NoErrorf(t, err, "error creating temporary certs dir - %s", err)
defer os.Remove(customCAPath)

generateCA(t, certCommonName, customCAPath)

SetCACertDirs([]string{customCAPath})

rootCAs := getRootCAs()
//nolint:staticcheck // func (s *CertPool) Subjects was deprecated if s was returned by SystemCertPool, Subjects
subjects := rootCAs.Subjects()
var rdnSequence pkix.RDNSequence
_, err := asn1.Unmarshal(subjects[len(subjects)-1], &rdnSequence)
_, err = asn1.Unmarshal(subjects[len(subjects)-1], &rdnSequence)
if err != nil {
t.Fatal("could not unmarshal der formatted subject")
}
Expand All @@ -56,8 +62,41 @@ func TestCustomCAsAreRegistered(t *testing.T) {
assert.Equal(t, certCommonName, name.CommonName, "certificate not found")
}

func generateCA(t *testing.T) {
err := os.MkdirAll(customCAPath, os.ModePerm)
func TestMultipleCustomCAsAreDirs(t *testing.T) {
customCAPath, err := ioutil.TempDir("", "test-ca-certdir")
require.NoErrorf(t, err, "error creating temporary certs dir - %s", err)
defer os.Remove(customCAPath)
customCAPath2, err := ioutil.TempDir("", "test-ca-certdir2")
require.NoErrorf(t, err, "error creating temporary certs dir - %s", err)
defer os.Remove(customCAPath2)

generateCA(t, certCommonName, customCAPath)
generateCA(t, certCommonName2, customCAPath2)
SetCACertDirs([]string{customCAPath, customCAPath2})

rootCAs := getRootCAs()
//nolint:staticcheck // func (s *CertPool) Subjects was deprecated if s was returned by SystemCertPool, Subjects
subjects := rootCAs.Subjects()
var rdnSequence pkix.RDNSequence
for i := 0; i < 2; i++ {
_, err = asn1.Unmarshal(subjects[len(subjects)-1-i], &rdnSequence)
if err != nil {
t.Fatal("could not unmarshal der formatted subject")
}
var name pkix.Name
name.FillFromRDNSequence(&rdnSequence)

if i == 1 {
assert.Equal(t, certCommonName, name.CommonName, "certificate not found")
} else {
assert.Equal(t, certCommonName2, name.CommonName, "certificate not found")
}
}
}

func generateCA(t *testing.T, cn, dir string) {
err := os.MkdirAll(dir, os.ModePerm)
caCrtPath := path.Join(dir, "ca.crt")
require.NoErrorf(t, err, "error generating the custom ca folder - %s", err)

ca := &x509.Certificate{
Expand All @@ -69,7 +108,7 @@ func generateCA(t *testing.T) {
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
CommonName: certCommonName,
CommonName: cn,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
Expand Down
6 changes: 6 additions & 0 deletions pkg/util/tls_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ func NewTLSConfigWithPassword(clientCert, clientKey, clientKeyPassword, caCert s
return config, nil
}

// SetCACertDirs sets location(s) containing CA certificates which should be trusted for
// all future calls to CreateTLSClientConfig
func SetCACertDirs(caCertDirs []string) {
setCACertDirs(caCertDirs)
}

// NewTLSConfig returns a *tls.Config using the given ceClient cert, ceClient key,
// and CA certificate. If none are appropriate, a nil *tls.Config is returned.
func NewTLSConfig(clientCert, clientKey, caCert string, unsafeSsl bool) (*tls.Config, error) {
Expand Down

0 comments on commit 8312549

Please sign in to comment.