Skip to content

feat(agents): authenticate deployed agents via service-principal sidecar - #914

Merged
benmccown merged 5 commits into
mainfrom
fix-deployments-auth
Jul 27, 2026
Merged

feat(agents): authenticate deployed agents via service-principal sidecar#914
benmccown merged 5 commits into
mainfrom
fix-deployments-auth

Conversation

@benmccown

@benmccown benmccown commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

TL;DR

When platform auth is enabled, a deployed agent's Inference Gateway calls were rejected with 401 — the agent (a NAT runtime whose OpenAI HTTP client we don't control) carries no platform credential (api_key: "not-used"). This injects a loopback auth-proxy sidecar that stamps a service-principal identity so those calls authenticate. Works for both k8s and docker agent deployments.

The problem

Follows #899 (which fixed connectivity — the agent reaching the internal API Service). This fixes authorization: with auth on, the agent's IGW call had no principal and got a 401 → OpenAI-client retry loop.

Per @ironcommit, static service:* identity is the intended fallback while workload token exchange is unavailable, and it's what the docker monolith already depends on. So this uses that mechanism rather than the (out-of-scope) RFC 8693 token exchange.

The fix

nemo-deployments owns the sidecar; nemo-agents just requests it.

  • New auth-proxy sidecar (nmp_common/auth/workload_proxy): a loopback FastAPI forwarder run via nemo services run --sidecars auth-proxy (registered in AVAILABLE_SIDECARS). It strips inbound auth/principal headers and stamps X-NMP-Principal-Id: service:<identity>, which OPA authorizes via the ServiceSystem role. Streams responses.
  • DeploymentConfig flags: auth_proxy_sidecar: bool + auth_proxy_sidecar_identity: str | None. The deployments plugin compiles and injects the sidecar from these flags (nemo_deployments_plugin.auth_proxy), for both backends:
    • k8s — native init-container sidecar (restartPolicy: Always); upstream is the in-cluster API Service DNS.
    • docker — sidecar sharing the primary's network namespace (agent reaches it on localhost); upstream is rewritten to a docker-reachable host (e.g. host.docker.internal / bridge addr) via determine_loopback_override(), mirroring fix(agents): make deployed agent inference URL container-reachable #899.
    • Injection is a no-op when platform auth is disabled.
  • nemo-agents sets the two flags (identity agents) and points the agent's llms.*.base_url at the proxy (http://127.0.0.1:8090) when auth is on. Unchanged when auth is off (direct Service DNS per fix(agents): make deployed agent inference URL container-reachable #899). It does not build the sidecar or know its mechanics.
  • Plugins don't import nmp_common: added nemo_platform_plugin.auth.platform_auth_enabled() as the wrapper both plugins consult.
  • Fix latent k8s compiler bug: exec probes failed because V1Probe needs the _exec kwarg (exec is a Python keyword). The sidecar uses an exec readiness probe since it binds loopback only (a pod-IP httpGet probe is refused).

The NIM/vLLM model images are never touched. The LoRA adapters sidecar already self-injects service:models via get_platform_sdk(as_service=...) and is unaffected — the proxy is only for workloads that can't set the header themselves.

Verified end-to-end (dev pod, auth enabled)

  • k8s (kind + Helm): agent deployment reaches 2/2 Running (auth-proxy: ready=true via the exec probe); invoke returns HTTP 200 {"value":"...96..."} through the sidecar + a real nvidia-build model.
  • docker (local nemo services run): deploy --mode docker yields the agent + auth-proxy containers (Up (healthy)), sidecar network=container:<agent> (shared netns), env NMP_AUTH_PROXY_PRINCIPAL=agents / NMP_BASE_URL=http://172.17.0.1:8080. Agent httpx log shows it calling 127.0.0.1:8090; the IGW logs status_code=404 (model resolution) — not 401, confirming the service:agents principal authenticated.

Tests

  • Sidecar forwarder: principal stamping, passthrough, /healthz.
  • Deployments-side build_auth_proxy_container: requested / auth-off no-op / identity default / docker loopback-upstream rewrite; docker build_docker_plan injection.
  • Agents-side: flag-setting + base_url wiring (auth on/off).
  • k8s exec-probe compiler serialization (_exec).

Summary by CodeRabbit

  • New Features
    • Added an auth-proxy sidecar to forward platform-bound requests while stamping a service identity and preventing header spoofing.
    • When platform auth is enabled, inference traffic is routed through the local auth-proxy for both Kubernetes and Docker deployments (configurable image and port).
    • Added /healthz for health/readiness checks.
  • Bug Fixes
    • Improved Kubernetes probe handling for exec-based probes.
  • Tests
    • Expanded coverage for principal stamping/normalization, status passthrough, sidecar injection, docker URL rewriting, probe serialization, and proxy shutdown cleanup.

When platform auth is enabled, a deployed agent's Inference Gateway calls
were rejected with 401 because the agent (a NAT runtime whose OpenAI HTTP
client we do not control) carries no platform credential — its LLM api_key
is the placeholder "not-used".

This injects a loopback auth-proxy sidecar (the nmp-api image running
`nemo services run --sidecars auth-proxy`) into k8s/docker agent deployments
when auth is enabled. The agent targets the sidecar on localhost; the sidecar
forwards to the platform stamping X-NMP-Principal-Id: service:agents, which
the OPA policy authorizes via the ServiceSystem role. This is the same static
service identity the platform's own SDK clients use (get_platform_sdk
as_service=...); the proxy exists only for workloads that cannot set the
header themselves. The LoRA adapters sidecar already self-injects
service:models via the SDK and is unaffected.

- New workload_proxy sidecar: loopback FastAPI forwarder that strips inbound
  auth/principal headers and stamps the service principal, streaming responses.
  Registered as "auth-proxy" in AVAILABLE_SIDECARS.
- Agents backend injects the sidecar (as a native init-container sidecar with
  restartPolicy=Always) and points the agent's llms.*.base_url at it when
  auth is enabled; unchanged behavior when auth is off.
- Fix k8s compiler exec-probe: V1Probe needs the `_exec` kwarg (`exec` is a
  Python keyword); the sidecar uses an exec readiness probe because it binds
  loopback only and a pod-IP httpGet probe would be refused.
- Unit tests for the sidecar forwarder, agents injection/base_url wiring, and
  the exec-probe compiler path.

Verified end-to-end on kind with auth enabled: agent deployment reaches 2/2
Running (sidecar ready via exec probe) and the agent's IGW call authenticates
(reaches model resolution instead of 401).

Signed-off-by: Ben McCown <bmccown@nvidia.com>
@benmccown
benmccown requested review from a team as code owners July 27, 2026 17:34
@github-actions github-actions Bot added the feat label Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d863b043-0cbf-4182-844b-224f73051937

📥 Commits

Reviewing files that changed from the base of the PR and between ee535eb and dba859b.

📒 Files selected for processing (2)
  • packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py
  • packages/nmp_common/tests/auth/test_workload_proxy.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py

📝 Walkthrough

Walkthrough

Adds a FastAPI auth-proxy sidecar that stamps service-principal headers, registers and configures the sidecar, routes authenticated agent traffic through loopback, injects it into Kubernetes and Docker workloads, and adds probe and forwarding tests.

Changes

Auth proxy deployment

