← Back to Attack Research

Serverless on Kubernetes: from function deploy to cluster admin

A serverless platform hands tenants a builder and a router so they can ship functions without touching the cluster. The moment either component is reachable without authentication, the convenience becomes the breach: deploy a pod, reach the node, lift the service-account token, own the cluster. Grounded in the Fission RCE pair CVE-2026-50545 and CVE-2026-50563 and the unauthenticated-router invocation CVE-2026-46614. Here is the decision tree your scanner never walks.

Function-as-a-service on Kubernetes is built to let an untrusted tenant ship code into a shared cluster. That is the whole value proposition, and it is also the whole attack surface. A serverless control plane exposes two tenant-facing primitives by design: a builder that turns submitted source into a runnable image, and a router that invokes the resulting function. If either accepts input without authenticating it and without constraining what the resulting pod may do, an attacker stops attacking the application and starts attacking the substrate. The Fission disclosures of June 2026 are the textbook case: CVE-2026-46614 let any caller who could reach the router invoke any function with no trigger and no auth, while CVE-2026-50545 and CVE-2026-50563 (both CVSS 9.9) let a tenant pass an unvalidated pod spec straight into the cluster, setting hostPID, privileged, and serviceAccountName on a pod the platform then scheduled. App workload to node to cluster-admin, in one pod spec.

This piece continues our attack-research series on the runtime trust boundaries that static tooling cannot see. The cloud-admin chain piece walked an application primitive into a workload identity and then into the cloud control plane. The multi-tenant isolation piece showed the attacker accepting that the IAM policy is correct and instead attacking the annotation channel the policy trusts. Serverless-on-Kubernetes is the same shape with the boundary moved one layer down: the platform intends to run tenant code, so the attacker does not need a memory-corruption bug. They need the platform to do exactly what it was built to do, with one field the platform forgot to validate. Verifiable security.

The attack pattern in one paragraph

A serverless-on-Kubernetes platform (Fission, and the same architecture in any FaaS-on-K8s system) decomposes into a handful of in-cluster services: a router that maps an inbound request to a function, a builder that compiles submitted source into a deployable package, an executor that turns a function definition into a running pod, and a storage service that holds the archives. Each is an ordinary Kubernetes Service, usually a ClusterIP, and each speaks plain HTTP. The platform's security model assumes two things that are frequently false: that these services are only reachable by other platform components, and that the function and pod definitions a tenant submits are constrained to safe values. The attacker breaks the first assumption by reaching a service that has no network policy in front of it (any pod in the cluster can dial a ClusterIP, and a misconfigured ingress can expose it outside the cluster entirely). They break the second by submitting a pod spec the platform merges into the real pod without filtering: hostPID: true to share the node's process namespace, privileged: true to drop container isolation, or serviceAccountName pointed at a high-privilege account. The platform schedules the pod. The attacker now has code running on the node with the node's view of everything, and the automounted service-account token of whatever identity the pod was given. From there the cluster's API server is one curl away.

The unifying observation: a function builder and router are an authenticated remote-code-execution surface by construction, and the platform's job is to keep the "authenticated" and the "constrained" parts true. When either slips, the FaaS layer is no longer a sandbox on top of the cluster. It is a deploy pipeline an attacker drives.

Why this still ships in 2026

