From 6056a65ebca525221845bd4abd9ed3ddbc8e6d09 Mon Sep 17 00:00:00 2001 From: Frank Spitulski Date: Wed, 27 May 2026 23:57:27 -0700 Subject: [PATCH] fix(chart): mount mTLS CA for auth-callout Signed-off-by: Frank Spitulski --- auth-callout/deploy/README.md | 41 ++++++++- auth-callout/deploy/templates/deployment.yaml | 12 +++ auth-callout/deploy/values.yaml | 16 ++++ auth-callout/src/internal/auth/auth_test.go | 86 +++++++++++++++---- deploy/README.md | 5 +- deploy/nats-event-bus/values.yaml | 24 ++++++ docs/authentication.md | 8 +- local/nats/k8s/local-dev-values.yaml | 13 --- 8 files changed, 165 insertions(+), 40 deletions(-) diff --git a/auth-callout/deploy/README.md b/auth-callout/deploy/README.md index d550925c..b15e7e80 100644 --- a/auth-callout/deploy/README.md +++ b/auth-callout/deploy/README.md @@ -88,12 +88,49 @@ serviceConfig: ### mTLS Configuration -For mTLS authentication, configure the CA certificate path: +For mTLS authentication, mount the CA Secret and set the auth-callout CA path: ```yaml serviceConfig: mtls: - ca-path: "/etc/ssl/certs/ca.crt" + ca-path: "/etc/mtls-ca/ca.crt" + +extraVolumeMounts: + - name: mtls-ca + mountPath: /etc/mtls-ca + readOnly: true + +extraVolumes: + - name: mtls-ca + secret: + secretName: my-mtls-ca + items: + - key: ca.crt + path: ca.crt +``` + +Parent charts can use templated snippets when the mount depends on values +visible to this subchart, such as `global`: + +```yaml +extraVolumeMountTemplates: + - | + {{- if .Values.global.eventBus.mtls.enabled }} + - name: mtls-ca + mountPath: /etc/mtls-ca + readOnly: true + {{- end }} + +extraVolumeTemplates: + - | + {{- if .Values.global.eventBus.mtls.enabled }} + - name: mtls-ca + secret: + secretName: my-mtls-ca + items: + - key: ca.crt + path: ca.crt + {{- end }} ``` ## Permissions Configuration diff --git a/auth-callout/deploy/templates/deployment.yaml b/auth-callout/deploy/templates/deployment.yaml index d275c764..4d41f1e1 100644 --- a/auth-callout/deploy/templates/deployment.yaml +++ b/auth-callout/deploy/templates/deployment.yaml @@ -116,6 +116,12 @@ spec: {{- with .Values.extraVolumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} + {{- range $template := .Values.extraVolumeMountTemplates }} + {{- $rendered := tpl $template $ | trim }} + {{- if $rendered }} +{{ $rendered | nindent 12 }} + {{- end }} + {{- end }} {{- if .Values.healthChecks.livenessProbe.enabled }} livenessProbe: @@ -169,3 +175,9 @@ spec: {{- with .Values.extraVolumes }} {{- toYaml . | nindent 8 }} {{- end }} + {{- range $template := .Values.extraVolumeTemplates }} + {{- $rendered := tpl $template $ | trim }} + {{- if $rendered }} +{{ $rendered | nindent 8 }} + {{- end }} + {{- end }} diff --git a/auth-callout/deploy/values.yaml b/auth-callout/deploy/values.yaml index 7152d39d..6d16380b 100644 --- a/auth-callout/deploy/values.yaml +++ b/auth-callout/deploy/values.yaml @@ -55,6 +55,14 @@ extraVolumes: # configMap: # name: my-configmap +# Additional templated volume snippets. +# Use when volume presence depends on values visible to this chart. +extraVolumeTemplates: [] + # - | + # - name: generated-volume + # secret: + # secretName: generated-secret + # Additional volume mounts to be added to the container extraVolumeMounts: [] @@ -64,6 +72,14 @@ extraVolumeMounts: # mountPath: /etc/secrets # readOnly: true +# Additional templated volume mount snippets. +# Use when mount presence depends on values visible to this chart. +extraVolumeMountTemplates: [] + # - | + # - name: generated-volume + # mountPath: /etc/generated + # readOnly: true + podAnnotations: {} podLabels: {} diff --git a/auth-callout/src/internal/auth/auth_test.go b/auth-callout/src/internal/auth/auth_test.go index db657c13..1ec6bd58 100644 --- a/auth-callout/src/internal/auth/auth_test.go +++ b/auth-callout/src/internal/auth/auth_test.go @@ -7,7 +7,11 @@ import ( "context" "crypto/rand" "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" + "encoding/pem" + "math/big" "net/http" "net/http/httptest" "os" @@ -476,7 +480,6 @@ func TestOAuth2RequiredScope(t *testing.T) { // TestMTLSAuthentication tests mTLS client certificate authentication func TestMTLSAuthentication(t *testing.T) { - // Create a test permissions file permFile := createTestPermissionsFile(t) defer os.Remove(permFile) @@ -484,28 +487,24 @@ func TestMTLSAuthentication(t *testing.T) { require.NoError(t, err) defer pm.Close() - // Initialize mTLS authenticator without CA (for testing) - mtlsAuth, err := NewMTLSAuthenticator(nil, pm, testLogger(), testServiceName) - require.NoError(t, err) + caPEM, caKey := createTestCA(t) + clientCertPEM := createClientCert(t, "device1", caPEM, caKey) - // Test certificate (self-signed for testing) - testCertPEM := `-----BEGIN CERTIFICATE----- -MIICxjCCAa4CCQDFPx3qvE6Y1DANBgkqhkiG9w0BAQsFADAkMQswCQYDVQQGEwJV -UzEVMBMGA1UEAwwMQ049ZGV2aWNlMQ== ------END CERTIFICATE-----` + mtlsAuth, err := NewMTLSAuthenticator(caPEM, pm, testLogger(), testServiceName) + require.NoError(t, err) - // This would fail without a valid cert, but tests the flow - profile, err := mtlsAuth.Authenticate(context.Background(), testCertPEM) - if err != nil { - t.Logf("mTLS authentication failed (expected with test cert): %v", err) - return - } + profile, err := mtlsAuth.Authenticate(context.Background(), clientCertPEM) + require.NoError(t, err) + require.NotNil(t, profile) + assert.Equal(t, "device1", profile.Name) + assert.Equal(t, "APP1", profile.Account) - if profile == nil { - t.Error("Expected non-nil profile") - } + otherCAPEM, otherCAKey := createTestCA(t) + untrustedCertPEM := createClientCert(t, "device1", otherCAPEM, otherCAKey) - t.Logf("mTLS authentication successful for profile: %s", profile.Name) + _, err = mtlsAuth.Authenticate(context.Background(), untrustedCertPEM) + require.Error(t, err) + assert.Contains(t, err.Error(), "certificate validation failed") } // TestNKeyAuthentication tests NKey authentication @@ -768,3 +767,52 @@ func createTestPermissionsFile(t *testing.T) string { tmpFile.Close() return tmpFile.Name() } + +func createTestCA(t *testing.T) ([]byte, *rsa.PrivateKey) { + t.Helper() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().UnixNano()), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: time.Now().Add(-time.Minute), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), key +} + +func createClientCert(t *testing.T, commonName string, caPEM []byte, caKey *rsa.PrivateKey) string { + t.Helper() + + block, _ := pem.Decode(caPEM) + require.NotNil(t, block) + + caCert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().UnixNano()), + Subject: pkix.Name{CommonName: commonName}, + NotBefore: time.Now().Add(-time.Minute), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + der, err := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey) + require.NoError(t, err) + + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) +} diff --git a/deploy/README.md b/deploy/README.md index 7f217716..76214d48 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -371,6 +371,8 @@ global: enabled: true # default ``` +When enabled, the chart mounts the `nats-mtls-server-tls` Secret's `ca.crt` into auth-callout at `/etc/mtls-ca/ca.crt` and sets `AUTH_CALLOUT_MTLS_CA_PATH` to that file. + Set `global.eventBus.mtls.enabled: false` to disable the mTLS NATS cluster. When disabled: - The `nats-mtls` subchart is not rendered (no pods, services, or config) @@ -378,6 +380,7 @@ Set `global.eventBus.mtls.enabled: false` to disable the mTLS NATS cluster. When - The `nats-mtls-accounts-config` ConfigMap is not created - mTLS-specific keys are omitted from `nats-env-config` - mTLS leaf nkey entries are omitted from the auth-callout permissions +- The mTLS CA Secret is not mounted into auth-callout - The mTLS secrets (`nats-mtls-server-tls`, `nats-mtls-leaf`, `nats-mtls-authx-leaf`, `nats-mtls-sys-leaf`) are not required ## Subchart Configuration @@ -551,8 +554,6 @@ auth-callout: url: "https://keycloak.example.com/realms/event-bus/protocol/openid-connect/certs" issuer: "https://keycloak.example.com/realms/event-bus" audience: "dsx-exchange" - mtls: - ca-path: "/etc/mtls-ca/ca.crt" ``` ### CSC Cluster diff --git a/deploy/nats-event-bus/values.yaml b/deploy/nats-event-bus/values.yaml index 9ecda04b..98afb57b 100644 --- a/deploy/nats-event-bus/values.yaml +++ b/deploy/nats-event-bus/values.yaml @@ -525,6 +525,30 @@ auth-callout: extraEnvTemplates: - | {{ include "nats-event-bus.authCalloutEventBusEnv" . }} + {{- if .Values.global.eventBus.mtls.enabled }} + - name: AUTH_CALLOUT_MTLS_CA_PATH + value: "/etc/mtls-ca/ca.crt" + {{- end }} + + # Umbrella-owned mTLS endpoint: wire auth-callout CA only when enabled. + extraVolumeMountTemplates: + - | + {{- if .Values.global.eventBus.mtls.enabled }} + - name: mtls-ca + mountPath: /etc/mtls-ca + readOnly: true + {{- end }} + + extraVolumeTemplates: + - | + {{- if .Values.global.eventBus.mtls.enabled }} + - name: mtls-ca + secret: + secretName: nats-mtls-server-tls + items: + - key: ca.crt + path: ca.crt + {{- end }} # Operator-defined auth-callout env vars. # The generated permissions use nkey names leaf-cpc-{id} and diff --git a/docs/authentication.md b/docs/authentication.md index eeeaae79..ebc12e12 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -52,13 +52,13 @@ auth-callout: BMS and OT devices connect to the mTLS NATS endpoint (port 8883) with a client certificate. TLS is terminated at the NATS pod (the Gateway API controller uses TCP passthrough for this listener). The auth-callout extracts the certificate's Common Name and matches it to a permissions entry. -Configure the CA certificate path: +The event-bus chart enables the mTLS endpoint by default. When `global.eventBus.mtls.enabled: true`, it mounts `nats-mtls-server-tls` into auth-callout and sets `AUTH_CALLOUT_MTLS_CA_PATH` automatically: ```yaml -auth-callout: - serviceConfig: +global: + eventBus: mtls: - ca-path: "/etc/mtls-ca/ca.crt" + enabled: true ``` ### NKey diff --git a/local/nats/k8s/local-dev-values.yaml b/local/nats/k8s/local-dev-values.yaml index 64c660dd..bf9fcc26 100644 --- a/local/nats/k8s/local-dev-values.yaml +++ b/local/nats/k8s/local-dev-values.yaml @@ -27,19 +27,6 @@ auth-callout: url: "http://172.18.200.1/realms/event-bus/protocol/openid-connect/certs" issuer: "http://172.18.200.1/realms/event-bus" audience: "dsx-exchange" - # CA certificate for mTLS client verification - mtls: - ca-path: "/etc/mtls-ca/ca.crt" observability: tracing: enabled: false - - # Mount CA certificate from TLS secret for mTLS client verification - extraVolumes: - - name: mtls-ca - secret: - secretName: nats-mtls-server-tls - extraVolumeMounts: - - name: mtls-ca - mountPath: /etc/mtls-ca - readOnly: true