#3666·fission

RFC-0030 phase 2: per-function env/envFrom on the poolmgr executor (+ fission/environments runtime support)

Author: sanketsudakeCreated Aug 5, 2026Updated Aug 20, 2026
Labelsenhancementarea-functionarea-apiarea-environmentexecutor-poolmgrpriority/high

Scoping issue for RFC-0030 phase 2 — per-function Env/EnvFrom on the poolmgr executor. Parent epic: #3628. Design: docs/rfc/0030-function-env-vars-secret-configmap-mapping.md §3.

Phases 1, 3, 4 shipped (newdeploy/container native injection, MountPath files mode, run-local parity). Poolmgr is the one execution backend still gated off: today FunctionSpec.validateEnvForAdmission rejects it outright —

go
// pkg/apis/core/v1/validation.go:434-440
if et := spec.InvokeStrategy.ExecutionStrategy.ExecutorType; et == ExecutorTypePoolmgr || et == "" {
    errs = errors.Join(errs, MakeValidationErr(ErrorInvalidObject, "FunctionSpec.Env", "",
        "env/envFrom are not supported on the poolmgr executor yet (lands with RFC-0030 phase 2); use the newdeploy or container executor"))
}

Phase 2 is a coordinated cross-repo change: the executor/fetcher wire work here in fission/fission, plus runtime support in fission/environments (python + node first). Neither half is useful alone. This issue captures the full technical approach and the two decisions that must be made before the runtime PRs start, because they shape the wire contract those PRs implement.


⛔ Decision 1 (blocking): how a runtime advertises env-injection support

The executor must refuse to build a specialize payload carrying Env/EnvFrom for a runtime image that does not apply it — otherwise the function deploys with a silently-empty DATABASE_URL (RFC-0030 §3, acceptance E5). Two options for the capability signal:

  • (a) Bump Environment.Spec.Version to 4. Cheap, but a weak and wrong-shaped signal: it is user-declared, CEL-bounded 1..3 and immutable (self == oldSelf), so every unrelated runtime would have to "claim" a version it does not implement, and v1 environments receive an empty specialize body so the payload cannot ride v1 at all.
  • (b) An explicit capability the runtime advertises (queried once at specialize, cached). Cleaner, and it is the only option that also enables a fetcher-side defense-in-depth check (see §3 — the fetcher today receives only EnvVersion and has no capability endpoint). Cost: a new contract with fission/environments (a /v2/getinfo-style endpoint or a field in the specialize response).

Recommendation: (b). The advertisement shape is an input to the python/node PRs, so this must land first. Proposed minimal shape: the runtime exposes its supported feature set (e.g. {"specializeEnv": true}) on an unauthenticated read-only /info endpoint OR echoes it in the /v2/specialize response; the executor caches per-image. Discuss the exact shape here before implementation.

⛔ Decision 2 (blocking): poolmgr Env scope under dynamic/cluster tenancy

The specialize channel must be authenticated before secret-bearing env rides it (below). The NetworkPolicy prerequisite renders into a single namespace (functionNamespace/defaultNamespace), but under dynamic/cluster tenancy function pods live in runtime-onboarded namespaces where the tenant controller provisions ServiceAccounts, RoleBindings and derived-key Secrets — but no NetworkPolicy (pkg/tenant/provision.go). So the prerequisite is unenforceable in exactly the multi-tenant mode whose threat model motivates it. Choose:

  • (a) The tenant controller provisions the specialize-guard NetworkPolicy per onboarded namespace (preferred — same provisioning path it already runs), or
  • (b) §3 declares poolmgr Env/EnvFrom unsupported under dynamic/cluster tenancy until (a) lands, and admission rejects it there.

Work item A — fission/fission executor + fetcher wire contract