If running untrusted code is the explicit feature, why do these platforms ship without the controls that contain it? Three structural reasons, each verifiable against your own cluster in an afternoon.

  1. The in-cluster trust boundary is assumed, not enforced. A ClusterIP service is reachable by every pod in the cluster unless a NetworkPolicy says otherwise, and most clusters ship with no default-deny. CVE-2026-46614 is exactly this: the Fission router registered an internal route, /fission-function/<ns>/<name>, on the same listener as user triggers, so any caller who could reach the router could invoke any function by guessing its name, bypassing every host, path, and method restriction the trigger objects encoded. The companion storage-service flaw, CVE-2026-46612, registered archive read/write/delete handlers with no authentication at all. The platform treated "inside the cluster" as "trusted," and the cluster never agreed.
  2. Pod-spec passthrough is a convenience that became an injection point. To let tenants tune resource limits and runtime images, Fission exposed spec.runtime.podSpec on the Environment CRD and Function.spec.podspec on the container executor, then merged them into the real pod. CVE-2026-50564 documents the merge propagating hostNetwork, hostPID, hostIPC, privileged, and serviceAccountName with no filtering. A field meant for "set my memory limit" also accepted "give me the node." The capability existed for a reason; the validation that should have bounded it did not.
  3. Scanners audit the image and the manifest, not the runtime trust graph. Container scanners grep the function image for CVEs. Manifest linters check the static YAML the platform ships. Neither asks the question that matters here: can an unauthenticated caller reach the builder or router, and if a tenant submits a pod spec, does an admission control reject privileged before the kubelet ever sees it? That check requires modelling the runtime boundary between "tenant input" and "node," and the runtime boundary is precisely what a static scan cannot walk.

The attacker decision tree

ATTACKER DECISION TREE Serverless Function → Node → Cluster Admin ┌──────────────────────────────────────────────┐ │ 1. Reach a tenant-facing FaaS service │ │ - router (invoke) CVE-2026-46614 │ │ - builder (compile) CVE-2026-46618 │ │ - storage (archives) CVE-2026-46612 │ │ in-cluster ClusterIP, OR exposed via ingress│ └───────────────────┬──────────────────────────┘ │ is it authenticated? ┌───────┴────────┐ YES│ │ NO (no NetworkPolicy / no authn) ▼ ▼ ┌──────────────┐ ┌──────────────────────────────────┐ │ need creds │ │ 2. Deploy / invoke a function I │ │ or another │ │ control │ │ entry, go to │ │ - submit Environment/Function │ │ step 1 │ │ with a custom podSpec │ └──────────────┘ └─────────────────┬──────────────────┘ │ podSpec validated? ┌───────┴────────┐ YES│ │ NO (CVE-2026-50545 / ▼ ▼ 50563 / 50564) ┌──────────────┐ ┌──────────────────────────┐ │ run as a │ │ 3. Break out to the node │ │ normal pod; │ │ - hostPID / hostIPC │ │ try token │ │ - privileged: true │ │ harvest only │ │ - hostPath / mount / │ └──────┬───────┘ └────────────┬─────────────┘ │ │ └──────────┬────────────┘ ▼ ┌──────────────────────────────────────┐ │ 4. Lift the service-account token │ │ /var/run/secrets/.../token │ │ - executor's high-priv SA, OR │ │ - fission-fetcher SA (ns secrets) │ │ CVE-2026-46617 │ └────────────────┬─────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 5. Enumerate what the token can do │ │ kubectl auth can-i --list │ │ - read secrets / create pods cluster- │ │ wide? bind roles? │ └────────────────┬─────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ 6. Escalate to cluster-admin │ │ - read every namespace's secrets │ │ - schedule a pod on a control node │ │ - exfil, persist, pivot to cloud IAM │ └──────────────────────────────────────┘

The path from a reachable FaaS service to cluster-admin. The decisive crossing is step 2 to step 3: the instant an unvalidated pod spec is scheduled, "running a function" becomes "running on the node."

The decisive junction is step 2 to step 3. Steps 1 and 2 are inside the FaaS sandbox: the attacker is invoking or deploying a function, which is what the platform offers everyone. The instant step 3 succeeds and the scheduled pod carries hostPID or privileged, the blast radius stops being "this function's container" and becomes "this node and everything its kubelet and service account can reach." A scanner that confirmed the function image was patched, the trigger was scoped, and the manifest was clean saw three green checks. The attacker who set one field in a pod spec owns the node.

A walk through the boundary crossing

