Skip to content

fix: [NPM] runtime bounds and configuration follow-up - #4858

Draft
Isaiah Raya (rayaisaiah) wants to merge 3 commits into
masterfrom
isaiahraya/npm-runtime-bounds
Draft

fix: [NPM] runtime bounds and configuration follow-up#4858
Isaiah Raya (rayaisaiah) wants to merge 3 commits into
masterfrom
isaiahraya/npm-runtime-bounds

Conversation

@rayaisaiah

@rayaisaiah Isaiah Raya (rayaisaiah) commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Reason for Change:

Summary

The two runtime changes extracted from #4831. The four policy-translation fixes are reviewed and validated independently in #4859.

This PR remains draft because HTTP access/lifecycle, metric semantics and managed-configuration decisions are not settled. It does not claim complete runtime resource bounding.

Problems addressed and proposed changes

1. Bound HTTP work and make debug/profiling access explicit

Problem: The HTTP server previously had no explicit read/write/idle deadlines, header bound or connection ceiling. The cache endpoint constructs a complete serialized cache response per request, so concurrent slow requests can retain multiple responses and compete with normal serving work.

Debug and profiling routes also shared the configured listener with metrics. Their exposure and lifecycle need to be intentional rather than incidental to enabling the server.

Proposed change:

  • Read-header, read, write and idle deadlines; a 64 KiB header limit; a 32-connection listener limit.
  • A single in-flight cache response, with additional requests receiving 503.
  • Debug/profiling disabled by default and filtered to loopback-origin requests when enabled.
  • A narrower profiling mount instead of exposing the entire /debug/ subtree of the default mux.
  • Normal server closure is not logged as an error.

Prometheus routes remain reachable off-node for scraping. Loopback filtering is an access boundary, not authentication: host-network processes remain inside it. The connection pool is still shared, and the existing startup paths do not yet provide graceful HTTP shutdown.

2. Materialize IPSets on demand and bound optional inventory detail

Problem: NPM tracks label-derived sets on each node. Applying every tracked set to the kernel makes kernel work grow with label inventory even when no policy references those sets. Tracking every set as a separate metric series adds another source of retained state and scrape work.

Proposed change:

  • Enable the existing ApplyIPSetsOnNeed option by default. Only policy-referenced sets need kernel materialization; membership tracking is retained so later references remain correct.
  • Limit the optional per-IPSet inventory breakdown to 20,000 series while retaining existing aggregate accounting.
  • Set the relevant values explicitly in the in-repository deployment configuration.

ApplyIPSetsOnNeed already exists; this PR changes its default rather than introducing a new property. A loaded configuration is not merged with DefaultConfig, so configuration generated elsewhere must explicitly include the intended value.

The metric limit is not a total heap or response-byte bound. Per-set detail is omitted beyond the cap, while the aggregate inventory map and userspace label bookkeeping still grow with input.

Decisions needed before leaving draft

  1. Metric contract: Agree on omitted-series behavior and an explicit omission signal, or use a policy-referenced inventory instead. Kernel-scoping changes existing metric semantics and needs consumer review.
  2. Connection isolation: Decide how to prevent shared connection slots from starving metric scrapes; handler-level throttling does not solve listener-level contention.
  3. Debug access: Confirm the operational migration for off-node debug/profiling clients, which now receive 403.
  4. Managed configuration: Coordinate explicit ApplyIPSetsOnNeed activation in the managed configuration generator and its rollout. Changing the compiled default alone does not update an existing mounted ConfigMap.
  5. Lifecycle: Thread caller-owned cancellation through the startup paths and define graceful shutdown behavior.
  6. Retained-state scale: Measure heap/RSS, scrape behavior and recovery with the complete retained workload state. A per-series cap does not settle all userspace growth questions.

Tests and evidence scope

The branch contains tests for HTTP limits, loopback filtering, profile routing, and the inventory cap/accounting. Earlier combined-branch runs are historical evidence only, not a standalone certification of this draft.

The full standalone validation in #4859 covers the translation branch, not these runtime changes. Runtime acceptance testing should be finalized after the decisions above, using the actual managed configuration.

Issue Fixed:

The runtime portion of #4831, pending the decisions above.

Requirements:

  • uses conventional commit messages
  • includes documentation in this description
  • adds unit tests
  • relevant PR labels added

…e node

The API listens on the host network of a privileged process, and its server was
created with only an address and a handler: no read, write or idle deadline, no
header bound, no limit on concurrent connections. The cache route additionally
serializes the entire policy cache into memory per request while holding the
cache lock, so slow clients could hold an unbounded number of full cache copies
alive.

The server now has deadlines, a header bound, a connection ceiling, and admits a
single cache encoding at a time with the rest shed as 503. A graceful close is
no longer reported as a failure.

The debug and profiling routes are also served only to requests that originate
on the node itself. Any pod on the node could otherwise reach them through the
node address it reads from the downward API, and the debug route returns the
whole policy cache. A pod has its own network namespace and cannot reach the
node's loopback, while the tooling that consumes these routes already connects
over localhost, so its only caller is unaffected and a refused request is
answered before the cache is encoded. The Prometheus routes are deliberately
left reachable, because they are scraped from off the node.

The profiling handlers move to the pprof prefix rather than /debug/. The router
matched a /debug/ prefix ahead of the individually named pprof routes, so those
never ran and every handler on the default mux was reachable under /debug/.
Mounting the default mux at the pprof prefix serves the profiles, including the
subpaths the named list missed, and exposes nothing else registered on that mux.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ipsets on demand

Two default changes, both of which only take effect where the deployed
configuration carries them.

The debug and profiling routes are no longer on by default. They are served
unauthenticated on the host network, so they are opt-in rather than the fallback
when a config file is missing or unreadable.

ApplyIPSetsOnNeed now defaults to true, so an ipset reaches the kernel only once
a network policy references it. NPM creates two ipsets per distinct pod label on
every node and applied every set unconditionally, and Kubernetes places no limit
on how many labels a pod may carry, so one pod with tens of thousands of labels
pushed thousands of sets into every node's kernel, pinned agent CPU and memory
until agents were OOM killed, and delayed policy programming in unrelated
namespaces. Sets are still tracked and pods still join them, so a set is already
populated by the time a policy references it; only kernel materialization is
deferred, and enforcement is unchanged.

Note that NPM does not merge its config file with DefaultConfig, so a cluster
that mounts a configmap takes Go zero values for absent keys. Both keys have to
be set in the deployed configmap, not only here.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Deferring kernel materialization does not on its own bound what a pod's labels
cost in the agent. The sets are still tracked, which is what keeps enforcement
correct, and ipset_counts carries the set name as a label, so the number of
series it reported followed workload labels rather than anything an operator
controls. One pod carrying tens of thousands of labels added that many series on
every node, which both retained them in the agent and inflated the response
built for each scrape of an endpoint served on the host network.

The per-set breakdown now stops growing at a bound far above what a cluster's
namespaces, policies and workloads produce. The aggregate counters are untouched
and stay exact, and nothing NPM does reads the breakdown, so only reported
detail is limited; an operator can tell it is incomplete by comparing the
reported series against num_ipsets.

Measured with one pod carrying 34,000 labels: agent memory for those labels
drops from 88.8 MB to 14.6 MB against a 300 MiB container limit, and the metrics
response from 6.42 MB to 1.87 MB, which no longer varies with the labels a
workload chooses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant