diff --git a/docs/modules/secret-operator/pages/scope.adoc b/docs/modules/secret-operator/pages/scope.adoc index 70ff91f0..b8920873 100644 --- a/docs/modules/secret-operator/pages/scope.adoc +++ b/docs/modules/secret-operator/pages/scope.adoc @@ -59,5 +59,6 @@ For example, a TLS certificate provisioned by the xref:secretclass.adoc#backend- xref:#node[] and xref:#pod[] would contain the following values in its `subjectAlternateName` (SAN) extension field: * The node's IP address -* The node's fully qualified domain name (`my-node.example.com`) -* The pod's fully qualified domain name (`my-pod.my-service.my-namespace.svc.cluster.local`) +* The node's domain name (`my-node.example.com`) +* The pod's domain name (`my-pod.my-service.my-namespace.svc.cluster.local`) +* The pod's fully qualified domain name (`my-pod.my-service.my-namespace.svc.cluster.local.`) diff --git a/rust/operator-binary/Cargo.toml b/rust/operator-binary/Cargo.toml index 3058dbe7..ebd80f80 100644 --- a/rust/operator-binary/Cargo.toml +++ b/rust/operator-binary/Cargo.toml @@ -23,6 +23,7 @@ p12.workspace = true pin-project.workspace = true prost-types.workspace = true prost.workspace = true +rand.workspace = true serde_json.workspace = true serde.workspace = true snafu.workspace = true @@ -39,7 +40,6 @@ tonic.workspace = true tracing.workspace = true uuid.workspace = true yasna.workspace = true -rand.workspace = true [dev-dependencies] serde_yaml.workspace = true diff --git a/rust/operator-binary/src/backend/kerberos_keytab.rs b/rust/operator-binary/src/backend/kerberos_keytab.rs index 9bce7f3f..122293bf 100644 --- a/rust/operator-binary/src/backend/kerberos_keytab.rs +++ b/rust/operator-binary/src/backend/kerberos_keytab.rs @@ -140,43 +140,13 @@ impl SecretBackend for KerberosKeytab { pod_info: super::pod_info::PodInfo, ) -> Result { let Self { - profile: - KerberosProfile { - realm_name, - kdc, - admin, - }, - admin_keytab, admin_principal, + admin_keytab, + profile: KerberosProfile { admin, .. }, } = self; - - let admin_server_clause = match admin { - KerberosKeytabBackendAdmin::Mit { kadmin_server } => { - format!(" admin_server = {kadmin_server}") - } - KerberosKeytabBackendAdmin::ActiveDirectory { .. } => String::new(), - }; + let profile = self.kerberos_profile(&pod_info.kubernetes_cluster_domain); let tmp = tempdir().context(TempSetupSnafu)?; - let profile = format!( - r#" -[libdefaults] -default_realm = {realm_name} -rdns = false -dns_canonicalize_hostnames = false -udp_preference_limit = 1 - -[realms] -{realm_name} = {{ - kdc = {kdc} -{admin_server_clause} -}} - -[domain_realm] -cluster.local = {realm_name} -.cluster.local = {realm_name} -"# - ); let profile_file_path = tmp.path().join("krb5.conf"); { let mut profile_file = File::create(&profile_file_path) @@ -280,3 +250,133 @@ cluster.local = {realm_name} ))) } } + +impl KerberosKeytab { + fn kerberos_profile(&self, cluster_domain: &str) -> String { + let Self { + profile: + KerberosProfile { + realm_name, + kdc, + admin, + }, + .. + } = self; + + let admin_server_clause = match admin { + KerberosKeytabBackendAdmin::Mit { kadmin_server } => { + format!(" admin_server = {kadmin_server}") + } + KerberosKeytabBackendAdmin::ActiveDirectory { .. } => String::new(), + }; + + let mut domain_realm_section = "[domain_realm]".to_owned(); + domain_realm_section.push_str(&format!( + " +{cluster_domain} = {realm_name} +.{cluster_domain} = {realm_name} +" + )); + if let Some(cluster_domain_without_trailing_dot) = cluster_domain.strip_suffix('.') { + domain_realm_section.push_str(&format!( + "{cluster_domain_without_trailing_dot} = {realm_name} +.{cluster_domain_without_trailing_dot} = {realm_name} +" + )); + } + + format!( + r#" +[libdefaults] +default_realm = {realm_name} +rdns = false +dns_canonicalize_hostnames = false +udp_preference_limit = 1 + +[realms] +{realm_name} = {{ +kdc = {kdc} +{admin_server_clause} +}} + +{domain_realm_section} +"# + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_kerberos_profile_without_trailing_dot() { + let kerberos_keytab = construct_kerberos_keytab(); + let kerberos_profile = kerberos_keytab.kerberos_profile("cluster.local"); + assert_eq!( + kerberos_profile, + " +[libdefaults] +default_realm = MY.CORP +rdns = false +dns_canonicalize_hostnames = false +udp_preference_limit = 1 + +[realms] +MY.CORP = { +kdc = krb5-kdc + admin_server = krb5-kdc +} + +[domain_realm] +cluster.local = MY.CORP +.cluster.local = MY.CORP + +" + ); + } + + #[test] + fn test_kerberos_profile_with_trailing_dot() { + let kerberos_keytab = construct_kerberos_keytab(); + let kerberos_profile = kerberos_keytab.kerberos_profile("custom.cluster.local."); + assert_eq!( + kerberos_profile, + " +[libdefaults] +default_realm = MY.CORP +rdns = false +dns_canonicalize_hostnames = false +udp_preference_limit = 1 + +[realms] +MY.CORP = { +kdc = krb5-kdc + admin_server = krb5-kdc +} + +[domain_realm] +custom.cluster.local. = MY.CORP +.custom.cluster.local. = MY.CORP +custom.cluster.local = MY.CORP +.custom.cluster.local = MY.CORP + +" + ); + } + + fn construct_kerberos_keytab() -> KerberosKeytab { + KerberosKeytab { + profile: KerberosProfile { + realm_name: KerberosRealmName::try_from("MY.CORP".to_owned()).unwrap(), + kdc: "krb5-kdc".parse().unwrap(), + admin: KerberosKeytabBackendAdmin::Mit { + kadmin_server: "krb5-kdc".parse().unwrap(), + }, + }, + admin_keytab: Unloggable(vec![]), + admin_principal: KerberosPrincipal::try_from("stackable-secret-operator".to_owned()) + .unwrap(), + } + } +} diff --git a/rust/operator-binary/src/backend/mod.rs b/rust/operator-binary/src/backend/mod.rs index 7c2cdad6..7e871287 100644 --- a/rust/operator-binary/src/backend/mod.rs +++ b/rust/operator-binary/src/backend/mod.rs @@ -182,6 +182,14 @@ impl SecretVolumeSelector { ) -> Result, ScopeAddressesError> { use scope_addresses_error::*; let cluster_domain = &pod_info.kubernetes_cluster_domain; + + // TODO: Docs!!! + + let mut cluster_domains = vec![cluster_domain.to_string()]; + if let Some(cluster_domain_without_trailing_dot) = cluster_domain.strip_suffix('.') { + cluster_domains.push(cluster_domain_without_trailing_dot.to_owned()); + } + let namespace = &self.namespace; Ok(match scope { scope::SecretScope::Node => { @@ -192,28 +200,48 @@ impl SecretVolumeSelector { scope::SecretScope::Pod => { let mut addrs = Vec::new(); if let Some(svc_name) = &pod_info.service_name { - addrs.push(Address::Dns(format!( - "{svc_name}.{namespace}.svc.{cluster_domain}" - ))); - addrs.push(Address::Dns(format!( - "{pod}.{svc_name}.{namespace}.svc.{cluster_domain}", - pod = self.pod - ))); + for cluster_domain in cluster_domains { + addrs.push(Address::Dns(format!( + "{svc_name}.{namespace}.svc.{cluster_domain}" + ))); + addrs.push(Address::Dns(format!( + "{pod}.{svc_name}.{namespace}.svc.{cluster_domain}", + pod = self.pod + ))); + } } addrs.extend(pod_info.pod_ips.iter().copied().map(Address::Ip)); addrs } - scope::SecretScope::Service { name } => vec![Address::Dns(format!( - "{name}.{namespace}.svc.{cluster_domain}", - ))], - scope::SecretScope::ListenerVolume { name } => pod_info - .listener_addresses - .get(name) - .context(NoListenerAddressesSnafu { listener: name })? - .to_vec(), + scope::SecretScope::Service { name } => cluster_domains + .iter() + .map(|d| Address::Dns(format!("{name}.{namespace}.svc.{d}"))) + .collect(), + scope::SecretScope::ListenerVolume { name } => { + let mut addresses = pod_info + .listener_addresses + .get(name) + .context(NoListenerAddressesSnafu { listener: name })? + .to_vec(); + Self::duplicate_addresses_without_trailing_dot(&mut addresses); + + addresses + } }) } + /// Duplicates all DNS addresses having a trailing dot, so we also have an similar entry without + /// a trailing dot. + fn duplicate_addresses_without_trailing_dot(addresses: &mut Vec
) { + for addr_index in 0..addresses.len() { + if let Address::Dns(addr) = &addresses[addr_index] { + if let Some(addr_without_trailing_dot) = addr.strip_suffix('.') { + addresses.push(Address::Dns(addr_without_trailing_dot.to_owned())) + } + } + } + } + fn default_kerberos_service_names() -> Vec { vec!["HTTP".to_string()] } @@ -299,3 +327,117 @@ impl SecretBackendError for Infallible { match *self {} } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use pod_info::PodInfo; + + use super::*; + + #[test] + fn test_scope_addresses_without_trailing_dot() { + let pod_info = construct_pod_info("cluster.local"); + + assert_eq!( + calculate_scope(&pod_info, &SecretScope::Pod), + vec![ + dns("my-sts.default.svc.cluster.local"), + dns("my-sts-0.my-sts.default.svc.cluster.local"), + ip("10.0.0.42"), + ] + ); + + assert_eq!( + calculate_scope( + &pod_info, + &SecretScope::Service { + name: "my-service".to_owned() + } + ), + vec![dns("my-service.default.svc.cluster.local"),] + ); + + assert_eq!( + calculate_scope(&pod_info, &SecretScope::Node), + vec![dns("my-node"), ip("192.168.0.1"),] + ); + } + + #[test] + fn test_scope_addresses_with_trailing_dot() { + let pod_info = construct_pod_info("custom.cluster.local."); + + assert_eq!( + calculate_scope(&pod_info, &SecretScope::Pod), + vec![ + dns("my-sts.default.svc.custom.cluster.local."), + dns("my-sts-0.my-sts.default.svc.custom.cluster.local."), + dns("my-sts.default.svc.custom.cluster.local"), + dns("my-sts-0.my-sts.default.svc.custom.cluster.local"), + ip("10.0.0.42"), + ] + ); + + assert_eq!( + calculate_scope( + &pod_info, + &SecretScope::Service { + name: "my-service".to_owned() + } + ), + vec![ + dns("my-service.default.svc.custom.cluster.local."), + dns("my-service.default.svc.custom.cluster.local") + ] + ); + + assert_eq!( + calculate_scope(&pod_info, &SecretScope::Node), + vec![dns("my-node"), ip("192.168.0.1"),] + ); + } + + fn construct_pod_info(cluster_domain: &str) -> PodInfo { + PodInfo { + pod_ips: vec!["10.0.0.42".parse().unwrap()], + service_name: Some("my-sts".to_owned()), + node_name: "my-node".to_owned(), + node_ips: vec!["192.168.0.1".parse().unwrap()], + listener_addresses: HashMap::from([]), + kubernetes_cluster_domain: cluster_domain.parse().unwrap(), + scheduling: SchedulingPodInfo { + namespace: "default".to_owned(), + volume_listener_names: HashMap::new(), + has_node_scope: false, + }, + } + } + + fn calculate_scope(pod_info: &PodInfo, scope: &SecretScope) -> Vec
{ + let secret_volume_selector = construct_secret_volume_selector(); + secret_volume_selector + .scope_addresses(pod_info, scope) + .unwrap() + } + + fn dns(dns: &str) -> Address { + Address::Dns(dns.to_owned()) + } + + fn ip(ip: &str) -> Address { + Address::Ip(ip.parse().unwrap()) + } + + fn construct_secret_volume_selector() -> SecretVolumeSelector { + serde_yaml::from_str( + r#" +secrets.stackable.tech/class: tls +csi.storage.k8s.io/pod.name: my-sts-0 +csi.storage.k8s.io/pod.namespace: default + "#, + ) + .unwrap() + } +} diff --git a/rust/operator-binary/src/backend/pod_info.rs b/rust/operator-binary/src/backend/pod_info.rs index 2185974e..3103cec1 100644 --- a/rust/operator-binary/src/backend/pod_info.rs +++ b/rust/operator-binary/src/backend/pod_info.rs @@ -176,7 +176,7 @@ impl PodInfo { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Address { Dns(String), Ip(IpAddr), diff --git a/tests/templates/kuttl/cert-manager-tls/consumer.yaml b/tests/templates/kuttl/cert-manager-tls/consumer.yaml index 73ee1189..e175f3d3 100644 --- a/tests/templates/kuttl/cert-manager-tls/consumer.yaml +++ b/tests/templates/kuttl/cert-manager-tls/consumer.yaml @@ -19,6 +19,7 @@ spec: ls -la /stackable/tls cat /stackable/tls/tls.crt | openssl x509 -noout -text cat /stackable/tls/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local" + cat /stackable/tls/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local\." volumeMounts: - mountPath: /stackable/tls name: tls diff --git a/tests/templates/kuttl/kerberos/01-install-kdc.yaml.j2 b/tests/templates/kuttl/kerberos/01-install-kdc.yaml.j2 index 703b6808..61f36dea 100644 --- a/tests/templates/kuttl/kerberos/01-install-kdc.yaml.j2 +++ b/tests/templates/kuttl/kerberos/01-install-kdc.yaml.j2 @@ -139,7 +139,9 @@ data: [domain_realm] .cluster.local = CLUSTER.LOCAL + .cluster.local. = CLUSTER.LOCAL cluster.local = CLUSTER.LOCAL + cluster.local. = CLUSTER.LOCAL kadm5.acl: | root/admin *e stackable-secret-operator *e diff --git a/tests/templates/kuttl/tls/consumer.yaml b/tests/templates/kuttl/tls/consumer.yaml index 1b168f6e..e4c518fd 100644 --- a/tests/templates/kuttl/tls/consumer.yaml +++ b/tests/templates/kuttl/tls/consumer.yaml @@ -39,7 +39,9 @@ spec: if test "${diff}" -gt "$((42*3600))"; then echo "Cert had a lifetime of greater than 42 hours!" && exit 1; fi cat /stackable/tls-3d/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local" + cat /stackable/tls-3d/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local\." cat /stackable/tls-42h/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local" + cat /stackable/tls-42h/tls.crt | openssl x509 -noout -text | grep "DNS:my-tls-service.$NAMESPACE.svc.cluster.local\." volumeMounts: - mountPath: /stackable/tls-3d name: tls-3d