To make the crossing concrete, here is the path against a serverless-on-Kubernetes deployment of the kind these CVEs describe, with the destructive last steps omitted. The target runs a FaaS control plane in namespace fission on a shared cluster, with tenants permitted to create Function and Environment objects in their own namespaces, a common "bring your own function" setup.

Step 1, reach the service. The attacker, holding a low-privilege foothold (a tenant account, or a single compromised pod elsewhere in the cluster), checks whether the router and builder services are gated. They are ClusterIP services and the cluster has no default-deny network policy, so any pod can dial them. The unauthenticated invocation route from CVE-2026-46614 answers.

# From any pod in the cluster, the router answers an internal route
# that no HTTPTrigger ever exposed:
$ curl -s http://router.fission.svc.cluster.local:8888/fission-function/team-a/report-export
{"status":"ok","fn":"report-export"}      # invoked without a trigger or auth

Step 2, deploy a function I control. Reaching the router is reconnaissance; the escalation is a pod spec. The attacker submits an Environment whose spec.runtime.podSpec sets the fields the merge layer fails to filter. This is the CVE-2026-50545 / CVE-2026-50564 primitive.

apiVersion: fission.io/v1
kind: Environment
metadata:
  name: builder-x
  namespace: team-a            # legitimately the attacker's namespace
spec:
  runtime:
    podSpec:                   # merged into the real pod, unvalidated
      hostPID: true            # share the node's process namespace
      serviceAccountName: fission-executor   # not the attacker's SA
      containers:
        - name: fn
          securityContext:
            privileged: true   # drop container isolation
          volumeMounts:
            - { name: host, mountPath: /host }
      volumes:
        - name: host
          hostPath: { path: / }   # the node's root filesystem

Step 3, break out to the node. The platform schedules the pod. Because hostPID and privileged passed through unfiltered, the container shares the node's process namespace and runs without the isolation that would normally contain it, and /host is the node's root filesystem. The attacker is no longer in a function sandbox; they are on the node. This is the boundary crossing.

Step 4, lift the service-account token. The pod was scheduled under fission-executor, a high-privilege account, and the kubelet automounts its token. Even without the pod-spec trick, the related CVE-2026-46617 shows the same shape: the fission-fetcher service account had namespace-wide read on secrets and config maps, and its token was reachable from inside the user's function container, so ordinary function code could already read every secret in the namespace.

# From the broken-out pod, read the mounted token and ask what it can do:
$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
$ kubectl --token="$TOKEN" auth can-i --list
Resources        Non-Resource URLs   Resource Names   Verbs
secrets          []                  []               [get list]
pods             []                  []               [create get list]
serviceaccounts  []                  []               [get list]
# A function's identity that can read secrets and create pods cluster-wide.

Steps 5 and 6, enumerate and escalate. The token can list and read secrets beyond the attacker's namespace and create pods. Cluster-wide secret read alone is a full compromise of every credential the cluster holds: database passwords, cloud IAM keys, registry tokens. From "deploy a function in my own namespace" the attacker has reached "read every secret in the cluster and schedule a pod wherever I like." On a managed cluster, those harvested cloud-IAM credentials are the same crossing into the cloud control plane that the cloud-admin chain piece walks. The FaaS layer was the entry; the cluster, and then the cloud, was the destination.

No memory corruption, no zero-day binary. Each step is the platform doing what it was built to do, with one input it failed to constrain. A row of "patched image, scoped trigger, clean manifest" green checks, and the cluster is gone.

Why scanners miss the runtime trust boundary

The reason this class survives a clean scan report is mechanical, and it is the same reason the multi-tenant read survived a green CSPM report. The dangerous property is a relationship between two runtime facts: can an untrusted caller reach this service, and does an admission control reject a dangerous pod spec before the kubelet schedules it. Neither fact lives in an image or a static manifest.

The honest read: serverless-on-Kubernetes lateral movement is a high-impact, low-frequency finding that, when it lands, hands over the whole cluster. It is reliably missed by single-artifact tooling because the vulnerability is the runtime relationship between two correct-looking artifacts, not a flaw in either one.

What to do about it: constrain the substrate, not just the function

The leverage in defense is that you do not have to fix every function. You have to cut two edges: make the FaaS services unreachable by untrusted callers, and make a dangerous pod spec impossible to schedule. Sever either and the tree above collapses even if a function bug survives.

Serverless-on-Kubernetes hardening contract: edges to cut

A function builder is an authenticated remote-code-execution surface by design. The platform's only job is to keep the "authenticated" and the "constrained" parts true, and a single unvalidated pod-spec field breaks both.

The cheapest first move for a platform lead this week: confirm a default-deny NetworkPolicy fronts every FaaS service, and confirm Pod Security Admission is enforced at restricted on every namespace where tenants can submit function or environment definitions. Those two controls, done honestly, cut the two edges in every walk above, and neither requires waiting on a vendor patch.

How Celvex catches this

Find. Prove. Fix. Verify.

Find

We fingerprint serverless-on-Kubernetes control planes and map the runtime boundary: which FaaS services answer an unauthenticated caller, and whether admission control would reject a host-namespace or privileged pod spec, using read-only public-vantage and in-scope structural probes.

Prove

A confirmed crossing ships a signed Proof Capsule that documents the full path: the reachable service, the unvalidated pod-spec field, the node-level break-out, and the service-account token reached, with the exact requests, in a controlled non-destructive replay.

Fix

The Capsule's remediation block names the cheapest edge to cut, usually the default-deny NetworkPolicy or the restricted Pod Security Admission, so the customer kills the whole chain with one control instead of chasing every function.

Verify

After the fix, we re-walk the path. The router rejects the unauthenticated call, admission rejects the dangerous pod spec, the token is gone, and the dashboard records a verified-fix event with the severed edge for the audit trail.

Where we sit honestly on the autonomy curve: today our corpus models the serverless-on-Kubernetes boundary as a named cross-layer chain, FaaS service to pod-spec passthrough to node to service-account token, and ships a reproducible Capsule for the crossings it can confirm against scoped assets. Our near-term work is breadth: extending the runtime probe across more FaaS-on-K8s control planes and managed-cluster variants, the same trust-channel primitive on different plumbing. We do not claim to model every cluster. We do claim to model the boundary crossing that image scanners and manifest linters structurally cannot: the one where a function deploy becomes the node, and the node becomes the cluster. You can see the deeper architecture in our cloud security validation capability and the evidence format on the Proof Capsule page.

Bottom line

Serverless-on-Kubernetes is the rare platform whose explicit job is to run untrusted code on shared infrastructure, which makes its trust boundaries the whole game. The Fission disclosures show both halves failing at once: an unauthenticated router and storage service that any in-cluster caller could reach (CVE-2026-46614, CVE-2026-46612), and a pod-spec passthrough that let a tenant set hostPID, privileged, and serviceAccountName on a scheduled pod (CVE-2026-50545, CVE-2026-50563, CVE-2026-50564, all CVSS 9.9). Stitched together they walk from "deploy a function in my namespace" to "read every secret in the cluster." Image scanners and manifest linters miss it because the vulnerability is the runtime relationship between two correct-looking artifacts. The fix is leverage: a default-deny network policy in front of every FaaS service and Pod Security Admission at restricted on tenant namespaces cut the two decisive edges, and the whole tree collapses even before the patch lands. Then prove the crossing fails, and verify the proof held.

Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.

Sources

Find out if your FaaS layer is a deploy pipeline for an attacker.

Free Exposure Check, no signup required. We map the runtime boundary between tenant input and the node, and ship a signed Proof Capsule for the highest-confidence crossing we can walk against your scoped assets.

Run a Free Scan →