Skip to content

docs: document environment agent authentication (inbound + outbound) - #23

Merged
gabriel-farache merged 3 commits into
dcm-project:mainfrom
gabriel-farache:docs/agent_auth
Oct 19, 2026
Merged

gabriel-farache merged 3 commits into
dcm-project:mainfrom
gabriel-farache:docs/agent_auth

Conversation

@gabriel-farache

@gabriel-farache gabriel-farache commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Adds a dedicated Environment Agent Authentication guide covering the two independent auth surfaces introduced by environment-agent#35 and environment-agent#38:

  • Inbound: AGENT_AUTH_DISABLED / AGENT_AUTH_ISSUER_URL / AGENT_AUTH_JWT_AUDIENCE protecting the agent's own REST API (external SP registration, provider listing), health-path bypass, RFC 7807 error format, the intentional audience fail-open behavior, caller-token issuer and audience requirements, and JWKS fetch timing.
  • Outbound: DCM_AUTH_TOKEN / DCM_AUTH_TOKEN_ENDPOINT / DCM_AUTH_CLIENT_ID / DCM_AUTH_CLIENT_SECRET for the agent's own registration/heartbeat calls to the control plane, mode precedence, token refresh, registration backoff versus heartbeat-ticker retries, and Keycloak setup using the reference dcm-proxy client with audience-mapper guidance for custom clients.

Addresses the open documentation request from
environment-agent#35 (review thread on internal/config/config.go, dcm-project/environment-agent#35 (comment)): 'put agent auth enablement on the website... same idea as the CP guide.'

Content validated against the latest reviewed commits on both PR branches (config.go, jwt.go, middleware.go, token.go, client.go, main.go, openapi.yaml, README.md, decisions.md) rather than assumed from the PR descriptions alone.

Updates the control-plane Authentication guide to replace the obsolete 'still landing' callout and distinguish SP-to-agent API authentication from agent-to-control-plane registration and heartbeat authentication in troubleshooting. Cross-links the new guide from Local Setup and the Getting Started index.

Summary by Sourcery

Document the environment agent's separate inbound and outbound authentication surfaces and update related getting-started guidance.

New Features:

  • Add a dedicated guide explaining independent inbound environment-agent API authentication and outbound agent-to-control-plane authentication, including configuration, token behavior, Keycloak setup, and troubleshooting.

Enhancements:

  • Clarify control-plane authentication documentation by distinguishing service-provider-to-agent authentication from agent-to-control-plane authentication.
  • Cross-link environment-agent authentication guidance from the Getting Started index, control-plane authentication guide, and local setup documentation.

Documentation:

  • Document environment-agent JWT validation, health endpoint behavior, caller requirements, error responses, and audience validation.
  • Document outbound static-token and OAuth2 client-credentials modes, precedence, refresh and retry behavior, Kubernetes secret handling, and production authentication considerations.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Document inbound and outbound environment agent authentication

📝 Documentation 🕐 10-20 Minutes

Grey Divider

AI Description

• Add a guide for securing the agent API and authenticating agent-to-control-plane requests.
• Explain configuration, token behavior, Keycloak setup, and troubleshooting for both independent
 flows.
• Link the guide from getting-started, local-setup, and control-plane authentication pages.
Diagram

sequenceDiagram
    participant SP as Service Provider
    participant IdP as OIDC Provider
    participant Agent as Environment Agent
    participant CP as Control Plane
    SP->>IdP: Obtain SP token
    IdP-->>SP: JWT
    SP->>Agent: Provider API with JWT
    Agent->>IdP: Retrieve signing keys
    Agent-->>SP: API response
    Agent->>IdP: Obtain agent token
    IdP-->>Agent: JWT
    Agent->>CP: Register and heartbeat with JWT
Loading
High-Level Assessment

Keep the dedicated agent guide. Folding both flows into the control-plane authentication page would obscure their separate settings and troubleshooting paths; cross-links provide the necessary context without conflating them.

Files changed (4) +242 / -17

Documentation (4) +242 / -17
_index.mdAdd agent authentication to the getting-started index +2/-0

Add agent authentication to the getting-started index

• Adds a navigation entry describing the new guide's inbound and outbound scope.

content/docs/getting-started/_index.md

authentication.mdConnect control-plane guidance to agent authentication +16/-17

Connect control-plane guidance to agent authentication

• Replaces the provisional service-provider authentication note with a link to the new guide. Updates troubleshooting to distinguish SP-to-agent failures from agent-to-control-plane failures.

content/docs/getting-started/authentication.md

environment-agent-authentication.mdDocument both environment agent authentication surfaces +219/-0

Document both environment agent authentication surfaces

• Explains inbound JWT protection and outbound client-credentials, static-token, and unauthenticated modes. Covers configuration, audience behavior, token failures, Keycloak setup, secret injection, and troubleshooting.

content/docs/getting-started/environment-agent-authentication.md

local-setup.mdClarify authentication for local agent deployments +5/-0

Clarify authentication for local agent deployments

• Adds a note distinguishing control-plane API authentication from the agent's independent inbound and outbound authentication, with a link to the new guide.