Layer / File(s) Summary
Proxy runtime and forwarding behavior
packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py, packages/nmp_common/tests/auth/test_workload_proxy.py, packages/nmp_platform_runner/src/nmp/platform_runner/registry.py
Adds streaming forwarding, header filtering, principal normalization, health checks, uvicorn lifecycle handling, sidecar registration, and forwarding tests.
Sidecar configuration and authentication gating
packages/nemo_platform_plugin/src/nemo_platform_plugin/auth.py, plugins/nemo-deployments/src/nemo_deployments_plugin/config.py, plugins/nemo-deployments/src/nemo_deployments_plugin/entities.py, plugins/nemo-deployments/src/nemo_deployments_plugin/auth_proxy.py, plugins/nemo-deployments/tests/unit/test_auth_proxy.py
Adds platform-auth detection, sidecar image and port configuration, identity validation, upstream URL resolution, proxy container construction, and unit tests.
Authenticated deployment injection
plugins/nemo-agents/..., plugins/nemo-deployments/src/nemo_deployments_plugin/backends/{k8s,docker}/*, plugins/nemo-agents/tests/unit/test_runner_deployments.py, plugins/nemo-deployments/tests/unit/backends/docker/test_containers.py
Routes authenticated inference traffic through loopback, injects the proxy into Kubernetes and Docker plans, and tests authenticated and unauthenticated paths.
Kubernetes probe compilation
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/k8s/compiler.py, plugins/nemo-deployments/tests/unit/backends/k8s/test_compiler.py
Uses the Kubernetes client’s _exec probe field and verifies exec and HTTP probe serialization.

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant AuthProxy
  participant Platform
  Agent->>AuthProxy: Send inference request to loopback
  AuthProxy->>Platform: Forward request with stamped principal header
  Platform-->>AuthProxy: Return response
  AuthProxy-->>Agent: Stream response
Loading

Possibly related PRs

Suggested labels: test

Suggested reviewers: stefan-kickoff, mckornfield, mikeknep, svvarom

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: deploying agents with a service-principal auth-proxy sidecar.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-deployments-auth

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py`:
- Around line 66-74: Update _platform_auth_enabled so failures from
get_auth_config are not converted to False; propagate the exception or
explicitly fail deployment instead. Preserve the boolean enabled result for
successfully resolved configuration, and ensure deployment cannot proceed as
unauthenticated when auth-config resolution fails.
- Around line 364-370: Update the deployment-building flow around
_build_auth_proxy_sidecar so Docker deployments do not append the proxy to
init_containers, which Docker rejects. Add a Docker-compatible sidecar and
network wiring for auth_proxy, while preserving the existing native sidecar
behavior for non-Docker modes; add coverage for an auth-enabled Docker
deployment verifying the proxy is configured and startup remains valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: df617f3b-67d2-46c4-9b27-8935a13853b4

📥 Commits

Reviewing files that changed from the base of the PR and between 2bbe311 and 575ea2e.

📒 Files selected for processing (8)
  • packages/nmp_common/src/nmp/common/auth/workload_proxy/main.py
  • packages/nmp_common/tests/auth/test_workload_proxy.py
  • packages/nmp_platform_runner/src/nmp/platform_runner/registry.py
  • plugins/nemo-agents/src/nemo_agents_plugin/config.py
  • plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py
  • plugins/nemo-agents/tests/unit/test_runner_deployments.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/k8s/compiler.py
  • plugins/nemo-deployments/tests/unit/backends/k8s/test_compiler.py

Comment thread plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py Outdated
Comment thread plugins/nemo-agents/src/nemo_agents_plugin/runner/deployments_backend.py Outdated
@benmccown benmccown self-assigned this Jul 27, 2026
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 27292/35040 77.9% 62.1%
Integration Tests 16038/33752 47.5% 20.0%

…flags

Addresses PR feedback: the auth-proxy sidecar belongs to nemo-deployments
(which compiles containers into deployments), not nemo-agents. Also removes a
direct nmp_common reference from the agents plugin.

- DeploymentConfig gains `auth_proxy_sidecar: bool` and
  `auth_proxy_sidecar_identity: str | None`. The deployments plugin compiles
  and injects the sidecar (nemo_deployments_plugin.auth_proxy) from these flags,
  interpolating the identity into the service-principal header. Injection is a
  no-op when platform auth is disabled.
- nemo-agents now just sets the two flags (identity="agents") and points the
  agent's llms.*.base_url at the proxy port when auth is enabled; it no longer
  builds the sidecar container or reads deployments image/port config.
- Plugins no longer import nmp_common: added nemo_platform_plugin.auth
  .platform_auth_enabled() wrapper; both agents and deployments consult it.
- Sidecar image/port config moved from AgentsConfig to DeploymentsConfig.
- Tests: deployments-side build_auth_proxy_container (requested/auth-off no-op/
  identity default) and agents-side flag-setting + base_url wiring.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
…h fallback

Addresses PR feedback.

- Docker backend now injects the auth-proxy sidecar too (build_docker_plan),
  reusing build_auth_proxy_container. The sidecar shares the primary container's
  network namespace, so the agent reaches it on localhost:8090 — the same
  loopback address used in k8s. Previously the sidecar was injected only in the
  k8s compiler, so auth-on docker agent deployments got no identity and would
  401.
- The docker sidecar's upstream (NMP_BASE_URL) is rewritten to a
  docker-reachable host via determine_loopback_override() (e.g.
  host.docker.internal on macOS) when the platform base URL is a loopback,
  mirroring the agent-side rewrite from #899. K8s uses the Service DNS verbatim.
- Documented platform_auth_enabled()'s fail-to-False behavior: the realistic
  failure is ImportError (package used outside the platform image); other
  failures are effectively unreachable in the controller (missing config ->
  defaults; malformed config would have crashed startup; the read is cached).
- Tests: docker auth-proxy injection, auth-off no-op, and loopback upstream
  rewrite.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
…er stripping

Addresses PR feedback.

- Remove the default service-principal identity. A DeploymentConfig with
  auth_proxy_sidecar=True now requires auth_proxy_sidecar_identity: a
  model_validator rejects the invalid combination, surfaced as a 4xx at the
  create endpoint. The sidecar's run() likewise requires NMP_AUTH_PROXY_PRINCIPAL
  rather than defaulting. No silent "agents" fallback anywhere.
- Trim the auth-proxy's header sanitization to only what would be actively
  wrong. Request: host + content-length (httpx recomputes), authorization +
  x-nmp-principal-id (we stamp the identity; must not be spoofed/conflict).
  Response: content-length + transfer-encoding (responses are streamed). Dropped
  the hop-by-hop policing that a trusted loopback sidecar doesn't need.
- Tests: replace the identity-default test with a validation-rejection test.

Signed-off-by: Ben McCown <bmccown@nvidia.com>

@ironcommit ironcommit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

…nt cleanup

Addresses review feedback on the auth-proxy sidecar.

- Add a FastAPI lifespan that closes the shared httpx AsyncClient on shutdown so
  its connection pool is released gracefully instead of relying on process exit.
- Remove the redundant BackgroundTask(response.aclose): the finally in the
  streaming generator already runs on normal completion, exception, and client
  disconnect, so the response is always closed there. httpx's aclose() is
  idempotent, so the duplicate was harmless but implied cleanup was missing.
- Test that the lifespan actually closes the upstream client.

Not changed: the request body is still buffered before forwarding. Requests
through this proxy are a workload's outbound platform API calls (bounded in
size), and streaming them would force chunked transfer-encoding on every
request since we strip content-length. Revisit if large payloads are routed
through the proxy.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
@benmccown
benmccown enabled auto-merge July 27, 2026 19:41
@benmccown
benmccown disabled auto-merge July 27, 2026 19:49
@benmccown
benmccown added this pull request to the merge queue Jul 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 27, 2026
@benmccown
benmccown added this pull request to the merge queue Jul 27, 2026
Merged via the queue into main with commit a87425b Jul 27, 2026
108 of 110 checks passed
@benmccown
benmccown deleted the fix-deployments-auth branch July 27, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants