Skip to content

fix(api-server): recognize OIDC service tokens as service callers for gRPC authz#1452

Merged
mergify[bot] merged 2 commits intomainfrom
fix/grpc-service-account-caller-type
Apr 23, 2026
Merged

fix(api-server): recognize OIDC service tokens as service callers for gRPC authz#1452
mergify[bot] merged 2 commits intomainfrom
fix/grpc-service-account-caller-type

Conversation

@markturansky
Copy link
Copy Markdown
Contributor

@markturansky markturansky commented Apr 23, 2026

Summary

  • Runner pods get OIDC tokens (SSO JWTs) from the CP token server for gRPC auth. These tokens pass JWT verification but carry the OIDC client username (ocm-ams-service), not the session creator's username
  • WatchSessionMessages rejects the call with PERMISSION_DENIED because the JWT username doesn't match session.CreatedByUserId
  • Add GRPC_SERVICE_ACCOUNT env var: when the JWT's preferred_username matches this value, the pre-auth interceptor sets CallerTypeService, bypassing per-user ownership checks — same as static AMBIENT_API_TOKEN calls
  • The env var is sourced from ambient-api-server secret's clientId key (same OIDC client ID the CP uses), optional: true

Test plan

  • Merge and deploy to Stage (auto-deploy on push to main)
  • Delete existing session, create new one via acpctl start
  • Tail runner logs for 30s — verify no PERMISSION_DENIED errors on WatchSessionMessages
  • Verify CP logs show clean gRPC watch streams

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Service accounts can now authenticate via gRPC bearer tokens.
  • Chores

    • Updated authentication middleware to read and configure service account credentials from environment variables.

… gRPC authz

The control-plane gives runner pods OIDC tokens (SSO JWTs) for gRPC
auth. These tokens pass JWT verification but carry the OIDC client
username (e.g. ocm-ams-service), not the session creator's username.
WatchSessionMessages then rejects the call with PERMISSION_DENIED
because the JWT username doesn't match session.CreatedByUserId.

Add GRPC_SERVICE_ACCOUNT env var support: when the JWT username matches
this configured service account name, the pre-auth interceptor sets
CallerTypeService on the context, allowing the call to bypass the
per-user ownership check — same as static AMBIENT_API_TOKEN calls.

The env var is sourced from the ambient-api-server secret's clientId
key (same OIDC client ID the CP uses), with optional: true so
deployments without OIDC continue to work.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 23, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit ca8cb83
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/69eaa8db7531e900081a1ebf

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Bearer token middleware now reads an optional GRPC_SERVICE_ACCOUNT environment variable and passes it to gRPC interceptors. The interceptors extract JWT claims and conditionally set CallerTypeService when the username matches the configured service account.

Changes

Cohort / File(s) Summary
gRPC Middleware Enhancement
components/ambient-api-server/pkg/middleware/bearer_token.go, bearer_token_grpc.go
Added serviceAccountUsername parameter to unary and stream interceptor constructors. Middleware reads optional GRPC_SERVICE_ACCOUNT env var, logs it, and passes through to interceptors. Interceptors conditionally override context to CallerTypeService when extracted JWT username matches the service account.
Kubernetes Service Configuration
components/manifests/base/core/ambient-api-server-service.yml
Added GRPC_SERVICE_ACCOUNT environment variable to api-server container, sourced from ambient-api-server Secret's clientId key (optional).
🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format (fix scope with clear description) and accurately describes the main change: enabling gRPC service token recognition for authorization.
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.
Performance And Algorithmic Complexity ✅ Passed PR introduces only O(1) operations in gRPC interceptor hot paths with necessary O(n) JWT token parsing; no loops over external data, N+1 patterns, or unbounded caches.
Security And Secret Handling ✅ Passed JWT tokens are never logged; service account name is treated as non-sensitive config metadata; serviceAccountUsername is only used for equality comparison, not injection-prone operations; no sensitive data leaked in responses.
Kubernetes Resource Safety ✅ Passed PR adds optional environment variable referencing existing Secret with proper resource limits, security contexts, and no new child resources or RBAC changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/grpc-service-account-caller-type
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/grpc-service-account-caller-type

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/ambient-api-server/pkg/middleware/bearer_token.go (1)