content/docs/getting-started/local-setup.md

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="content/docs/getting-started/environment-agent-authentication.md" line_range="183" />
<code_context>
 auth is enabled.

 > **Service providers:** Service providers reach the control plane through the
-> environment agent, not direct HTTP calls. End-to-end authentication for the
</code_context>
<issue_to_address>
**Agent credential enables impersonation**

When an operator follows the reference setup and an attacker gains access to the agent's environment, the agent's `DCM_AUTH_CLIENT_SECRET` is also `AUTH_PROXY_SECRET`, so an attacker who extracts the agent environment can send forged `X-Forwarded-User` and `X-Forwarded-Preferred-Username` headers to the reachable control plane and be resolved as another actor, including a privileged one.

Recommend a dedicated OAuth client secret for the agent and keep it distinct from `AUTH_PROXY_SECRET`.
</issue_to_address>

### Comment 2
<location path="content/docs/getting-started/environment-agent-authentication.md" line_range="79-80" />
<code_context>
+> clients for service accounts (used by SPs registering with the agent) don't
+> ship with an audience mapper out of the box. Rather than reject every token
+> from an unmapped client, the agent skips the audience check when
+> `AGENT_AUTH_JWT_AUDIENCE` is empty, and only enforces `aud` matching when you
+> explicitly set it. Set it once your SP clients have a matching audience
+> mapper.
+
</code_context>
<issue_to_address>
**Unrelated realm tokens reach agent API**

When `AGENT_AUTH_JWT_AUDIENCE` is left at its documented empty default and a caller has a valid token for another client in the issuer realm, the agent validates the token's issuer but skips audience validation, so a bearer token issued for an unrelated client in the same realm can call the provider registration and listing APIs, exposing them to callers who were not issued agent tokens.

Recommend configuring a dedicated audience and matching SP client mappers before enabling inbound authentication in production, and treat an unset audience as development-only.
</issue_to_address>

### Comment 3
<location path="content/docs/getting-started/environment-agent-authentication.md" line_range="40" />
<code_context>
+as the only interim mitigation.
+
+`GET /api/v1alpha1/health` is the only endpoint that stays public — the bypass
+checks both the method and the path, so `GET /health` is unauthenticated but any
+other method at that path still requires a token.
+
</code_context>
<issue_to_address>
**Health probes miss endpoint**

When a reader follows the literal `/health` path when configuring or calling a health probe, the reader requests a path different from the documented public route `/api/v1alpha1/health`, so the health check is not reached.

Use `/api/v1alpha1/health` in the bypass sentence, or explicitly identify `/health` as shorthand or an internal router path.
</issue_to_address>

Sourcery assessment

Approval pending. 3 findings to address first.

Blocking findings: content/docs/getting-started/environment-agent-authentication.md:183, content/docs/getting-started/environment-agent-authentication.md:80, content/docs/getting-started/environment-agent-authentication.md:40


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@y-first y-first left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good, thorough coverage of both auth surfaces. A few issues to address before merge — see inline comments.

as the only interim mitigation.

`GET /api/v1alpha1/health` is the only endpoint that stays public — the bypass
checks both the method and the path, so `GET /health` is unauthenticated but any

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] Path inconsistency in health-bypass sentence

The sentence uses the bare path /health while the full endpoint /api/v1alpha1/health is used everywhere else in the file (lines 39, 110, 113). A reader could misread this as meaning only the short path is public or that there are two separate routes.

Suggest:

checks both the method and the path, so `GET /api/v1alpha1/health` is unauthenticated but any
other method at that path still requires a token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated: only GET /api/v1alpha1/health bypasses authentication; every other request, including other methods at that exact path, requires a token. This matches the middleware's exact method and path check (internal/auth/middleware.go).

`DCM_REGISTRATION_MAX_BACKOFF`). A failed heartbeat is logged and retried on
the next heartbeat tick, controlled by `AGENT_HEARTBEAT_INTERVAL` (30 seconds
by default); heartbeat failures do not use the registration backoff.
- **Static tokens do not refresh.** Once the token expires, the control plane

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM] Static token expiry also halts heartbeat — only registration is called out

This sentence says registration halts, but when the static token is expired the control plane returns 401 on both the registration call and every heartbeat call. A user keeping a long-lived agent running could miss that heartbeat silently stops succeeding too.

Suggest:

returns `401`, which the agent treats as non-retryable — registration and heartbeat both halt
until the agent is restarted with a fresh token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I checked the current registrar and client code. A 401 is non-retryable for registration, so registration retries stop. After a successful registration, though, the heartbeat ticker continues calling sendHeartbeat and logs each failure; the next tick tries again (internal/dcm/registrar.go, internal/dcm/client.go). I updated the guide to say heartbeat attempts continue but fail until restart with a fresh token. I did not use “heartbeat halts” because it does not match the implementation.

