Feb 15, 2026·8 min read·2 visits
The Flux Operator Web UI fails to validate empty OIDC claims. If a user's identity maps to an empty string, the backend skips adding impersonation headers to Kubernetes API requests. Consequently, the request falls back to using the Operator's own Service Account (usually cluster-admin), allowing any authenticated user to escalate privileges and take over the cluster.
A critical logic flaw in the Flux Operator Web UI's OIDC handling allows authenticated users to bypass Kubernetes impersonation mechanisms. By manipulating OIDC claims to resolve to empty values, attackers can force the Operator to execute API requests using its own high-privileged Service Account rather than the user's restricted credentials, effectively granting full cluster administrative access.
Kubernetes operators are the unsung heroes of modern infrastructure, automating the tedious tasks that would otherwise drive SysAdmins to drink. The Flux Operator is particularly nifty: it provides a slick Web UI to manage Flux CD, giving developers a window into their GitOps pipelines. To do this securely, it employs a technique called User Impersonation.
Here is how it should work: You log in via OIDC (say, Google or GitHub). The Operator takes your token, verifies who you are, and then says to the Kubernetes API, "Hey, I'm the Operator, but please execute this next command as Bob from Accounting." This is handled via Impersonate-User headers. The Kubernetes API checks if Bob has permission to delete the production database (he shouldn't), and allows or denies the request accordingly. The Operator is just the messenger.
But in security, the messenger is often the most dangerous link in the chain. This vulnerability, CVE-2026-23990, exists in the handshake between the Operator's authentication logic and the Kubernetes client library. It turns out, if you whisper your name quietly enough (or don't say it at all), the Operator forgets to tell Kubernetes who you are. And when the Operator forgets to impersonate you, it accidentally acts as itself—a Service Account with god-mode privileges.
The root cause here is a classic "fail-open" scenario born from a misunderstanding of the underlying client-go library. The Flux Operator allows administrators to define CEL (Common Expression Language) expressions to map OIDC claims (like email or groups) to Kubernetes identities. For example, claim.email becomes the Kubernetes username.
But what happens if that expression resolves to an empty string? Perhaps the OIDC provider didn't send an email claim, or the CEL expression was malformed. The code handled this by creating an internal Impersonation object where Username was "" and Groups were [].
Here is the fatal flaw: The developer assumed that an empty impersonation config would result in an error or a harmless no-op. Instead, they handed this empty configuration to the Kubernetes client-go transport wrapper.
When client-go sees an impersonation config, it checks: "Is there a username?" If yes, it adds the Impersonate-User header. If no, it simply doesn't add the header. It doesn't throw an error; it just sends a standard request. Since the Operator runs inside the cluster, it uses its own Service Account token for authentication. Without the impersonation header acting as a filter, the Kubernetes API treats the request as coming directly from the Flux Operator Service Account.
It is the digital equivalent of a bouncer checking your ID. If you show a fake ID, he kicks you out. But if you show him nothing, he apparently assumes you own the club and lets you into the VIP room.
Let's look at the "smoking gun" in the code. Prior to version 0.40.0, the authentication flow extracted claims and happily passed them along without ensuring they actually contained data. The logic resided in how the Impersonation struct was populated and subsequently used.
The vulnerability existed because there was no gatekeeper ensuring len(Username) > 0. The fix, introduced in commit 0845404, adds a specific SanitizeAndValidate method that is called immediately after claim extraction.
Here is the breakdown of the fix:
// internal/web/user/user.go
func (imp *Impersonation) SanitizeAndValidate() error {
// Trim spaces to prevent " " from bypassing empty checks
imp.Username = strings.TrimSpace(imp.Username)
for i, g := range imp.Groups {
imp.Groups[i] = strings.TrimSpace(g)
}
// THE FIX: Explicitly forbid the "invisible man" scenario
if imp.Username == "" && len(imp.Groups) == 0 {
return fmt.Errorf("at least one of 'username' or 'groups' must be set for user impersonation")
}
// ... additional validation ...
return nil
}Before this patch, the code would simply proceed. If Username was empty, the transport layer in client-go would construct the HTTP request without the Impersonate-User header. The request would look like this on the wire:
Vulnerable Request (Effective Admin):
GET /api/v1/pods HTTP/1.1
Authorization: Bearer <OPERATOR_SERVICE_ACCOUNT_TOKEN>
Accept: application/jsonIntended Request (Restricted):
GET /api/v1/pods HTTP/1.1
Authorization: Bearer <OPERATOR_SERVICE_ACCOUNT_TOKEN>
Impersonate-User: bob@example.com
Accept: application/jsonBy ensuring the Impersonation struct is never empty, the patch forces the second path, or errors out before the network call is ever made.
To exploit this, we don't need buffer overflows or heap spraying. We just need to be a "nobody." The goal is to authenticate against the Flux Operator Web UI but ensure our resulting identity claims are empty.
preferred_username to the K8s user, the attacker might modify their OIDC profile (if they have control over the IdP) or use an IdP that omits this claim for their specific user scope.ClaimsProcessor runs the CEL expression.
claim.preferred_username{"sub": "123", "email": "attacker@evil.com"} (Note: missing preferred_username)"" (Empty String)Impersonation config. The request goes out signed by the Operator's Service Account.system:serviceaccount:flux-system:flux-operator. Since this account needs to manage the entire Flux lifecycle, it has permissions to read Secrets, create Deployments, or delete Namespaces. The attacker receives the JSON response containing the cluster's secrets.This is a clean, logic-based privilege escalation. No crashing services, no noisy binaries—just one "empty" string turning a guest into a god.
The severity of this vulnerability is deceptive. The CVSS score sits at a "Medium" 5.3, mostly because it requires a specific configuration (OIDC claims resulting in empty values) and authenticated access. However, in the environments where it is exploitable, the impact is catastrophic.
The Flux Operator is not a lowly web app; it is a control plane component. By design, its Service Account requires extensive permissions—often cluster-admin or near-equivalent roles—to reconcile arbitrary Kubernetes manifests defined in Git.
If an attacker exploits this:
Secret and ConfigMap in the cluster, compromising database credentials, API keys, and cloud provider tokens.Because the vulnerability exploits the lack of an identity, audit logs will likely show the actions as being performed by the flux-operator service account, making attribution difficult. The security team will see the "system" doing system things, while the attacker sits quietly behind the web interface.
The path to safety is straightforward, but urgency is key. If you are running flux-operator versions 0.36.0 through 0.39.0, you are vulnerable.
FluxOperator resource configuration. Look at the authentication.oidc.claims section. Ensure that your CEL expressions have fallbacks.
claim.preferred_usernamehas(claim.preferred_username) ? claim.preferred_username : claim.emailSince this exploit abuses legitimate Service Account tokens, network-level detection is hard. Focus on Kubernetes Audit Logs. Look for an anomaly in the behavior of the flux-operator Service Account. If the Service Account is making API calls that correlate exactly with user HTTP sessions on the Flux UI, but are not impersonated requests, that is a red flag.
Normally, the Operator's automated reconciliation loop is periodic and predictable. User-driven actions (via this exploit) will be sporadic and might access resources the reconciliation loop typically ignores (like reading specific Secrets outside of managed namespaces).
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
flux-operator controlplaneio-fluxcd | >= 0.36.0, < 0.40.0 | 0.40.0 |
| Attribute | Detail |
|---|---|
| CWE | CWE-269 (Improper Privilege Management) |
| CVSS v3.1 | 5.3 (Medium) |
| Attack Vector | Network (Authenticated) |
| Attack Complexity | High (Requires specific OIDC/CEL state) |
| Exploit Maturity | PoC Available |
| Impact | Full Cluster Privilege Escalation |
The application does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.