VYPR
High severityNVD Advisory· Published Dec 5, 2025· Updated Dec 5, 2025

Strimzi allows unrestricted access to all Secrets in the same Kubernetes namespace from Kafka Connect and MirrorMaker 2 operands

CVE-2025-66623

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.

PackageAffected versionsPatched versions
io.strimzi:strimziMaven
>= 0.47.0, < 0.49.10.49.1

Affected products

2
  • Strimzi/Strimzillm-fuzzy
    Range: >=0.47.0, <0.49.1
  • strimzi/strimzi-kafka-operatorv5
    Range: >= 0.47.0, < 0.49.1

Patches

1
c8a14935e99c

Merge 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

News mentions

0

No linked articles in our index yet.