Strimzi allows unrestricted access to all Secrets in the same Kubernetes namespace from Kafka Connect and MirrorMaker 2 operands
Description
Strimzi provides a way to run an Apache Kafka cluster on Kubernetes or OpenShift in various deployment configurations. From 0.47.0 and prior to 0.49.1, in some situations, Strimzi creates an incorrect Kubernetes Role which grants the Apache Kafka Connect and Apache Kafka MirrorMaker 2 operands the GET access to all Kubernetes Secrets that exist in the given Kubernetes namespace. The issue is fixed in Strimzi 0.49.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Strimzi 0.47.0 through 0.49.0 creates an overly permissive Kubernetes Role for Kafka Connect and MirrorMaker 2 operands, granting GET access to all Secrets in the namespace.
Vulnerability
Overview
CVE-2025-66623 affects Apache Strimzi, a Kubernetes operator for running Apache Kafka clusters. In versions 0.47.0 to 0.49.0, the operator generates a Kubernetes Role for certain operand configurations that incorrectly grants the get verb on all secrets resources within the namespace, rather than restricting access to only the secrets the operand requires (e.g., TLS certificates) [1][4]. This role is assigned to the operands' ServiceAccounts.
Exploitation
Scenario
The flawed Role is created when Kafka Connect or MirrorMaker 2 is deployed without specific TLS or mTLS configurations. For Kafka Connect, this occurs when the custom resource lacks the .spec.tls.trustedCertificates section, type: tls authentication, or .spec.authentication.tlsTrustedCertificates for OAuth. For MirrorMaker 2, the same conditions apply to the target cluster configuration [4]. In these scenarios, any process running within the operand's Pod can leverage the ServiceAccount to request any Secret from the Kubernetes API server within that namespace.
Impact
An attacker who gains code execution inside an affected Kafka Connect or MirrorMaker 2 Pod can retrieve all Secrets in the namespace, including credentials, certificates, and other sensitive data [1][4]. This would allow lateral movement and privilege escalation within the Kubernetes environment.
Mitigation
The issue is fixed in Strimzi 0.49.1 [1]. The patch modifies the role generation logic to list only the specific secret names needed, as shown in the commit that changes generateRole() to return a filtered list of secrets [2]. Administrators should upgrade Strimzi to version 0.49.1 or later and re-deploy their Kafka Connect and MirrorMaker 2 operands.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.strimzi:strimziMaven | >= 0.47.0, < 0.49.1 | 0.49.1 |
Affected products
2- strimzi/strimzi-kafka-operatorv5Range: >= 0.47.0, < 0.49.1
Patches
1c8a14935e99cMerge commit from fork
4 files changed · +145 −34
cluster-operator/src/main/java/io/strimzi/operator/cluster/model/KafkaCluster.java+13 −8 modified@@ -1652,14 +1652,19 @@ public Role generateRole() { } } - List<PolicyRule> rules = List.of(new PolicyRuleBuilder() - .withApiGroups("") - .withResources("secrets") - .withVerbs("get") - .withResourceNames(certSecretNames.stream().toList()) - .build()); - - return RbacUtils.createRole(componentName, namespace, rules, labels, ownerReference, null); + if (certSecretNames.isEmpty()) { + // This should never happen but just in case it does, we throw an error + throw new RuntimeException("No TLS certificate secrets found for the Kafka cluster."); + } else { + List<PolicyRule> rules = List.of(new PolicyRuleBuilder() + .withApiGroups("") + .withResources("secrets") + .withVerbs("get") + .withResourceNames(certSecretNames.stream().toList()) + .build()); + + return RbacUtils.createRole(componentName, namespace, rules, labels, ownerReference, null); + } } /**
cluster-operator/src/main/java/io/strimzi/operator/cluster/model/KafkaConnectCluster.java+43 −26 modified@@ -595,7 +595,6 @@ private List<VolumeMount> getExternalConfigurationVolumeMounts() { /** * Generates the StrimziPodSet for the Kafka cluster. - * enabled. * * @param replicas Number of replicas the StrimziPodSet should have. During scale-ups or scale-downs, node * sets with different numbers of pods are generated. @@ -909,13 +908,11 @@ public ClusterRoleBinding generateClusterRoleBinding() { } /** - * Creates a Role for reading TLS certificate secrets in the same namespace as the resource. - * This is used for loading certificates from secrets directly. - ** - * @return role for the Kafka Connect + * @return The list of Secrets the Pods will need access to through Kubernetes API to load their values using + * configuration providers. */ @SuppressWarnings("deprecation") // OAuth authentication is deprecated - public Role generateRole() { + private List<String> secretsToAllowAccessTo() { List<String> certSecretNames = new ArrayList<>(); if (tls != null && tls.getTrustedCertificates() != null && !tls.getTrustedCertificates().isEmpty()) { certSecretNames.add(KafkaConnectResources.internalTlsTrustedCertsSecretName(cluster)); @@ -930,14 +927,30 @@ public Role generateRole() { } } - List<PolicyRule> rules = List.of(new PolicyRuleBuilder() - .withApiGroups("") - .withResources("secrets") - .withVerbs("get") - .withResourceNames(certSecretNames) - .build()); + return certSecretNames; + } + + /** + * Creates a Role for reading TLS certificate secrets in the same namespace as the resource. + * This is used for loading certificates from secrets directly. + ** + * @return role for the Kafka Connect + */ + public Role generateRole() { + List<String> certSecretNames = secretsToAllowAccessTo(); - return RbacUtils.createRole(componentName, namespace, rules, labels, ownerReference, null); + if (certSecretNames.isEmpty()) { + return null; + } else { + List<PolicyRule> rules = List.of(new PolicyRuleBuilder() + .withApiGroups("") + .withResources("secrets") + .withVerbs("get") + .withResourceNames(certSecretNames) + .build()); + + return RbacUtils.createRole(componentName, namespace, rules, labels, ownerReference, null); + } } /** @@ -946,19 +959,23 @@ public Role generateRole() { * @return Role Binding for the Kafka Connect */ public RoleBinding generateRoleBindingForRole() { - Subject subject = new SubjectBuilder() - .withKind("ServiceAccount") - .withName(componentName) - .withNamespace(namespace) - .build(); - - RoleRef roleRef = new RoleRefBuilder() - .withName(componentName) - .withApiGroup("rbac.authorization.k8s.io") - .withKind("Role") - .build(); - - return RbacUtils.createRoleBinding(getRoleBindingName(), namespace, roleRef, List.of(subject), labels, ownerReference, null); + if (secretsToAllowAccessTo().isEmpty()) { + return null; + } else { + Subject subject = new SubjectBuilder() + .withKind("ServiceAccount") + .withName(componentName) + .withNamespace(namespace) + .build(); + + RoleRef roleRef = new RoleRefBuilder() + .withName(componentName) + .withApiGroup("rbac.authorization.k8s.io") + .withKind("Role") + .build(); + + return RbacUtils.createRoleBinding(getRoleBindingName(), namespace, roleRef, List.of(subject), labels, ownerReference, null); + } } /**
cluster-operator/src/test/java/io/strimzi/operator/cluster/model/KafkaConnectClusterTest.java+40 −0 modified@@ -46,6 +46,8 @@ import io.fabric8.kubernetes.api.model.networking.v1.NetworkPolicyIngressRule; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget; import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; +import io.fabric8.kubernetes.api.model.rbac.Role; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; import io.strimzi.api.kafka.model.common.CertSecretSource; import io.strimzi.api.kafka.model.common.CertSecretSourceBuilder; import io.strimzi.api.kafka.model.common.JvmOptions; @@ -2378,4 +2380,42 @@ public void testOciConnectorPlugins() { assertThat(containers.get(0).getVolumeMounts().get(4).getMountPath(), is("/opt/kafka/plugins/second-connector/695ab9d6")); }); } + + @Test + public void testRoleAbdRoleBindingNoSecrets() { + assertThat(KC.generateRole(), is(nullValue())); + assertThat(KC.generateRoleBindingForRole(), is(nullValue())); + } + + @Test + public void testRoleAbdRoleBindingWithSecrets() { + KafkaConnect resource = new KafkaConnectBuilder(RESOURCE) + .editSpec() + .withNewTls() + .withTrustedCertificates(new CertSecretSourceBuilder().withSecretName("my-secret").withCertificate("ca.crt").build()) + .endTls() + .endSpec() + .build(); + + KafkaConnectCluster kc = KafkaConnectCluster.fromCrd(Reconciliation.DUMMY_RECONCILIATION, resource, VERSIONS, SHARED_ENV_PROVIDER); + + Role role = kc.generateRole(); + assertThat(role.getMetadata().getName(), is(kc.componentName)); + assertThat(role.getMetadata().getNamespace(), is(NAMESPACE)); + assertThat(role.getRules().size(), is(1)); + assertThat(role.getRules().get(0).getApiGroups(), is(List.of(""))); + assertThat(role.getRules().get(0).getResources(), is(List.of("secrets"))); + assertThat(role.getRules().get(0).getVerbs(), is(List.of("get"))); + assertThat(role.getRules().get(0).getResourceNames(), is(List.of(kc.componentName + "-tls-trusted-certs"))); + + RoleBinding rb = kc.generateRoleBindingForRole(); + assertThat(rb.getMetadata().getName(), is(KafkaConnectResources.connectRoleBindingName(NAME))); + assertThat(rb.getMetadata().getNamespace(), is(NAMESPACE)); + assertThat(rb.getSubjects().size(), is(1)); + assertThat(rb.getSubjects().get(0).getKind(), is("ServiceAccount")); + assertThat(rb.getSubjects().get(0).getNamespace(), is(NAMESPACE)); + assertThat(rb.getSubjects().get(0).getName(), is(kc.componentName)); + assertThat(rb.getRoleRef().getKind(), is("Role")); + assertThat(rb.getRoleRef().getName(), is(kc.componentName)); + } }
cluster-operator/src/test/java/io/strimzi/operator/cluster/model/KafkaMirrorMaker2ClusterTest.java+49 −0 modified@@ -42,6 +42,8 @@ import io.fabric8.kubernetes.api.model.networking.v1.NetworkPolicyIngressRule; import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget; import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBinding; +import io.fabric8.kubernetes.api.model.rbac.Role; +import io.fabric8.kubernetes.api.model.rbac.RoleBinding; import io.strimzi.api.kafka.model.common.CertSecretSource; import io.strimzi.api.kafka.model.common.CertSecretSourceBuilder; import io.strimzi.api.kafka.model.common.JvmOptions; @@ -2498,4 +2500,51 @@ public void testLoggingWithLog4j2() { ConfigMap cm = KMM2.generateConnectConfigMap(new MetricsAndLogging(METRICS_CONFIG, null)); assertThat(cm.getData().get(LoggingModel.LOG4J2_CONFIG_MAP_KEY), is(notNullValue())); } + + @Test + public void testRoleAbdRoleBindingNoSecrets() { + assertThat(KMM2.generateRole(), is(nullValue())); + assertThat(KMM2.generateRoleBindingForRole(), is(nullValue())); + } + + @Test + public void testRoleAbdRoleBindingWithSecrets() { + KafkaMirrorMaker2 resource = new KafkaMirrorMaker2Builder(RESOURCE) + .editSpec() + .editTarget() + .withNewTls() + .withTrustedCertificates(new CertSecretSourceBuilder().withSecretName("my-secret").withCertificate("ca.crt").build()) + .endTls() + .endTarget() + .editFirstMirror() + .editSource() + .withNewTls() + .withTrustedCertificates(new CertSecretSourceBuilder().withSecretName("my-source-secret").withCertificate("ca.crt").build()) + .endTls() + .endSource() + .endMirror() + .endSpec() + .build(); + + KafkaMirrorMaker2Cluster kmm2 = KafkaMirrorMaker2Cluster.fromCrd(Reconciliation.DUMMY_RECONCILIATION, resource, VERSIONS, SHARED_ENV_PROVIDER); + + Role role = kmm2.generateRole(); + assertThat(role.getMetadata().getName(), is(kmm2.componentName)); + assertThat(role.getMetadata().getNamespace(), is(NAMESPACE)); + assertThat(role.getRules().size(), is(1)); + assertThat(role.getRules().get(0).getApiGroups(), is(List.of(""))); + assertThat(role.getRules().get(0).getResources(), is(List.of("secrets"))); + assertThat(role.getRules().get(0).getVerbs(), is(List.of("get"))); + assertThat(role.getRules().get(0).getResourceNames(), is(List.of(KafkaConnectResources.componentName(NAME) + "-tls-trusted-certs"))); + + RoleBinding rb = kmm2.generateRoleBindingForRole(); + assertThat(rb.getMetadata().getName(), is(KafkaMirrorMaker2Resources.mm2RoleBindingName(NAME))); + assertThat(rb.getMetadata().getNamespace(), is(NAMESPACE)); + assertThat(rb.getSubjects().size(), is(1)); + assertThat(rb.getSubjects().get(0).getKind(), is("ServiceAccount")); + assertThat(rb.getSubjects().get(0).getNamespace(), is(NAMESPACE)); + assertThat(rb.getSubjects().get(0).getName(), is(kmm2.componentName)); + assertThat(rb.getRoleRef().getKind(), is("Role")); + assertThat(rb.getRoleRef().getName(), is(kmm2.componentName)); + } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-xrhh-hx36-485qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66623ghsaADVISORY
- github.com/strimzi/strimzi-kafka-operator/commit/c8a14935e99c91eb0dd865431f46515da9f82cccghsax_refsource_MISCWEB
- github.com/strimzi/strimzi-kafka-operator/security/advisories/GHSA-xrhh-hx36-485qghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.