The security-critical core (RFC-0030 §3). FunctionLoadRequest is built by the executor and, on newdeploy, marshalled into the fetcher container's command args — i.e. into the Deployment pod template, readable by anyone with get pods/deployments and stored in etcd. Therefore the value must never be executor-side:

  1. Executor-side payload carries references only — an EnvSources reference list (non-secret) on the executor-built type. Never a resolved value. (pkg/fetcher/config, pkg/fetcher/types.go.)
  2. Fetcher resolves values pod-locally on a distinct type that is not reachable from FunctionSpecializeRequest (the pod-args marshal serializes the whole parent, so any field hung off LoadReq is exposed through it). Resolution is same-namespace by the fetcher's existing per-namespace RBAC. Values are populated immediately before the 127.0.0.1 /v2/specialize POST.
  3. Executor-side refuse-to-build capability enforcement (Decision 1) — the authoritative E5 backstop, located executor-side because that is where the live *fv1.Environment is in hand (pkg/fetcher/config is imported by the executor, not cmd/fetcher). A fetcher-side check is not implementable on today's contract; it becomes possible as defense-in-depth only if Decision 1 picks (b).
  4. Path-B fetcherVariant predicate extension — the OCI B-direct specialize path must carry the references too.
  5. Reader-rebuild fix — the fetcher's specialize retry loop reuses a single bytes.Reader; safe only because retries currently fire on dial errors before the body is consumed. Once the payload carries env references, rebuild the reader per attempt or a retry silently mis-specializes with an empty payload.
  6. Flip the gate at validation.go:438 from always-reject to a capability check.
  7. RFC-0025 classifier (pkg/versioning/classifier.go): the auto-publish runtime-affecting default returns false, so new top-level Env/EnvFrom fields mint no version until explicitly added to the switch. Its coverage test forces the decision — name it as a choice.

Work item B — fission/environments runtime PRs (python + node first)

Each runtime applies the resolved env map to the process environment before importing user code (valid because specialization precedes user-code load) and advertises support per Decision 1.

  • pythonpython/server.py, _load_v2(self, specialize_info) (currently reads filepath/functionName and importlib.import_modules the user module): read the env map from the specialize body and apply to os.environ before the import_module call. Bump envconfig.json version + capability signal.
  • nodejsnodejs/server.js, specializeV2(req, res) (currently resolves filepath/functionName then requires the user module): apply the env map to process.env before loading the user module. Same envconfig.json/capability bump.
  • Tests: extend each runtime's tests/local_test.sh with an env-injection case; regression case for E7 (a second /v2/specialize is already rejected by published runtimes — "Not a generic container" — so this is a regression test, not new runtime work).
  • Remaining runtimes (go, jvm, php, ruby, dotnet, …) follow in phase 4; python + node prove the contract.

Security prerequisites (land WITH the runtime contract)

  • Authenticate the specialize channel. The env runtime's :8888 is pod-network-reachable, and the executor's OCI B-direct call (pkg/executor/executortype/poolmgr/oci_specialize.go) POSTs /v2/specialize over plain HTTP with no HMAC signer (unlike the fetcher's ServiceFetcher-verified :8000/specialize). Sign the B-direct path and/or require networkPolicy.enabled: true as a documented prerequisite (default is false).
  • One-shot specialize is the backstop, and already holds in published runtimes — no cross-repo change needed, covered as an E7 regression test.

Acceptance criteria (from RFC-0030, gate phase 2)

  • E3 — no resolved secret value ever appears in executor logs, pod annotations/args, or k8s events. Integration grep-gate over the five serialization/construction sites: pod-args marshal (pkg/fetcher/config), the fetcher client signed POST (pkg/fetcher/client), the B-direct load-only marshal (poolmgr/oci_specialize.go), and the two run-local constructors (pkg/fission-cli/cmd/function/run.go, run_local.go).
  • E5 — a new-field Function against an old runtime is never deployed-and-empty: rejected at admission where the Environment is visible, and refused by the executor when it builds the request where it is not. Capability-gate unit matrix + a test mutating the Environment after function admission + an integration case pinning an old env image.
  • E7 — replay: a second specialize on a specialized pod is rejected (regression test).

Open question (sequencing)

Does the one-shot-specialize / channel-auth hardening belong to this RFC or a standalone hardening PR? It fixes a pre-existing replayable /v2/specialize surface; coupling this feature to a cross-repo runtime change may be the wrong coupling — but shipping poolmgr Env without it turns that surface into a credential-theft primitive. Proposed: split the channel-auth hardening (B-direct signing + one-shot verification) into its own PR that merges first, so Env lands on an already-authenticated channel.

Not release-blocking

The poolmgr gate rejects cleanly today; newdeploy and container already carry the full feature. This is an additive extension with a cross-repo dependency and two design decisions — it should be planned as its own effort, not squeezed into a release cut.