Confused Deputy in the Cloud: CVE-2026-22822 & The ESO Secret Heist
Jan 20, 2026·6 min read·2 visits
Executive Summary (TL;DR)
The External Secrets Operator (ESO) v2 templating engine contained a helper function, `getSecretKey`, that ran with the controller's cluster-wide privileges. It failed to check if the requesting user actually had permission to read the target secret. This allowed any user who could create an `ExternalSecret` resource to exfiltrate credentials from restricted namespaces (like `kube-system`). The fix was the nuclear option: the function was deleted entirely in v1.2.0.
A critical 'Confused Deputy' vulnerability in the External Secrets Operator (ESO) allows low-privileged users to hijack the controller's identity. By abusing the v2 templating engine's `getSecretKey` function, an attacker can trick the operator into fetching and revealing any secret from any namespace in the cluster, completely bypassing Kubernetes RBAC boundaries.
The Hook: The Helper That Helped Itself
The External Secrets Operator (ESO) is the darling of GitOps. It’s the glue that binds your shiny external vaults (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) to the gritty reality of Kubernetes Secrets. It automates the drudgery of syncing credentials. But like any tool that sits at the intersection of high privilege and complex configuration, it’s a ticking time bomb if the boundaries aren't strictly enforced.
In ESO, you often need to massage data before it lands in a Kubernetes Secret. Maybe you need to append a string, base64 decode something, or template a configuration file. Enter the v2 Templating Engine. It gave users the power to transform data using Go templates. It was flexible, powerful, and, as it turns out, fatally flawed.
Included in this engine was a seemingly innocuous function called getSecretKey. The intent was benign: allow a template to reference another existing Kubernetes secret to compose a new value. The reality was a master key. This function didn't just look up data; it bypassed the entire premise of Kubernetes multi-tenancy.
The Flaw: A Textbook Confused Deputy
To understand this vulnerability, you have to understand the Confused Deputy problem. In Kubernetes, controllers (like ESO) are the deputies. They hold the keys to the kingdom (ClusterRoles) because they need to manage resources everywhere. Users are the prisoners. They are stuck in their namespaces with limited rights.
When a user creates an ExternalSecret, they are handing instructions to the deputy. The flaw in CVE-2026-22822 was that the deputy followed those instructions blindly, using its own badge to open doors the user could never touch.
Specifically, the getSecretKey function initialized a new Kubernetes client using the controller's own service account. It did not impersonate the user. It did not check the user's RBAC permissions. It simply accepted a namespace and a secret name as arguments, strutted over to that namespace with its cluster-wide read privileges, grabbed the data, and handed it back to the user in the form of a templated secret. It’s the digital equivalent of asking a bank guard to fetch cash from the vault because "my friend said it's okay."
The Code: The Smoking Gun
The vulnerability lived in runtime/template/v2/kubernetes.go. Let's look at the crime scene. The code essentially spun up a fresh client using ctrlcfg.GetConfig(). In the context of a Kubernetes operator, GetConfig() returns the configuration of the pod running the code—i.e., the privileged controller itself.
// The Vulnerable Logic (Pseudo-code representation)
func getSecretKey(name, namespace, key string) string {
// 1. Get the controller's privileged config
cfg := ctrlcfg.GetConfigOrDie()
// 2. Create a client with those privileges
client := kubernetes.NewForConfig(cfg)
// 3. Fetch the secret requested by the user
// NO CHECK: Does the user owning the ExternalSecret have access to 'namespace'?
secret, _ := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
// 4. Return the sensitive data
return string(secret.Data[key])
}The fix provided in commit 17d3e22b8d3fbe339faf8515a95ec06ec92b1feb was not a subtle adjustment of permissions. The maintainers realized that safely implementing this feature would require complex impersonation logic or a massive overhaul of the permission model. So, they chose the Scorched Earth strategy.
The Fix:
- func (t *Template) getSecretKey(name, namespace, key string) (string, error) {
- // ... entire function deleted ...
- }They literally deleted the file runtime/template/v2/kubernetes.go. If the code isn't there, it can't be exploited. Efficient.
The Exploit: Stealing the Keys to the Kingdom
Exploiting this is trivially easy for anyone with create access to ExternalSecret resources in any namespace. Let's assume you are an attacker who has compromised a low-privilege pod in the marketing namespace, and you want to steal the Cluster Admin credentials (often found in kube-system) or a database root password from the production namespace.
Here is the attack chain:
You craft an ExternalSecret that uses the v2 engine and calls the forbidden function. You don't even need a real external store; you can use a dummy one or a valid one you have access to.
The Payload:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: heist-job
namespace: marketing-ns
spec:
target:
template:
engineVersion: v2
data:
# The Magic Line: Stealing the default service account token from kube-system
captured_token: '{{ getSecretKey "default-token-xxxxx" "kube-system" "token" }}'
data:
- secretKey: ignore_me
remoteRef:
key: legitimate-keyOnce applied, the ESO controller processes this. It sees the template execution request, executes getSecretKey, unknowingly uses its own credentials to read the kube-system token, and writes it into a new Secret called heist-job inside marketing-ns. The attacker then just reads heist-job and pivots to full cluster compromise.
The Impact: Total Namespace Isolation Failure
The impact here cannot be overstated. Kubernetes security relies heavily on Namespaces as the boundary of isolation. RBAC rules are scoped to namespaces. Quotas are scoped to namespaces. Secrets are scoped to namespaces.
CVE-2026-22822 renders these boundaries null and void for any cluster running a vulnerable version of ESO. If you are a multi-tenant provider offering "secure" namespaces to different customers, one customer could read the secrets of every other customer.
Even in single-tenant clusters, this allows for trivial Privilege Escalation. A developer with access to a sandbox environment can pull production database credentials, CI/CD API keys, or cloud provider IAM keys (if stored as K8s secrets). Since the ESO controller usually needs broad read permissions to function, the blast radius is effectively the entire cluster's secret store.
The Mitigation: Update or Die
There is no configuration workaround. You cannot simply "disable" the v2 templating engine without breaking functionality for users who rely on it. You cannot restrict the controller's permissions without breaking its core ability to reconcile secrets.
Immediate Action Required:
- Upgrade: Update to External Secrets Operator v1.2.0 or later immediately. This version removes the vulnerable function entirely.
- Audit: Check your cluster for existing
ExternalSecretresources that usegetSecretKey. Since the function was removed, these resources will enter an error state upon upgrade. This is a good way to find out if anyone was already using it (legitimately or maliciously).
Lessons Learned: When building Kubernetes Operators, never assume that just because code is running inside your controller, it should use the controller's credentials. If you are performing actions on behalf of a user, you must validate their permissions or impersonate them. The "God Mode" client is a temptation that leads to ruin.
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
External Secrets Operator External Secrets | < 1.2.0 | 1.2.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-441 (Confused Deputy) |
| Attack Vector | Network |
| CVSS | 8.8 (High) |
| Privileges Required | Low (Create ExternalSecret) |
| Impact | Confidentiality (High), Integrity (Medium) |
| Status | Patched (Feature Removed) |
MITRE ATT&CK Mapping
The product receives a request from an upstream component, but it does not correctly verify that the request is authorized, allowing the request to operate with the product's own elevated privileges.
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.