Summary
The Fission router registers an internal-style route — /fission-function/<name> and
/fission-function/<ns>/<name> — for every Function object, independent of whether any
HTTPTrigger exists for that function. The route was mounted on the same listener as
user-defined HTTPTriggers (svc/router, port 8888), so any caller who could reach the router
could invoke any function by guessing its metadata.name (and namespace), bypassing the host /
path / method / method-allow-list restrictions encoded in HTTPTrigger objects.
Affected component
pkg/router/httpTriggers.go:280-284 — internalRoute registration via
utils.UrlForFunction(fn.Name, fn.Namespace), bound to the function handler.
Impact
An external caller who reaches the public router could:
- Invoke functions that the operator intentionally did not publish through an
HTTPTrigger
(e.g. functions used only as Kubewatcher / Timer / MessageQueue trigger targets, internal
helpers, or sample functions).
- Bypass
HTTPTrigger-level restrictions: a function published only on POST /api/v2/foo
could still be invoked as GET /fission-function/<ns>/<name> with arbitrary headers and body.
- Enumerate function names by probing the response semantics (404 vs 200 vs 502 from cold
start).
In multi-tenant deployments this also crosses tenant boundaries when functions in tenant
namespace B are reachable from tenant A's pods (or from anywhere on the internet if the router
is ingress-exposed).
Root cause
/fission-function/... was historically used by internal trigger sources (timer, kubewatcher,
mqtrigger) that share the cluster network with the router, but the route was registered on the
public listener that also serves user HTTPTriggers. The two audiences were never separated.
Fix
Released in v1.23.0:
- PR #3369 (commit
814d232c): the router now runs two listeners — a public listener
(port 8888, svc/router) that serves only user-defined HTTPTriggers, /router-healthz, and
/_version, and an internal listener (port 8889, svc/router-internal, ClusterIP-only) that
exclusively serves /fission-function/<ns>/<name>. The internal listener is wrapped with the
pkg/auth/hmac.ServiceVerifier using the ServiceRouterInternal derived key — internal trigger
sources sign their requests with a per-service HKDF-derived key from a cluster master secret.
Empty master secret falls back to pass-through (preserves compatibility for clusters not yet
rotating in a secret).
- PR #3365 (commit
0aa24788): added per-service NetworkPolicy resources to
charts/fission-all, ensuring svc/router-internal is only reachable from kubewatcher,
timer, mqtrigger, and mqt-keda pods inside the release namespace.
- The internal-listener path itself is still
/fission-function/<ns>/<name> — only its
location moved.
Mitigation (until upgrade)
- Apply a
NetworkPolicy to the Fission namespace that allows ingress to svc/router (port
- only from your ingress controller, and blocks
/fission-function/... at the ingress layer
(path-based filter on the ingress).
- Avoid exposing the router directly via LoadBalancer/NodePort; front it with an ingress that
path-filters /fission-function/.
- Treat function
metadata.name as not a secret — names should not be the access control
boundary.
Summary
The Fission router registers an internal-style route —
/fission-function/<name>and/fission-function/<ns>/<name>— for everyFunctionobject, independent of whether anyHTTPTriggerexists for that function. The route was mounted on the same listener asuser-defined
HTTPTriggers (svc/router, port 8888), so any caller who could reach the routercould invoke any function by guessing its
metadata.name(and namespace), bypassing the host /path / method / method-allow-list restrictions encoded in
HTTPTriggerobjects.Affected component
pkg/router/httpTriggers.go:280-284—internalRouteregistration viautils.UrlForFunction(fn.Name, fn.Namespace), bound to the function handler.Impact
An external caller who reaches the public router could:
HTTPTrigger(e.g. functions used only as Kubewatcher / Timer / MessageQueue trigger targets, internal
helpers, or sample functions).
HTTPTrigger-level restrictions: a function published only onPOST /api/v2/foocould still be invoked as
GET /fission-function/<ns>/<name>with arbitrary headers and body.start).
In multi-tenant deployments this also crosses tenant boundaries when functions in tenant
namespace B are reachable from tenant A's pods (or from anywhere on the internet if the router
is ingress-exposed).
Root cause
/fission-function/...was historically used by internal trigger sources (timer, kubewatcher,mqtrigger) that share the cluster network with the router, but the route was registered on the
public listener that also serves user
HTTPTriggers. The two audiences were never separated.Fix
Released in v1.23.0:
814d232c): the router now runs two listeners — a public listener(port 8888,
svc/router) that serves only user-definedHTTPTriggers,/router-healthz, and/_version, and an internal listener (port 8889,svc/router-internal, ClusterIP-only) thatexclusively serves
/fission-function/<ns>/<name>. The internal listener is wrapped with thepkg/auth/hmac.ServiceVerifierusing theServiceRouterInternalderived key — internal triggersources sign their requests with a per-service HKDF-derived key from a cluster master secret.
Empty master secret falls back to pass-through (preserves compatibility for clusters not yet
rotating in a secret).
0aa24788): added per-serviceNetworkPolicyresources tocharts/fission-all, ensuringsvc/router-internalis only reachable fromkubewatcher,timer,mqtrigger, andmqt-kedapods inside the release namespace./fission-function/<ns>/<name>— only itslocation moved.
Mitigation (until upgrade)
NetworkPolicyto the Fission namespace that allows ingress tosvc/router(port/fission-function/...at the ingress layer(path-based filter on the ingress).
path-filters
/fission-function/.metadata.nameas not a secret — names should not be the access controlboundary.