diff --git a/CHANGELOG.md b/CHANGELOG.md index ad70d71c..db09adc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. - Support configuring the name of the key in the ConfigMap/Secret, in which the PEM encoded CA certificate of the Truststore should be placed. This is e.g. needed to be able to use the generated Secret within an OpenShift Ingress ([#679]). +- Support adding domain components to the subject DN of TLS certificates with the volume annotation + `secrets.stackable.tech/backend.autotls.cert.domain-components-in-subject-dn` ([#708]). ### Changed @@ -19,6 +21,7 @@ All notable changes to this project will be documented in this file. [#693]: https://github.com/stackabletech/secret-operator/pull/693 [#706]: https://github.com/stackabletech/secret-operator/pull/706 +[#708]: https://github.com/stackabletech/secret-operator/pull/708 ## [26.3.0] - 2026-03-16 diff --git a/docs/modules/secret-operator/pages/volume.adoc b/docs/modules/secret-operator/pages/volume.adoc index 067ddf51..743905c3 100644 --- a/docs/modules/secret-operator/pages/volume.adoc +++ b/docs/modules/secret-operator/pages/volume.adoc @@ -166,6 +166,36 @@ For example, given a requested lifetime of 1 day and a jitter factor of 0.2, the Jittering may be disabled by setting the jitter factor to 0. +=== `secrets.stackable.tech/backend.autotls.cert.domain-components-in-subject-dn` + +*Required*: false + +*Default value*: `"false"` + +*Backends*: xref:secretclass.adoc#backend-autotls[] + +If set to `"true"`, the domain components of the Pod's fully qualified domain name (FQDN) are appended to the subject distinguished name (DN) of the TLS certificate. + +For example, the subject DN could look as follows: + +``` +CN=generated certificate for pod, DC=my-pod-0, DC=my-statefulset-service, DC=my-namespace, DC=svc, DC=cluster, DC=local +``` + +[NOTE] +==== +Many products use the string representation of distinguished names as described in https://www.ietf.org/rfc/rfc4514.txt[RFC 4514{external-link-icon}^]. +The string representation starts with the last element of the sequence and moving backwards toward the first. + +The example above becomes: + +``` +DC=local,DC=cluster,DC=svc,DC=my-namespace,DC=my-statefulset-service,DC=my-pod-0,CN=generated certificate for pod +``` + +Attribute names can be in upper or lower case, with or without spaces between them. +==== + === `secrets.stackable.tech/backend.cert-manager.cert.lifetime` *Required*: false diff --git a/rust/operator-binary/src/backend/auto_tls/mod.rs b/rust/operator-binary/src/backend/auto_tls/mod.rs index dfa9d424..27acb336 100644 --- a/rust/operator-binary/src/backend/auto_tls/mod.rs +++ b/rust/operator-binary/src/backend/auto_tls/mod.rs @@ -355,6 +355,21 @@ impl SecretBackend for TlsGenerate { } } + let domain_components = if selector.autotls_cert_domain_components_in_subject_dn { + [ + Some(pod_info.pod_name.as_str()), + pod_info.service_name.as_deref(), + Some(&pod_info.namespace), + Some("svc"), + ] + .into_iter() + .flatten() + .chain(pod_info.kubernetes_cluster_domain.split('.')) + .collect() + } else { + vec![] + }; + let pod_cert = X509Builder::new() .and_then(|mut x509| { let subject_name = X509NameBuilder::new() @@ -363,6 +378,12 @@ impl SecretBackend for TlsGenerate { Nid::COMMONNAME, "generated certificate for pod", )?; + for domain_component in domain_components { + name.append_entry_by_nid( + Nid::DOMAINCOMPONENT, + domain_component, + )?; + } Ok(name) })? .build(); diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index 8d57c774..a2e036c7 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -129,6 +129,19 @@ pub struct SecretVolumeSelector { )] pub autotls_cert_jitter_factor: f64, + /// If set to `"true"`, the domain components of the Pod's fully qualified domain name (FQDN) + /// are appended to the subject distinguished name (DN) of the TLS certificate. + /// + /// For example, the subject DN could look as follows: + /// + /// `CN=generated certificate for pod, DC=my-pod-0, DC=my-statefulset-service, DC=my-namespace, DC=svc, DC=cluster, DC=local` + #[serde( + rename = "secrets.stackable.tech/backend.autotls.cert.domain-components-in-subject-dn", + deserialize_with = "SecretVolumeSelector::deserialize_str_as_bool", + default + )] + pub autotls_cert_domain_components_in_subject_dn: bool, + /// The TLS cert lifetime (when using the [`cert_manager`] backend). /// /// The format is documented in . @@ -301,6 +314,16 @@ impl SecretVolumeSelector { ) }) } + + fn deserialize_str_as_bool<'de, D: Deserializer<'de>>(de: D) -> Result { + let str = String::deserialize(de)?; + str.parse().map_err(|_| { + ::invalid_value( + Unexpected::Str(&str), + &"a string containing a boolean", + ) + }) + } } #[derive(Debug)] diff --git a/rust/operator-binary/src/backend/pod_info.rs b/rust/operator-binary/src/backend/pod_info.rs index d4236ff5..2c1e44d2 100644 --- a/rust/operator-binary/src/backend/pod_info.rs +++ b/rust/operator-binary/src/backend/pod_info.rs @@ -104,7 +104,9 @@ pub enum FromPodError { #[derive(Debug)] pub struct PodInfo { pub pod_ips: Vec, + pub pod_name: String, pub service_name: Option, + pub namespace: String, pub node_name: String, pub node_ips: Vec, pub listener_addresses: Option, @@ -119,6 +121,8 @@ impl PodInfo { scopes: &[SecretScope], ) -> Result { use from_pod_error::*; + let pod_name = pod.metadata.name.clone().context(NoPodNameSnafu)?; + let namespace = pod.metadata.namespace.clone().context(NoNamespaceSnafu)?; let node_name = pod .spec .as_ref() @@ -150,7 +154,9 @@ impl PodInfo { .context(from_pod_error::IllegalAddressSnafu { address: ip }) }) .collect::>()?, + pod_name, service_name: pod.spec.as_ref().and_then(|spec| spec.subdomain.clone()), + namespace, node_name, node_ips: node .status diff --git a/tests/templates/kuttl/tls/10_consumer.yaml.j2 b/tests/templates/kuttl/tls/10_consumer.yaml.j2 index 8a8f4b32..544d2d0b 100644 --- a/tests/templates/kuttl/tls/10_consumer.yaml.j2 +++ b/tests/templates/kuttl/tls/10_consumer.yaml.j2 @@ -22,7 +22,7 @@ spec: CERT_NAME=tls.crt CA_NAME=ca.crt {% endif %} - - | + set -euo pipefail ls -la /stackable/tls-3d ls -la /stackable/tls-42h @@ -67,11 +67,18 @@ spec: assert_trusted_roots_contain "cert 4b" assert_trusted_roots_contain "cert 5" assert_trusted_roots_contain "cert 6" + + echo Test subject DN with domain components + + openssl x509 -in /stackable/tls-domain-components/tls.crt -subject -noout | \ + grep "subject=CN=generated certificate for pod, DC=tls-consumer-.*, DC=$NAMESPACE, DC=svc, DC=.*" volumeMounts: - mountPath: /stackable/tls-3d name: tls-3d - mountPath: /stackable/tls-42h name: tls-42h + - mountPath: /stackable/tls-domain-components + name: tls-domain-components volumes: - name: tls-3d ephemeral: @@ -111,6 +118,21 @@ spec: resources: requests: storage: "1" + - name: tls-domain-components + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls-$NAMESPACE + secrets.stackable.tech/scope: pod + secrets.stackable.tech/backend.autotls.cert.domain-components-in-subject-dn: "true" + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" securityContext: runAsUser: 1000 runAsGroup: 1000