Skip to content

fix(onboarding): require jwt-validation inbound and warn on login-client tool audiences - #234

Closed
oblinder wants to merge 6 commits into
mainfrom
fix/inbound-auth-checks
Closed

oblinder wants to merge 6 commits into
mainfrom
fix/inbound-auth-checks

Conversation

@oblinder

@oblinder oblinder commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two onboarding checks that close gaps in how a managed service's inbound is protected, on both enforcement sides.

1. PRD D30 check #2 requires an enforcing jwt-validation inbound, and tools are checked under agent side.

 check_preconditions(service, side)
-  if side is agent-side and service is a tool: skip every check
+  #1 sidecar        — every service (also a tool under agent side)
+  #2 pipeline
+    inbound:  jwt-validation            — every service, both sides
+              + opa                     — an agent; a tool under target side
+              + mcp-parser              — a tool under target side
+    outbound: mcp-parser + opa          — an agent under agent side
+    a required plugin with on_error: off | observe fails (it does not enforce)
+  #6 probes         — not for a tool under agent side (_probes_apply)

Why: under agent side a tool gets a pass-through CR, so jwt-validation (signature, issuer, aud = the tool's own client ID) is the only thing in front of it. Without it the tool is fully open. Under target side a missing jwt-validation already fails closed (no identity, PRD D27), but it is now caught at onboarding. The failure semantics do not change: EnforcementPreconditionError before the bootstrap, 409 with failed_checks, DLQ for NATS.

2. A warning when a platform login client can mint tokens for a tool (agent side only).

sequenceDiagram
    participant C as Controller
    participant I as IdP Configuration Service
    participant K as Keycloak
    C->>I: GET /platform-clients/tool-audiences?client_id=TOOL_ID
    I->>K: read the login clients in PLATFORM_SOURCE_CLIENTS, their scopes and mappers
    I-->>C: items of login_client, scope, link, mapper, tool_client_id
    Note over C: one WARNING per item, under agent side only, never raises
Loading

The operator attaches each workload's agent-<ns>-<workload>-aud scope to every client in PLATFORM_CLIENT_IDS as a default scope. Then that login client's user tokens pass the tool's jwt-validation and, under agent side, reach the tool through its pass-through CR with no AIAC check. The match is by the oidc-audience-mapper config (included.client.audience / included.custom.audience), not by the scope name. It runs after a tool's onboarding build and at the Controller start after the resync. Under target side there is no warning: the tool's inbound has no platform-client bypass, so such a token is denied there.

src/aiac/
├── agent/uc/onboarding/preconditions.py      # C1: per-side, per-type plugin rules; on_error
├── agent/uc/onboarding/platform_audience.py  # new: warn_platform_tool_audiences
├── agent/uc/onboarding/orchestrator.py       # call after the build (tools)
├── agent/controller/start.py                 # call after the resync
├── idp/service/configuration/keycloak/main.py  # new endpoint
└── idp/configuration/{api,models}.py         # get_platform_tool_audiences, PlatformToolAudience

Docs: PRD D30, known limits, UC-1a flow, §9; the uc1, aiac-agent, idp-configuration-service and library-idp specs; the runbook, deploy.md, the k8s comments, the github-tool demo spec.

Evidence

  • Before (origin/main code, the new tests of test_preconditions.py):
    FAILED TestCheck2Pipeline::test_every_service_needs_jwt_validation_in_the_inbound_pipeline[agent-side-tool-inbound3]
    FAILED TestCheck2Pipeline::test_a_required_plugin_that_does_not_enforce_fails[agent-side-tool-inbound2-observe]
    FAILED TestAgentSideScope::test_a_tool_without_jwt_validation_fails_2[('mcp-parser', 'opa')]
    FAILED TestAgentSideScope::test_a_tool_with_no_sidecar_fails_1
    17 failed
    
    After:
    17 passed
    unit lane: 2147 passed (was 2077 on origin/main)
    ruff, ruff-format, pyright (changed files): clean
    
  • C2 has about 40 new unit tests: the endpoint (default / optional / client-mapper links, match by mapper not by name, access.token.claim per Keycloak, one realm scope read, 502 on a Keycloak error, read-only), the library method, and the helper (agent side only, one warning per link, a failure never raises, no line breaks in log values).
  • System lane (-m system, 102 tests collected): not run on either side. The deploy of this PR's images to the Kind cluster was blocked, so the cluster still runs an older AIAC build. A run against that build would not test this PR. The Kind overlay and the chart default pipeline already have jwt-validation (map form, default on_error), so a valid setup passes check Chore(deps): Bump python from 3.12-slim to 3.14-slim in /src/aiac/policy/model_store/service #2 unchanged.
    • Target side: command .venv/bin/pytest -m system -rs, not run. 0 passed, 0 failed, 0 skipped. C1/C2: not observed. The precondition for C1 is present: k8s/opa-kind-verify.sh passes, and the team1 inbound pipeline has jwt-validation (default on_error), mcp-parser and opa.
    • Agent side: command .venv/bin/pytest -m system -rs after a clean redeploy with AIAC_ENFORCEMENT_SIDE=agent-side, not run. 0 passed, 0 failed, 0 skipped. C1/C2: not observed. Expected C2 result on this cluster: no WARNING, because the operator's PLATFORM_CLIENT_IDS is not set, so no login client holds a tool audience.

Merge Danger

Door: two-way

Revert the commits to restore the old checks. Nothing is stored or migrated.

Blast Radius: onboarding

  • An onboarding whose namespace pipeline has no enforcing jwt-validation inbound now fails with 409, on both sides. So does a tool under agent side with no sidecar.
  • Under agent side, C2 logs one WARNING for each tool that a platform login client can reach. That happens when the operator's PLATFORM_CLIENT_IDS names the login client (empty by default). This is a log line only.
  • Known limits (in PRD §6): C2 does not read the realm default-default scopes or the operator's per-namespace PLATFORM_CLIENT_IDS; a tool under agent side is not required to have opa inbound, so in such a namespace PRD D20 cannot deny a tool that has no CR.

🤖 Generated with Claude Code

oblinder and others added 5 commits October 12, 2026 02:42
… agent side

D30 check #2 now needs the jwt-validation plugin in the inbound pipeline
of every managed service: it is the only plugin that validates the token
and sets input.identity. Under agent side the checks #1 and #2 now also
run for tools (a tool needs only jwt-validation inbound; its pass-through
CR needs no opa or mcp-parser), and #6 stays for agents only. The
outbound plugins are required for agents only. The failure semantics do
not change (409, DLQ, no rollback, no quarantine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…ence

Under agent side a tool gets a pass-through CR, so only AuthBridge
jwt-validation protects its inbound. The operator attaches each
workload's audience scope to the login clients in PLATFORM_CLIENT_IDS,
so their user tokens reach the tool with no AIAC check.

The IdP Configuration Service gets the read-only endpoint
GET /platform-clients/tool-audiences. For each PLATFORM_SOURCE_CLIENTS
login client it reads the default and optional client scopes (mappers
from one realm scope list) and the mappers on the client itself, and
lists each oidc-audience-mapper that puts a managed tool's clientId into
the access token. The IdP library gets get_platform_tool_audiences and
the PlatformToolAudience model.

The new helper warn_platform_tool_audiences logs one WARNING for each
link, under agent side only. It runs at a tool's onboarding after the
build and at each Controller start after the resync. It is best effort:
a failure is logged and never stops the onboarding or the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
…arning

Update the PRD (D30, D31, the known limits, the platform prerequisites
and the unit-test table), the UC1 onboarding spec (the scope table for
each side and the new warning section), the AIAC Agent start sequence,
the IdP Configuration Service endpoint and the IdP library method. Also
update the runbook, the deploy parameters, the k8s comments, the demo
tool page, the UC1 testing doc and the system-test hint text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
- Check #2 (D30) counts a required plugin only when its entry enforces:
  on_error off (cortex does not run it) or observe (cortex ignores its
  Reject) fails the check. A bare plugin name counts as enforce, as in
  cortex. An unquoted YAML `off` is read as off.
- Replace the leaked "C2" label of the login-client audience warning
  with "D30" (C2 already names the hand-disabled client limit).
- Docs: precise inbound plugin needs for each side and type (runbook,
  enable script, uc1), the D20 deny only where the inbound has opa, the
  agent-side known limit for a tool namespace with no opa, the warning
  step order in the UC-1a flow, the aiac-agent.md file tree, the PRD
  link anchor, login_client naming, and the PRD 9 library test row.
- Tests: on_error values, the bare-name form, the de-duplication of one
  mapper with both audience keys, and no line breaks in the read-failure
  warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
Pyright flagged the name from a map entry as possibly None. An entry
without a string name cannot be a required plugin, so check #2 skips it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
@oblinder
oblinder removed the request for review from omerboehm October 11, 2026 23:42
@oblinder
oblinder force-pushed the fix/inbound-auth-checks branch from 081f2c6 to e44c295 Compare October 11, 2026 23:42
The operator attaches a workload's audience scope to the clients in its
PLATFORM_CLIENT_IDS list, which is empty by default and not set on the
Kind setup. So a default install gets no login-client audience warning.
Correct the claim that the link is made for every tool by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
oblinder added a commit that referenced this pull request Oct 12, 2026
Bring in the D30 check #2 jwt-validation requirement (tools are now
checked under agent side) and the login-client audience warning (PR #234).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
oblinder added a commit that referenced this pull request Oct 12, 2026
Bring in the PR #234 doc fix: the operator links audience scopes only
for the clients in PLATFORM_CLIENT_IDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Oleg Blinder <oblinder@gmail.com>
@oblinder
oblinder requested review from omerboehm and removed request for omerboehm October 12, 2026 16:01
@oblinder
oblinder marked this pull request as draft October 13, 2026 08:33
@oblinder

Copy link
Copy Markdown
Contributor Author

Replaced by #240. #240 merges all six commits of this PR, and then removes the login-client audience warning (C2) together with the agent-side enforcement scheme, so C2 does not get to main. The jwt-validation check (C1) is kept.

@oblinder oblinder closed this Oct 19, 2026
@oblinder
oblinder deleted the fix/inbound-auth-checks branch October 19, 2026 18:57
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Oct 19, 2026
omerboehm added a commit that referenced this pull request Oct 20, 2026
refactor(policy): target-side access control only (replaces #234)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant