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.
- The in-cluster trust boundary is assumed, not enforced. A
ClusterIPservice is reachable by every pod in the cluster unless aNetworkPolicysays 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. - Pod-spec passthrough is a convenience that became an injection point. To let tenants tune resource limits and runtime images, Fission exposed
spec.runtime.podSpecon the Environment CRD andFunction.spec.podspecon the container executor, then merged them into the real pod. CVE-2026-50564 documents the merge propagatinghostNetwork,hostPID,hostIPC,privileged, andserviceAccountNamewith 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. - 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
privilegedbefore 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
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.
- Image scanners answer the wrong question. They tell you the function's base image has no known CVEs. They cannot tell you that the executor will cheerfully schedule that image with
privileged: truebecause nothing validates the merged pod spec. - Manifest linters check the manifest the platform ships, not the pod a tenant induces. The vulnerable pod spec does not exist in any committed YAML. It is synthesized at runtime from tenant input merged into the platform template. A linter reading the repository never sees it.
- The reachability fact is network-state, not config. Whether a
ClusterIPis reachable by a hostile pod depends on whether aNetworkPolicyexists and what the CNI enforces, a property of the live cluster, not of any single file. A policy audit that reads IAM and RBAC never looks at the pod-to-service reachability graph.
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
- Default-deny network policy around every FaaS service. Apply a Kubernetes NetworkPolicy that denies all ingress to the router, builder, executor, and storage services except from the named platform components that legitimately call them. A
ClusterIPis not a boundary; the network policy is. - Never expose builder, router, or storage outside the cluster. Audit every Ingress and LoadBalancer for a route that lands on a FaaS internal service. Outside-the-cluster reach turns an in-cluster RCE into an internet-facing one.
- Enforce Pod Security Admission at
restrictedon tenant namespaces. The restricted Pod Security Standard forbidshostPID,hostIPC,hostNetwork,privileged, and host-path mounts. With it enforced at admission, the merged pod spec is rejected before the kubelet ever sees it, which closes the CVE-2026-50545 / 50563 / 50564 class regardless of what the platform's own validation does. - Add an admission policy that denies tenant-set security-relevant pod fields. Even with Pod Security Admission, add a Gatekeeper or Kyverno rule that rejects any tenant-submitted Environment or Function whose pod spec sets
serviceAccountName,privileged, or host namespaces. Defense in depth on the exact fields the merge layer trusted. - Scope and de-automount the platform service accounts. The executor and fetcher accounts should hold the minimum RBAC their job requires, following Kubernetes RBAC good practices, and function pods that do not call the API server should set
automountServiceAccountToken: falseso a broken-out container finds no token to lift. - Patch the platform and verify the boundary, do not assume it. Upgrade Fission to 1.24.0 or later (1.25.0 for the full sweep). Then prove an unauthenticated caller cannot reach the router and a dangerous pod spec is rejected at admission, by attempting both in a controlled, evidence-producing way, and re-attempting after the fix.
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.
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.
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.
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.
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
- NVD: CVE-2026-50545 (Fission Environment podSpec passthrough, CVSS 9.9)
- NVD: CVE-2026-50563 (Fission Container Executor podspec passthrough, CVSS 9.9)
- NVD: CVE-2026-50564 (Fission Environment merge propagates host namespaces / privileged)
- NVD: CVE-2026-46614 (Fission router unauthenticated function invocation)
- NVD: CVE-2026-46612 (Fission storagesvc unauthenticated archive CRUD)
- NVD: CVE-2026-46617 (Fission fission-fetcher service-account token reachable from function code)
- Fission security advisory GHSA-wmgg-3p4h-48x7 (CVE-2026-50545)
- Fission security advisory GHSA-v455-mv2v-5g92 (CVE-2026-50563)
- Fission security advisory GHSA-3g33-6vg6-27m8 (CVE-2026-46614)
- Fission v1.24.0 release (podSpec validation fixes)
- Kubernetes: Pod Security Standards (restricted profile)
- Kubernetes: Network Policies (default-deny)
- Kubernetes: RBAC Good Practices (least privilege)
- Kubernetes: Configure Service Accounts (automountServiceAccountToken)
- CWE-306: Missing Authentication for Critical Function
- CELVEX Group: Proof Capsule format
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 →