25-38: ⚠️ Potential issue | 🟠 Major

OIDC service-account support is unintentionally gated by AMBIENT_API_TOKEN.

On Line 27-30, init() returns before reading GRPC_SERVICE_ACCOUNT, so when AMBIENT_API_TOKEN is unset the new gRPC service-account path never registers.

Suggested fix
 func init() {
 	token := os.Getenv(ambientAPITokenEnv)
-	if token == "" {
-		glog.Infof("Service token auth disabled: %s not set", ambientAPITokenEnv)
-		return
-	}
 	serviceAccount := os.Getenv(grpcServiceAccountEnv)
-	glog.Infof("Service token auth enabled via %s (gRPC only)", ambientAPITokenEnv)
+	if token == "" && serviceAccount == "" {
+		glog.Infof("Service token auth disabled: %s and %s not set", ambientAPITokenEnv, grpcServiceAccountEnv)
+		return
+	}
+	if token != "" {
+		glog.Infof("Service token auth enabled via %s (gRPC only)", ambientAPITokenEnv)
+	} else {
+		glog.Infof("Static service token disabled: %s not set", ambientAPITokenEnv)
+	}
 	if serviceAccount != "" {
 		glog.Infof("OIDC service account username: %s", serviceAccount)
 	}
 	pkgserver.RegisterPreAuthGRPCUnaryInterceptor(bearerTokenGRPCUnaryInterceptor(token, serviceAccount))
 	pkgserver.RegisterPreAuthGRPCStreamInterceptor(bearerTokenGRPCStreamInterceptor(token, serviceAccount))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-api-server/pkg/middleware/bearer_token.go` around lines 25
- 38, The init() currently returns when AMBIENT_API_TOKEN is empty, preventing
GRPC_SERVICE_ACCOUNT handling; change init() to always read
grpcServiceAccountEnv and register the appropriate gRPC pre-auth interceptors
for the service-account path even if ambientAPITokenEnv is unset: read
serviceAccount := os.Getenv(grpcServiceAccountEnv) before the token check (or
remove the early return), log its presence with glog.Infof("OIDC service account
username: %s", serviceAccount) when non-empty, and then conditionally call
pkgserver.RegisterPreAuthGRPCUnaryInterceptor and
pkgserver.RegisterPreAuthGRPCStreamInterceptor for the service-account flow
(using bearerTokenGRPCUnaryInterceptor/bearerTokenGRPCStreamInterceptor or a
service-account-specific interceptor) when serviceAccount != "", while still
registering the token-based interceptors only when token != "".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/manifests/base/core/ambient-api-server-service.yml`:
- Around line 107-112: The base manifest sets the GRPC_SERVICE_ACCOUNT env var
from secretKeyRef (name: ambient-api-server, key: clientId) with optional: true,
which leaves GRPC_SERVICE_ACCOUNT empty by default and prevents the OIDC
service-caller mapping from activating; remove the optional: true from the
GRPC_SERVICE_ACCOUNT secretKeyRef in the base manifest so the env var must be
present by default, and if you need optional behavior for specific environments,
add optional: true back only in the overlay manifests that require it.

---

Outside diff comments:
In `@components/ambient-api-server/pkg/middleware/bearer_token.go`:
- Around line 25-38: The init() currently returns when AMBIENT_API_TOKEN is
empty, preventing GRPC_SERVICE_ACCOUNT handling; change init() to always read
grpcServiceAccountEnv and register the appropriate gRPC pre-auth interceptors
for the service-account path even if ambientAPITokenEnv is unset: read
serviceAccount := os.Getenv(grpcServiceAccountEnv) before the token check (or
remove the early return), log its presence with glog.Infof("OIDC service account
username: %s", serviceAccount) when non-empty, and then conditionally call
pkgserver.RegisterPreAuthGRPCUnaryInterceptor and
pkgserver.RegisterPreAuthGRPCStreamInterceptor for the service-account flow
(using bearerTokenGRPCUnaryInterceptor/bearerTokenGRPCStreamInterceptor or a
service-account-specific interceptor) when serviceAccount != "", while still
registering the token-based interceptors only when token != "".
🪄 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: 9c2d3057-fec3-4102-ac56-c9fdea1dd379

📥 Commits

Reviewing files that changed from the base of the PR and between 725c995 and ca8cb83.

📒 Files selected for processing (3)
  • components/ambient-api-server/pkg/middleware/bearer_token.go
  • components/ambient-api-server/pkg/middleware/bearer_token_grpc.go
  • components/manifests/base/core/ambient-api-server-service.yml

Comment on lines +107 to +112
- name: GRPC_SERVICE_ACCOUNT
valueFrom:
secretKeyRef:
name: ambient-api-server
key: clientId
optional: true
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.

⚠️ Potential issue | 🟠 Major

GRPC_SERVICE_ACCOUNT is silently disabled in base manifests.

optional: true on Line 112 combined with the base ambient-api-server Secret lacking clientId means this env var is empty by default, so the new OIDC service-caller mapping won’t activate.

Suggested fix
-                  optional: true
+                  optional: false

If you need optional behavior in some environments, keep this optional only in those overlays, not in base.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/manifests/base/core/ambient-api-server-service.yml` around lines
107 - 112, The base manifest sets the GRPC_SERVICE_ACCOUNT env var from
secretKeyRef (name: ambient-api-server, key: clientId) with optional: true,
which leaves GRPC_SERVICE_ACCOUNT empty by default and prevents the OIDC
service-caller mapping from activating; remove the optional: true from the
GRPC_SERVICE_ACCOUNT secretKeyRef in the base manifest so the env var must be
present by default, and if you need optional behavior for specific environments,
add optional: true back only in the overlay manifests that require it.

@mergify mergify Bot added the queued label Apr 23, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 23, 2026

Merge Queue Status

  • Entered queue2026-04-23 23:29 UTC · Rule: default
  • Checks skipped · PR is already up-to-date
  • Merged2026-04-23 23:30 UTC · at ca8cb83ca49292a1b63cf91767ddc94713f93952 · squash

This pull request spent 10 seconds in the queue, including 1 second running CI.

Required conditions to merge

@mergify mergify Bot merged commit 17d66f1 into main Apr 23, 2026
71 checks passed
@mergify mergify Bot deleted the fix/grpc-service-account-caller-type branch April 23, 2026 23:30
@mergify mergify Bot removed the queued label Apr 23, 2026
mergify Bot pushed a commit that referenced this pull request Apr 24, 2026
…E_ACCOUNT is set (#1455)

## Summary

Bug in #1452: the `init()` guard requires `AMBIENT_API_TOKEN` to be
non-empty before registering gRPC pre-auth interceptors. On Stage, only
`GRPC_SERVICE_ACCOUNT` is set — `AMBIENT_API_TOKEN` is not in the
api-server env vars — so interceptors are never registered and the OIDC
service caller logic is dead code.

## Evidence

Stage api-server env vars (no `AMBIENT_API_TOKEN`):
```
AMBIENT_ENV: production
GRPC_SERVICE_ACCOUNT: {secretKeyRef: {key: clientId, name: ambient-api-server}}
```

Stage api-server startup logs — no bearer token init messages,
confirming early return:
```
I0423 23:54:22.927314  Enabling JWT authentication middleware
# no "Service token auth enabled" or "OIDC service account" line
```

Runner pods still getting `PERMISSION_DENIED` on `WatchSessionMessages`
because the interceptor that would set `CallerTypeService` was never
registered.

## Fix

Change `init()` guard from `if token == ""` to `if token == "" &&
serviceAccount == ""` — register interceptors when either env var is
set.

## Test plan

- [ ] After merge + deploy, verify api-server logs show `OIDC service
account username: ocm-ams-service`
- [ ] Create session, tail runner logs — no `PERMISSION_DENIED` on
`WatchSessionMessages`

🤖 Generated with [Claude Code](https://claude.ai/code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Updated gRPC authentication middleware initialization to support
multiple authentication configuration options simultaneously. Bearer
token interceptors are now enabled based on the presence of either
authentication method, providing greater flexibility in authentication
setup.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: user <u@example.com>
Co-authored-by: Claude <noreply@anthropic.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.

1 participant