```bash
curl -s \
-H "Authorization: Bearer ${SP_TOKEN}" \
-H "Content-Type: application/json" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MEDIUM] Port 8081 appears without context here

A reader who hasn't set up the environment agent yet won't know where port 8081 comes from. This section doesn't explain how the agent is actually started (--with-environment-agent flag in deploy-dcm.sh), nor that 8081 is its default API port (overridable via --agent-port / AGENT_PORT, which matters when combining with a standalone KubeVirt SP that also uses 8081).

Suggest adding a note before the curl example, or a cross-reference sentence in the intro, e.g.:

The agent is started alongside the DCM stack via deploy-dcm.sh --with-environment-agent; see Local Setup. The default API port is 8081 (override with --agent-port / AGENT_PORT).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the agent launch flags, the required --agent-embedded-sps list, the 8081 default and overrides, and the port conflict with the standalone KubeVirt SP. The launch requirements and port behavior are verified against the utilities deployment script and its README.

| Symptom | Things to check |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SP registration returns `401` from the agent | Auth enabled on the agent (`AGENT_AUTH_DISABLED=false`); the SP's token is valid and not expired; issuer matches `AGENT_AUTH_ISSUER_URL`; if `AGENT_AUTH_JWT_AUDIENCE` is set, the token's `aud` must match. |
| Agent registration/heartbeat to the control plane returns `401` | Check the `DCM_AUTH_*` client credentials and token endpoint, then verify that the token issuer and (when enabled) audience match the control plane's `AUTH_*` settings. Add a `dcm-api` mapper for a custom client if required; the reference `dcm-proxy` client already has one. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[LOW] aud shorthand reads as jargon in end-user context

The rest of the guide (including this table's own row for AGENT_AUTH_JWT_AUDIENCE) uses the word "audience" consistently. Using aud bare here without expansion could confuse a reader who hasn't memorised JWT claim names.

Suggest: "informational unless you intended to enforce the aud claim".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Expanded the configuration row to “Expected token audience (the aud claim)” and changed the troubleshooting text to “token audience claim” so the JWT shorthand is introduced and explained.

### How it works

1. At startup, the agent discovers OIDC metadata from the issuer. When it
validates a token, it fetches signing keys from the issuer's JWKS endpoint as

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This says the agent fetches JWKS keys at startup. It fetches the OIDC config document at startup (which has the JWKS URL). The actual signing keys come later, on first token check. Maybe Discovers the issuer's OIDC configuration is more accurate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I tightened this sentence to distinguish the two steps: startup OIDC discovery reads issuer metadata, including the JWKS URL; signing keys are fetched on first token validation as needed and cached. This matches NewOIDCValidator in internal/auth/jwt.go.

as the only interim mitigation.

Only `GET /api/v1alpha1/health` stays public. The bypass checks both the method
and exact path, so `GET /health` and every other method or path require a token.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The first half of this sentence says the public path is /api/v1alpha1/health, but then the second half shortens it to /health. The agent has no /health route, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated to name the complete public route, GET /api/v1alpha1/health, and removed the /health shorthand. Only that method and exact path bypass authentication, matching internal/auth/middleware.go.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sourcery assessment

Approved.

@gciavarrini gciavarrini left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm

gabriel-farache and others added 3 commits October 19, 2026 09:56
Adds a dedicated Environment Agent Authentication guide covering the
two independent auth surfaces introduced by environment-agent#35 and
environment-agent#38:

- Inbound: AGENT_AUTH_DISABLED / AGENT_AUTH_ISSUER_URL /
  AGENT_AUTH_JWT_AUDIENCE protecting the agent's own REST API (external
  SP registration, provider listing), health-path bypass, RFC 7807
  error format, and the intentional audience fail-open behavior.
- Outbound: DCM_AUTH_TOKEN / DCM_AUTH_TOKEN_ENDPOINT /
  DCM_AUTH_CLIENT_ID / DCM_AUTH_CLIENT_SECRET for the agent's own
  registration/heartbeat calls to the control plane, mode precedence,
  token refresh and failure behavior, Keycloak service-account setup,
  and the known reference-realm audience-mapper gap.

Addresses the open documentation request from
environment-agent#35 (review thread on internal/config/config.go,
dcm-project/environment-agent#35 (comment)):
'put agent auth enablement on the website... same idea as the CP
guide.'

Content validated against the latest reviewed commits on both PR
branches (config.go, jwt.go, middleware.go, token.go, client.go,
main.go, openapi.yaml, README.md, decisions.md) rather than assumed
from the PR descriptions alone.

Updates the control-plane Authentication guide's Service-providers
callout and troubleshooting row to link to the new page instead of a
vague 'still landing' note, and cross-links from Local Setup and the
Getting Started index.

Signed-off-by: Gabriel Farache <gfarache@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: gabriel-farache <gfarache@redhat.com>
Signed-off-by: gabriel-farache <gfarache@redhat.com>
Signed-off-by: gabriel-farache <gfarache@redhat.com>
@gabriel-farache
gabriel-farache merged commit 8f8ff39 into dcm-project:main Oct 19, 2026
4 checks passed
@gabriel-farache
gabriel-farache deleted the docs/agent_auth branch October 19, 2026 08:00
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.

3 participants