Skip to content

feat: Add spec.auth to AgentRuntime for SPIFFE token exchange - #510

Open
Alan-Cha wants to merge 6 commits into
mainfrom
feat/agentruntime-auth-config
Open

feat: Add spec.auth to AgentRuntime for SPIFFE token exchange#510
Alan-Cha wants to merge 6 commits into
mainfrom
feat/agentruntime-auth-config

Conversation

@Alan-Cha

Copy link
Copy Markdown
Member

Summary

Implements issue #2174 by adding spec.auth field to the AgentRuntime CRD and operator logic to generate AuthBridge token-exchange routes from spec.auth.outbound configuration.

Changes

CRD Extension

  • Added spec.auth field with:
    • mode: Authentication mechanism (federated-jwt, client-secret, disabled)
    • outbound: List of routes defining destination matching and audiences for token exchange
  • Generated CRD manifest with OpenAPI validation

Operator Logic

  • Modified webhook to fetch AgentRuntime CR for each workload
  • Updated ensurePerAgentConfigMap to accept AgentRuntime parameter
  • Added route generation logic that reads spec.auth.outbound and injects routes into pipeline.outbound.plugins[token-exchange].config.routes
  • Routes include destination matching (host or hostRegex) and SPIFFE audience list

Tests

  • Added pod_mutator_auth_test.go with 2 new tests:
    • TestEnsurePerAgentConfigMap_WithAuthRoutes: Verifies route injection
    • TestEnsurePerAgentConfigMap_NoAuth: Verifies backward compatibility
  • Updated existing tests to pass nil for new agentRuntime parameter
  • All webhook tests pass ✅

Route Format

Generated routes are injected as:

routes:
  - destination:
      host: "weather-tool-mcp.team1.svc.cluster.local"
    audiences:
      - "spiffe://localtest.me/ns/team1/sa/weather-tool"

Backward Compatibility

  • AgentRuntime without spec.auth continues to work (no routes injected)
  • Nil AgentRuntime is handled gracefully (for workloads without CRs)

Testing

Unit tests verify:

  • Routes are correctly generated from spec.auth.outbound
  • Both host and hostRegex matching work
  • Multiple routes and multiple audiences per route work
  • Backward compatibility preserved

Closes #2174

Assisted-By: Claude Code

Alan-Cha and others added 3 commits August 10, 2026 21:45
The existing warning 'SPIRE not detected for mTLS-enabled workload' is
confusing because:

1. SPIRE may be running (via Helm) but not detected by this check
2. JWT-SVID authentication works fine despite this warning
3. The check only looks for socket volumes in the pod spec

This commit clarifies that:
- The check is specific to X.509-SVID based mTLS
- JWT-SVID authentication is unaffected
- The message explains what's actually being checked

Fixes rossoctl/rossoctl#2361

Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
…nge configuration

Add authentication configuration to AgentRuntime CRD to support per-agent
SPIFFE-based token exchange with scoped audiences.

New fields:
- spec.auth.mode: Authentication mechanism (federated-jwt, client-secret, disabled)
- spec.auth.outbound: List of routes specifying which destinations require
  token exchange and what audiences to request

This enables agents to declare which tools they call and what SPIFFE
audiences are needed, allowing the operator to generate proper AuthBridge
route configuration instead of relying on blanket all-to-all FGAP policies.

Example usage:
```yaml
apiVersion: agent.rossoctl.dev/v1alpha1
kind: AgentRuntime
metadata:
  name: weather-service
spec:
  type: agent
  auth:
    mode: federated-jwt
    outbound:
    - destination:
        host: weather-tool-mcp.team1.svc.cluster.local
      audiences:
        - spiffe://localtest.me/ns/team1/sa/weather-tool
```

Related to #2174 (Remove all provisioned credentials in favor of SPIFFE-based
Keycloak authentication)

Co-Authored-By: Claude Code <noreply@anthropic.com>
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
….outbound

Implement operator logic to read spec.auth.outbound from AgentRuntime CRs
and inject token-exchange routes into per-agent AuthBridge ConfigMaps. This
enables workloads to declaratively configure which audiences to request when
calling specific destinations via SPIFFE token exchange.

Changes:
- Fetch AgentRuntime CR in InjectAuthBridge webhook handler
- Pass AgentRuntime to ensurePerAgentConfigMap function
- Generate routes config from spec.auth.outbound when mode is federated-jwt
- Add unit tests for route generation (with and without spec.auth)

