This is an automated email from the ASF dual-hosted git repository. pcongiusti pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit c1783f43e9b6056bfa1e97a1baae1605b3d81e2e Author: Pranjul Kalsi <[email protected]> AuthorDate: Sat Dec 13 23:16:36 2025 +0530 feat(jvm): add caCert option for trusted root certificates --- docs/modules/traits/pages/jvm.adoc | 39 ++++++ e2e/common/traits/jvm_test.go | 66 ++++++++++ helm/camel-k/crds/camel-k-crds.yaml | 80 ++++++++++++ pkg/apis/camel/v1/trait/jvm.go | 6 + .../camel.apache.org_integrationplatforms.yaml | 20 +++ .../camel.apache.org_integrationprofiles.yaml | 20 +++ .../crd/bases/camel.apache.org_integrations.yaml | 20 +++ .../config/crd/bases/camel.apache.org_pipes.yaml | 20 +++ pkg/trait/jvm.go | 138 +++++++++++++++++++++ pkg/trait/jvm_test.go | 55 ++++++++ 10 files changed, 464 insertions(+) diff --git a/docs/modules/traits/pages/jvm.adoc b/docs/modules/traits/pages/jvm.adoc index 65076b9e5..fd82ed728 100755 --- a/docs/modules/traits/pages/jvm.adoc +++ b/docs/modules/traits/pages/jvm.adoc @@ -62,6 +62,14 @@ Deprecated: no longer in use. | []string | A list of JVM agents to download and execute with format `<agent-name>;<agent-url>[;<jvm-agent-options>]`. +| jvm.ca-cert +| string +| A reference to a Secret containing CA certificate(s) to be trusted by the JVM. The secret should contain PEM-encoded certificates. Example: `secret:my-ca-certs` or `secret:my-ca-certs/custom-ca.crt` + +| jvm.ca-cert-mount-path +| string +| The path where the generated truststore will be mounted. Default: `/etc/camel/conf.d/_truststore` + |=== // End of autogenerated code - DO NOT EDIT! (configuration) @@ -94,3 +102,34 @@ You can use `jvm.classpath` configuration with dependencies available externally kubectl create configmap my-dep --from-file=sample-1.0.jar ... $ kamel run --resource configmap:my-dep -t jvm.classpath=/etc/camel/resources/my-dep/sample-1.0.jar MyApp.java + +== Trusting Custom CA Certificates + +When connecting to services that use TLS with certificates signed by a private CA (e.g., internal Elasticsearch, Kafka, or databases), you can use the `ca-cert` option to add the CA certificate to the JVM's truststore. + +First, create a Kubernetes Secret containing the CA certificate: + +[source,console] +---- +kubectl create secret generic my-private-ca --from-file=ca.crt=/path/to/ca-certificate.pem +---- + +Then reference the secret when running the integration: + +[source,console] +---- +$ kamel run MyRoute.java -t jvm.ca-cert=secret:my-private-ca +---- + +If your certificate is stored under a different key in the secret: + +[source,console] +---- +$ kamel run MyRoute.java -t jvm.ca-cert=secret:my-private-ca/custom-ca.pem +---- + +This will automatically: + +1. Mount the CA certificate secret +2. Generate a JVM truststore using an init container +3. Configure the JVM to use the generated truststore via `-Djavax.net.ssl.trustStore` diff --git a/e2e/common/traits/jvm_test.go b/e2e/common/traits/jvm_test.go index a6456c324..274e804f0 100644 --- a/e2e/common/traits/jvm_test.go +++ b/e2e/common/traits/jvm_test.go @@ -24,8 +24,15 @@ package common import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" "os" "testing" + "time" . "github.com/onsi/gomega" "github.com/stretchr/testify/require" @@ -83,5 +90,64 @@ func TestJVMTrait(t *testing.T) { g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) g.Eventually(IntegrationLogs(t, ctx, ns, name), TestTimeoutShort).Should(ContainSubstring("Hello World!")) }) + + t.Run("JVM trait CA cert", func(t *testing.T) { + // Generate a valid self-signed certificate + certPem, err := generateSelfSignedCert() + require.NoError(t, err) + + caCertData := make(map[string]string) + caCertData["ca.crt"] = string(certPem) + + err = CreatePlainTextSecret(t, ctx, ns, "test-ca-cert", caCertData) + require.NoError(t, err) + + name := RandomizedSuffixName("cacert") + g.Expect(KamelRun(t, ctx, ns, + "./files/Java.java", + "--name", name, + "-t", "jvm.ca-cert=secret:test-ca-cert", + ).Execute()).To(Succeed()) + + g.Eventually(IntegrationPodPhase(t, ctx, ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning)) + g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + + // Verify init container was added + pod := IntegrationPod(t, ctx, ns, name)() + g.Expect(pod).NotTo(BeNil()) + initContainerNames := make([]string, 0) + for _, c := range pod.Spec.InitContainers { + initContainerNames = append(initContainerNames, c.Name) + } + g.Expect(initContainerNames).To(ContainElement("generate-truststore")) + }) }) } + +// Helper to generate a self-signed certificate for testing +func generateSelfSignedCert() ([]byte, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Test Co"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil +} diff --git a/helm/camel-k/crds/camel-k-crds.yaml b/helm/camel-k/crds/camel-k-crds.yaml index e2c43f338..d480cd3de 100644 --- a/helm/camel-k/crds/camel-k-crds.yaml +++ b/helm/camel-k/crds/camel-k-crds.yaml @@ -4721,6 +4721,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -7114,6 +7124,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -9409,6 +9429,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -11681,6 +11711,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -20787,6 +20827,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -23013,6 +23063,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -33481,6 +33541,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -35639,6 +35709,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) diff --git a/pkg/apis/camel/v1/trait/jvm.go b/pkg/apis/camel/v1/trait/jvm.go index 5ce4353c8..1f17f35a7 100644 --- a/pkg/apis/camel/v1/trait/jvm.go +++ b/pkg/apis/camel/v1/trait/jvm.go @@ -42,4 +42,10 @@ type JVMTrait struct { Jar string `json:"jar,omitempty" property:"jar"` // A list of JVM agents to download and execute with format `<agent-name>;<agent-url>[;<jvm-agent-options>]`. Agents []string `json:"agents,omitempty" property:"agents"` + // The secret should contain PEM-encoded certificates. + // Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + CACert string `json:"caCert,omitempty" property:"ca-cert"` + // The path where the generated truststore will be mounted + // Default: "/etc/camel/conf.d/_truststore" + CACertMountPath string `json:"caCertMountPath,omitempty" property:"ca-cert-mount-path"` } diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml index e32934e26..eb8324738 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml @@ -1472,6 +1472,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -3865,6 +3875,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml index 2ea56647c..8e6205d2e 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml @@ -1340,6 +1340,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -3612,6 +3622,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml index deec05fe1..4f57d79dd 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml @@ -8154,6 +8154,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -10380,6 +10390,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) diff --git a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml index a922f1582..1ad74cde2 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml @@ -8210,6 +8210,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) @@ -10368,6 +10378,16 @@ spec: items: type: string type: array + caCert: + description: |- + The secret should contain PEM-encoded certificates. + Example: "secret:my-ca-certs" or "secret:my-ca-certs/custom-ca.crt" + type: string + caCertMountPath: + description: |- + The path where the generated truststore will be mounted + Default: "/etc/camel/conf.d/_truststore" + type: string classpath: description: Additional JVM classpath (use `Linux` classpath separator) diff --git a/pkg/trait/jvm.go b/pkg/trait/jvm.go index a9939d25b..9923b379b 100644 --- a/pkg/trait/jvm.go +++ b/pkg/trait/jvm.go @@ -18,12 +18,14 @@ limitations under the License. package trait import ( + "errors" "fmt" "net/url" "path/filepath" "sort" "strings" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -47,6 +49,10 @@ const ( defaultMaxMemoryPercentage = int64(50) lowMemoryThreshold = 300 lowMemoryMAxMemoryDefaultPercentage = int64(25) + defaultCACertMountPath = "/etc/camel/conf.d/_truststore" + caCertVolumeName = "jvm-truststore" + caCertSecretVolumeName = "ca-cert-secret" //nolint:gosec // G101: not a credential, just a volume name + trustStoreName = "truststore.jks" ) type jvmTrait struct { @@ -158,6 +164,14 @@ func (t *jvmTrait) Apply(e *Environment) error { args = append(args, httpProxyArgs...) } + caCertArgs, err := t.configureCaCert(e) + if err != nil { + return err + } + if caCertArgs != nil { + args = append(args, caCertArgs...) + } + return t.feedContainer(container, args, e) } @@ -369,3 +383,127 @@ func getLegacyCamelQuarkusDependenciesPaths() *sets.Set { return s } + +// parseSecretRef parses a secret reference in the format "secret:name" or "secret:name/key". +func parseSecretRef(ref string) (string, string, error) { + if !strings.HasPrefix(ref, "secret:") { + return "", "", fmt.Errorf("invalid CA cert reference %q: must start with 'secret:'", ref) + } + + ref = strings.TrimPrefix(ref, "secret:") + parts := strings.SplitN(ref, "/", 2) + secretName, secretKey := parts[0], "" + + if len(parts) > 1 { + secretKey = parts[1] + } + if secretName == "" { + return "", "", errors.New("invalid CA cert reference: secret name is empty") + } + + return secretName, secretKey, nil +} + +// configureCACert sets up the truststore for CA certificates. +func (t *jvmTrait) configureCaCert(e *Environment) ([]string, error) { + if t.CACert == "" { + return nil, nil + } + + secretName, secretKey, err := parseSecretRef(t.CACert) + if err != nil { + return nil, err + } + + if secretKey == "" { + secretKey = "ca.crt" + } + + mountPath := defaultCACertMountPath + if t.CACertMountPath != "" { + mountPath = t.CACertMountPath + } + + // Use a deterministic password based on integration name to avoid + // changing the deployment spec on every reconciliation cycle. + // For a truststore i.e public CA certs only, security of this password is not critical. + trustStorePass := "camelk-" + e.Integration.Name + trustStorePath := filepath.Join(mountPath, trustStoreName) + + // add secret volume. + secretVolume := corev1.Volume{ + Name: caCertSecretVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + } + + // add an emptyDir volume. + trustStoreVolume := corev1.Volume{ + Name: caCertVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + + // add volumes to deployment. + e.Resources.VisitDeployment(func(deployment *appsv1.Deployment) { + deployment.Spec.Template.Spec.Volumes = append( + deployment.Spec.Template.Spec.Volumes, + secretVolume, trustStoreVolume, + ) + }) + + // add mount to integration container + container := e.GetIntegrationContainer() + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: caCertVolumeName, + MountPath: mountPath, + ReadOnly: true, + }) + + initContainer := corev1.Container{ + Name: "generate-truststore", + Image: container.Image, + ImagePullPolicy: container.ImagePullPolicy, + Command: []string{ + "keytool", + "-importcert", + "-noprompt", + "-alias", + "custom-ca", + "-storepass", + trustStorePass, + "-keystore", + trustStorePath, + "-file", + filepath.Join("/etc/secrets/cacert", secretKey), + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: caCertSecretVolumeName, + MountPath: "/etc/secrets/cacert", + ReadOnly: true, + }, + { + Name: caCertVolumeName, + MountPath: mountPath, + }, + }, + } + + // add to deployment container + e.Resources.VisitDeployment(func(deployment *appsv1.Deployment) { + deployment.Spec.Template.Spec.InitContainers = append( + deployment.Spec.Template.Spec.InitContainers, + initContainer, + ) + }) + + return []string{ + "-Djavax.net.ssl.trustStore=" + trustStorePath, + "-Djavax.net.ssl.trustStorePassword=" + trustStorePass, + }, nil +} diff --git a/pkg/trait/jvm_test.go b/pkg/trait/jvm_test.go index 35f35ecb2..cb9c99730 100644 --- a/pkg/trait/jvm_test.go +++ b/pkg/trait/jvm_test.go @@ -719,3 +719,58 @@ func TestApplyJvmTraitAgentFail(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "could not parse JVM agent") } + +func TestApplyJvmTraitWithCACert(t *testing.T) { + trait, environment := createNominalJvmTest(v1.IntegrationKitTypePlatform) + trait.CACert = "secret:my-ca-secret" + + d := appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: defaultContainerName, + }, + }, + }, + }, + }, + } + + environment.Resources.Add(&d) + configure, condition, err := trait.Configure(environment) + require.NoError(t, err) + assert.True(t, configure) + assert.Nil(t, condition) + + err = trait.Apply(environment) + require.NoError(t, err) + + assert.Contains(t, d.Spec.Template.Spec.Containers[0].Args, "-Djavax.net.ssl.trustStore=/etc/camel/conf.d/_truststore/truststore.jks") + assert.Len(t, d.Spec.Template.Spec.Volumes, 2) + assert.Equal(t, "ca-cert-secret", d.Spec.Template.Spec.Volumes[0].Name) + assert.Equal(t, "jvm-truststore", d.Spec.Template.Spec.Volumes[1].Name) + assert.Len(t, d.Spec.Template.Spec.InitContainers, 1) + assert.Equal(t, "generate-truststore", d.Spec.Template.Spec.InitContainers[0].Name) +} + +func TestParseSecretRef(t *testing.T) { + name, key, err := parseSecretRef("secret:my-secret") + require.NoError(t, err) + assert.Equal(t, "my-secret", name) + assert.Equal(t, "", key) + + name, key, err = parseSecretRef("secret:my-secret/ca.crt") + require.NoError(t, err) + assert.Equal(t, "my-secret", name) + assert.Equal(t, "ca.crt", key) + + _, _, err = parseSecretRef("configmap:my-cm") + require.Error(t, err) + assert.Contains(t, err.Error(), "must start with 'secret:'") + + _, _, err = parseSecretRef("secret:") + require.Error(t, err) + assert.Contains(t, err.Error(), "secret name is empty") +}
