CVE-2026-41184
Description
In Calico, the install-cni init container logs the rendered CNI configuration to standard output. When the configuration template uses the __SERVICEACCOUNT_TOKEN__ placeholder (Canal/Flannel-Calico deployments), the installer substitutes the live Kubernetes ServiceAccount bearer token before logging, exposing the token to any authenticated user with pods/log permission in the namespace with calico-node. The token holds patch privileges on pods/status, enabling annotation-based attacks against cluster workloads. The default kubeconfig-based authentication path is not affected. This is a direct regression of TTA-2018-001.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In Calico, the install-cni init container logs the live Kubernetes ServiceAccount token when using the __SERVICEACCOUNT_TOKEN__ placeholder, exposing it to anyone with pods/log permission in the calico-node namespace.
Vulnerability
The install-cni init container in Calico logs the rendered CNI configuration to standard output [1][2]. When the configuration template uses the __SERVICEACCOUNT_TOKEN__ placeholder (typical in Canal/Flannel-Calico deployments), the installer substitutes the live Kubernetes ServiceAccount bearer token before logging [2]. This exposes the token to any authenticated user with pods/log permission in the namespace where calico-node runs [1][2]. The default kubeconfig-based authentication path is not affected [2]. This is a direct regression of TTA-2018-001 [2]. Affected versions include all releases prior to the patch in branches 3.31 and 3.32 [1][2][3].
Exploitation
An attacker needs only read access to pod logs in the namespace where calico-node is deployed (typically kube-system or a dedicated namespace) [1][2]. The token is logged each time the install-cni container runs, so the attacker can retrieve it from the container logs [2]. The token has patch privileges on pods/status, which enables annotation-based attacks against cluster workloads [2]. No additional authentication or network position is required beyond the initial pods/log permission [1][2].
Impact
An attacker who retrieves the ServiceAccount token can use the token's patch pods/status permission to modify pod annotations [2]. This capability can be leveraged to tamper with running workloads, potentially escalating privileges or disrupting cluster operations [2]. The token does not grant full cluster-admin access, but the annotation-based attack surface is significant for workloads that rely on certain annotations for security or configuration [2].
Mitigation
The fix sanitizes the log output in the CNI plugin to redact the ServiceAccount token before logging [1][2][3]. The fix has been backported to release branches 3.31 and 3.32 [1][3]. Users should upgrade to patched versions: for the 3.31 branch, apply the backport PR #12527 [1]; for the 3.32 branch, apply the backport PR #12526 [3]. Alternatively, users can switch to the default kubeconfig-based authentication path, which is not affected [2]. As of the publication date, the CVE is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
- [release 3.31] Sanitize log output in CNI plugin (pick #12502) by Behnam-Shobiri · Pull Request #12527 · projectcalico/calico
- Sanitize log output in CNI plugin by Behnam-Shobiri · Pull Request #12502 · projectcalico/calico
- [release 3.32] Sanitize log output in CNI plugin (pick #12502) by Behnam-Shobiri · Pull Request #12526 · projectcalico/calico
AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
3da9ea3a13927Merge pull request #12527 from Behnam-Shobiri/fix-logs31
2 files changed · +10 −9
cni-plugin/internal/pkg/azure/azure.go+7 −2 modified@@ -30,7 +30,8 @@ func MutateConfigAdd(args *skel.CmdArgs, network AzureNetwork) error { if err != nil { return err } - logrus.Infof("Updated CNI network configuration for Azure Add: %#v", stdinData) + // Don't log the entire stdinData here because it may contain sensitive information + logrus.WithField("subnet", network.Subnets[0]).Info("Updated CNI network configuration for Azure Add") return nil } @@ -67,6 +68,10 @@ func MutateConfigDel(args *skel.CmdArgs, network AzureNetwork, endpoint AzureEnd if err != nil { return err } - logrus.Infof("Updated CNI network configuration for Azure Del: %#v", stdinData) + // Don't log the entire stdinData here because it may contain sensitive information + logrus.WithFields(logrus.Fields{ + "subnet": network.Subnets[0], + "ipAddress": splits[0], + }).Info("Updated CNI network configuration for Azure Del") return nil }
cni-plugin/pkg/install/install.go+3 −7 modified@@ -376,7 +376,8 @@ func writeCNIConfig(c config) { err = isValidJSON(netconf) if err != nil { - logrus.Fatalf("%s is not a valid json object\nerror: %s", netconf, err) + // Don't log the entire config/netconf here because it may contain sensitive information + logrus.Fatalf("CNI config is not valid JSON: %v", err) } // Write out the file. @@ -401,13 +402,8 @@ func writeCNIConfig(c config) { logrus.Fatal(err) } - content, err := os.ReadFile(path) - if err != nil { - logrus.Fatal(err) - } + // Do not log the config file (path) contents here — it may contain sensitive credentials. logrus.Infof("Created %s", winutils.GetHostPath(fmt.Sprintf("/host/etc/cni/net.d/%s", name))) - text := string(content) - fmt.Println(text) // Remove any old config file, if one exists. oldName := getEnv("CNI_OLD_CONF_NAME", "10-calico.conflist")
0ca653db0ae7Merge pull request #12526 from Behnam-Shobiri/fix-logs32
2 files changed · +10 −9
cni-plugin/internal/pkg/azure/azure.go+7 −2 modified@@ -30,7 +30,8 @@ func MutateConfigAdd(args *skel.CmdArgs, network AzureNetwork) error { if err != nil { return err } - logrus.Infof("Updated CNI network configuration for Azure Add: %#v", stdinData) + // Don't log the entire stdinData here because it may contain sensitive information + logrus.WithField("subnet", network.Subnets[0]).Info("Updated CNI network configuration for Azure Add") return nil } @@ -67,6 +68,10 @@ func MutateConfigDel(args *skel.CmdArgs, network AzureNetwork, endpoint AzureEnd if err != nil { return err } - logrus.Infof("Updated CNI network configuration for Azure Del: %#v", stdinData) + // Don't log the entire stdinData here because it may contain sensitive information + logrus.WithFields(logrus.Fields{ + "subnet": network.Subnets[0], + "ipAddress": splits[0], + }).Info("Updated CNI network configuration for Azure Del") return nil }
cni-plugin/pkg/install/install.go+3 −7 modified@@ -381,7 +381,8 @@ func writeCNIConfig(c config) { err = isValidJSON(netconf) if err != nil { - logrus.Fatalf("%s is not a valid json object\nerror: %s", netconf, err) + // Don't log the entire config/netconf here because it may contain sensitive information + logrus.Fatalf("CNI config is not valid JSON: %v", err) } // Write out the file. @@ -406,13 +407,8 @@ func writeCNIConfig(c config) { logrus.Fatal(err) } - content, err := os.ReadFile(path) - if err != nil { - logrus.Fatal(err) - } + // Do not log the config file (path) contents here — it may contain sensitive credentials. logrus.Infof("Created %s", winutils.GetHostPath(fmt.Sprintf("/host/etc/cni/net.d/%s", name))) - text := string(content) - fmt.Println(text) // Remove any old config file, if one exists. oldName := getEnv("CNI_OLD_CONF_NAME", "10-calico.conflist")
cd73bc2cea0fMerge pull request #12502 from Behnam-Shobiri/fix-logs
2 files changed · +10 −9
cni-plugin/internal/pkg/azure/azure.go+7 −2 modified@@ -30,7 +30,8 @@ func MutateConfigAdd(args *skel.CmdArgs, network AzureNetwork) error { if err != nil { return err } - logrus.Infof("Updated CNI network configuration for Azure Add: %#v", stdinData) + // Don't log the entire stdinData here because it may contain sensitive information + logrus.WithField("subnet", network.Subnets[0]).Info("Updated CNI network configuration for Azure Add") return nil } @@ -67,6 +68,10 @@ func MutateConfigDel(args *skel.CmdArgs, network AzureNetwork, endpoint AzureEnd if err != nil { return err } - logrus.Infof("Updated CNI network configuration for Azure Del: %#v", stdinData) + // Don't log the entire stdinData here because it may contain sensitive information + logrus.WithFields(logrus.Fields{ + "subnet": network.Subnets[0], + "ipAddress": splits[0], + }).Info("Updated CNI network configuration for Azure Del") return nil }
cni-plugin/pkg/install/install.go+3 −7 modified@@ -381,7 +381,8 @@ func writeCNIConfig(c config) { err = isValidJSON(netconf) if err != nil { - logrus.Fatalf("%s is not a valid json object\nerror: %s", netconf, err) + // Don't log the entire config/netconf here because it may contain sensitive information + logrus.Fatalf("CNI config is not valid JSON: %v", err) } // Write out the file. @@ -406,13 +407,8 @@ func writeCNIConfig(c config) { logrus.Fatal(err) } - content, err := os.ReadFile(path) - if err != nil { - logrus.Fatal(err) - } + // Do not log the config file (path) contents here — it may contain sensitive credentials. logrus.Infof("Created %s", winutils.GetHostPath(fmt.Sprintf("/host/etc/cni/net.d/%s", name))) - text := string(content) - fmt.Println(text) // Remove any old config file, if one exists. oldName := getEnv("CNI_OLD_CONF_NAME", "10-calico.conflist")
Vulnerability mechanics
Root cause
"The install-cni init container logs the rendered CNI configuration to stdout after substituting the live Kubernetes ServiceAccount bearer token, exposing the token in plaintext logs."
Attack vector
An attacker with pods/log permission in the namespace running calico-node can read the install-cni init container logs. The init container substitutes the __SERVICEACCOUNT_TOKEN__ placeholder with the live Kubernetes ServiceAccount bearer token before writing the CNI configuration, then logs the rendered configuration to stdout [ref_id=1]. The exposed token holds patch privileges on pods/status, enabling annotation-based attacks against cluster workloads. The default kubeconfig-based authentication path is not affected; only Canal/Flannel-Calico deployments using the __SERVICEACCOUNT_TOKEN__ placeholder are vulnerable.
Affected code
The vulnerable code is in cni-plugin/pkg/install/install.go, which logs the full CNI config content during JSON validation and after writing the conflist [ref_id=1]. The Azure CNI mutation code in cni-plugin/internal/pkg/azure/azure.go also dumps full stdin JSON via %#v formatting [ref_id=1].
What the fix does
The patches [patch_id=2961930] [patch_id=2961931] [patch_id=2961932] sanitize CNI plugin log output by removing full config content from fatal logs and stopping the printing of generated CNI config file contents after writing the conflist [ref_id=1]. In the Azure CNI path, the fix replaces %#v dumps of stdinData with structured logs containing only selected fields, avoiding emission of the full config payload [ref_id=1]. These changes prevent the ServiceAccount token from appearing in log output.
Preconditions
- authAttacker must have pods/log permission in the namespace running calico-node.
- configDeployment must use the __SERVICEACCOUNT_TOKEN__ placeholder (Canal/Flannel-Calico deployments).
- networkAttacker must be able to access the Kubernetes API server to read pod logs.
Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.