Routes are injected into pipeline.outbound.plugins[token-exchange].config.routes
with the following structure:
  routes:
    - destination:
        host: "service.namespace.svc.cluster.local"
      audiences: ["spiffe://trust-domain/ns/namespace/sa/serviceaccount"]

Relates-To: #2174
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
- Add tokenExchangePluginName constant for 'token-exchange' string
- Remove redundant nil check before len() (len returns 0 for nil slices)

Fixes CI linter errors from golangci-lint.

Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
Complete guide for using spec.auth field to configure SPIFFE-based
authentication and token exchange. Includes:

- Overview of authentication modes (federated-jwt, client-secret, disabled)
- Route configuration examples (exact host, regex, multiple audiences)
- How the operator generates AuthBridge routes
- Complete working example with agent and tool
- Troubleshooting guide
- Best practices

Assists-By: Claude Code
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>

@cwiklik cwiklik 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.

Adds spec.auth to AgentRuntime and generates AuthBridge token-exchange routes into the Envoy outbound pipeline (#2174). The route-gen is nicely defensive — nil-checked navigation of the untyped pipeline with WARN-and-skip at each level, gated to mode: federated-jwt, and a graceful missing-CR fallback (pre-CRD workloads get no routes, existing behavior preserved). mode is Enum-validated with a client-secret default, audiences is MinItems=1, and both generated CRD copies (charts/ and config/crd/bases/) are byte-identical.

Three hardening notes in the credential-minting path (all non-blocking — exploitability is gated by who can create AgentRuntime CRs, and the worst case is scope-broadening for the author's own audiences, not arbitrary escalation):

  1. RouteMatch allows neither/both host+hostRegex; an empty match emits destination: {} into the token-exchange config (silent catch-all/no-op). Recommend a CEL XValidation requiring exactly one, plus a defensive skip+WARN.
  2. hostRegex is unbounded — bound it (maxLength) given ReDoS + credential-scope-broadening.
  3. pluginConfig["routes"] overwrite — confirm spec.auth is the intended source of truth.

15 checks green; E2E pending (maintainer trigger). All commits signed, no .claude/.vscode. LGTM.

Assisted-By: Claude Code

}

// RouteMatch defines how to match an outbound destination.
type RouteMatch struct {

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.

suggestion: RouteMatch has both host and hostRegex as +optional with no "exactly one" constraint. An empty RouteMatch is accepted, and route-gen in pod_mutator.go then emits destination: {} into the token-exchange config — a silent catch-all/no-op in a credential-minting path (tokens exchanged for the listed audiences on all outbound calls, or none, depending on AuthBridge's empty-match semantics). Recommend a +kubebuilder:validation:XValidation (CEL) requiring exactly one of host/hostRegex, and defensively skip+WARN on an empty destination in route-gen. A test for the empty/both cases would lock it in.

// Example: ".*\\.team1\\.svc\\.cluster\\.local"
//
// +optional
HostRegex string `json:"hostRegex,omitempty"`

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.

suggestion: hostRegex is an unbounded, arbitrary regex evaluated on the request path. Risks: ReDoS from a pathological pattern, and an over-broad pattern (.*) silently broadening which destinations get token-exchange for the configured audiences. Consider a maxLength bound and documenting that the pattern scopes credential exchange (broad regex = broad credential scope).

routes = append(routes, route)
}

pluginConfig["routes"] = routes

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.

nit: pluginConfig["routes"] = routes replaces any pre-existing routes key wholesale — if the namespace template ever ships default outbound routes, they'd be clobbered by the spec.auth-derived set. Assuming spec.auth is intended as the source of truth here, a one-line comment saying so would help future readers.

BREAKING CHANGE: Removed spec.auth.mode field from AgentRuntime CRD

The authentication mode (federated-jwt vs client-secret) is configured
globally at the namespace level via authBridge.clientAuthType, not
per-agent. Having a per-agent mode field was misleading and could
create conflicts.

Changes:
- Removed AuthConfig.Mode field
- Updated webhook to inject routes when spec.auth.outbound is present,
  regardless of mode
- Routes are only effective when namespace has federated-jwt enabled
- Regenerated CRD manifests
- Updated tests to remove mode field
- Removed separate documentation (will be added to existing auth guide)

The authentication mode is a platform-wide setting that applies to all
agents in a namespace. Individual agents configure only their outbound
routes via spec.auth.outbound.

Assisted-By: Claude Code
Signed-off-by: Alan Cha <Alan.cha1@ibm.com>
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.

2 participants