Skip to content

multi-stage: support projected SA token volumes with custom audiences#5093

Open
patjlm wants to merge 7 commits intoopenshift:mainfrom
patjlm:support-projected-sa-token-volumes
Open

multi-stage: support projected SA token volumes with custom audiences#5093
patjlm wants to merge 7 commits intoopenshift:mainfrom
patjlm:support-projected-sa-token-volumes

Conversation

@patjlm
Copy link
Copy Markdown

@patjlm patjlm commented Apr 9, 2026

Summary

Two features for multi-stage test steps:

  1. Projected SA token volumes with custom audiences — new service_account_tokens field on LiteralTestStep that mounts projected tokens with custom audiences into step containers. Enables WIF/Vault token exchange without serviceaccounts/token RBAC.

  2. Audience ownership validation — new --allowed-audiences-config flag for ci-operator-checkconfig that restricts which org/repo combinations can use specific audiences. Follows the existing cluster profile ownership pattern. Prevents unauthorized CI configs from using other teams' WIF audiences.

Motivation

We're migrating GCP HCP CI jobs from static service account JSON keys to Workload Identity Federation (WIF). WIF requires a token with a custom audience, but CI step pods don't have RBAC to call kubectl create token --audience=... (verified on build01). The standard Kubernetes solution is a projected volume with serviceAccountToken.audience — the kubelet handles the token request transparently.

The audience ownership validation ensures that only authorized repos can use a given WIF audience, preventing other teams' CI configs from authenticating to our GCP resources.

See openshift-online/gcp-hcp#38 and openshift-online/gcp-hcp#39 for context.

Changes

Commit 1: Projected SA token volumes

  • pkg/api/types.go: ServiceAccountTokenVolume type and ServiceAccountTokens field on LiteralTestStep
  • pkg/steps/multi_stage/gen.go: addServiceAccountTokenVolumes() injects projected volumes into step pods
  • pkg/validation/test.go: Validates audience/mount_path are non-empty, no duplicate mount paths, expiration >= 600s
  • Test + fixture

Commit 2: Audience ownership validation

  • pkg/api/types.go: AllowedAudienceDetails, AllowedAudienceOwners, AllowedAudiencesMap types
  • pkg/load/load.go: AllowedAudiencesConfig() loads config from YAML (missing file = empty, no restrictions)
  • pkg/validation/config.go: allowedAudiences field on Validator, WithAllowedAudiences option
  • pkg/validation/test.go: verifyAudienceOwnership(), metadata threading through context
  • cmd/ci-operator-checkconfig/main.go: --allowed-audiences-config flag (optional)

Example usage

Step config (openshift/release)

- as: e2e-gke
  service_account_tokens:
  - audience: gcp-hcp-ci-wif
    mount_path: /var/run/secrets/wif
    expiration_seconds: 3600

Audience ownership config (openshift/release)

# ci-operator/step-registry/allowed-audiences/allowed-audiences-config.yaml
- audience: gcp-hcp-ci-wif
  owners:
    - org: openshift
      repos:
        - hypershift

Test plan

  • TestGeneratePods/service_account_token_projection — verifies projected volumes
  • All existing TestGeneratePods subtests pass
  • All pkg/validation tests pass (including updated newContext callers)
  • go build ./pkg/... and go build ./cmd/ci-operator-checkconfig/ succeed
  • Verified on build01 that step pods cannot call kubectl create token (no RBAC)
  • Verified projected volumes work without RBAC (kubelet handles it)

🤖 Generated with Claude Code

@openshift-merge-bot
Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: automatic mode

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

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

Walkthrough

Adds projected ServiceAccount token support: new API types for token volumes and allowed-audiences mapping, pod generation to add projected token volumes/mounts, validation (including audience ownership checks using metadata), loader/CLI for allowed-audiences config, and tests/fixtures updates.

Changes

Cohort / File(s) Summary
API Types
pkg/api/types.go, pkg/api/zz_generated.deepcopy.go
Added ServiceAccountTokens []ServiceAccountTokenVolume to LiteralTestStep; introduced ServiceAccountTokenVolume, AllowedAudiencesMap, AllowedAudienceDetails, and AllowedAudienceOwners; updated deepcopy methods to handle new types/fields.
Pod Generation
pkg/steps/multi_stage/gen.go
Added addServiceAccountTokenVolumes() and called it from generatePods to create sa-token-* ProjectedVolumeSource entries and read-only mounts with audience and expiration handling (defaulting expiration to 3600s).
Tests & Fixtures
pkg/steps/multi_stage/gen_test.go, pkg/steps/multi_stage/testdata/zz_fixture_TestGeneratePods_service_account_token_projection.yaml
Added unit test "service account token projection" and a pod YAML fixture demonstrating two projected service account token volumes/mounts (one with explicit expirationSeconds).
Validation Logic
pkg/validation/test.go
Threaded metadata *api.Metadata into validation context; added validateServiceAccountTokens() enforcing non-empty audience/mount_path, absolute/non-nested/unique mount paths, expiration_seconds >= 600 if set, and verifyAudienceOwnership() to check allowed-audiences against metadata.
Validator Config & Tests
pkg/validation/config.go, pkg/validation/test_test.go
Added allowedAudiences api.AllowedAudiencesMap to Validator and WithAllowedAudiences(...) option; updated newContext call sites and tests to accept an additional metadata argument.
Loader & CLI
pkg/load/load.go, cmd/ci-operator-checkconfig/main.go
Added AllowedAudiencesConfig(configPath string) (api.AllowedAudiencesMap, error) loader (strict-unmarshal, missing file → empty map, duplicate/empty-audience checks); added --allowed-audiences-config flag and wired parsed data into validator via WithAllowedAudiences.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@openshift-ci openshift-ci Bot requested review from deepsm007 and hector-vido April 9, 2026 09:26
@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Apr 9, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: patjlm
Once this PR has been reviewed and has the lgtm label, please assign bear-redhat for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
pkg/validation/test.go (1)

946-965: Consider validating that mount_path is an absolute path.

The validation for credentials (line 815-816) enforces that mountPath must be an absolute path using filepath.IsAbs(). For consistency and to prevent potential issues with relative paths, the same check should be applied here.

♻️ Proposed fix to add absolute path validation
 		if token.MountPath == "" {
 			ret = append(ret, fmt.Errorf("%s.mount_path: must not be empty", fieldPath))
 		} else if mountPaths.Has(token.MountPath) {
 			ret = append(ret, fmt.Errorf("%s.mount_path: duplicate mount path %q", fieldPath, token.MountPath))
+		} else if !filepath.IsAbs(token.MountPath) {
+			ret = append(ret, fmt.Errorf("%s.mount_path: must be an absolute path", fieldPath))
 		} else {
 			mountPaths.Insert(token.MountPath)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/validation/test.go` around lines 946 - 965, In
validateServiceAccountTokens, add an absolute-path check for token.MountPath
(similar to the other credentials validation using filepath.IsAbs): after
confirming token.MountPath is non-empty and before/when inserting into
mountPaths, call filepath.IsAbs(token.MountPath) and if false append an error
like "%s.mount_path: must be an absolute path" to the returned errors; keep
existing duplicate/mountPaths logic intact and reference token.MountPath,
mountPaths, and filepath.IsAbs when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/validation/test.go`:
- Around line 946-965: In validateServiceAccountTokens, add an absolute-path
check for token.MountPath (similar to the other credentials validation using
filepath.IsAbs): after confirming token.MountPath is non-empty and before/when
inserting into mountPaths, call filepath.IsAbs(token.MountPath) and if false
append an error like "%s.mount_path: must be an absolute path" to the returned
errors; keep existing duplicate/mountPaths logic intact and reference
token.MountPath, mountPaths, and filepath.IsAbs when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef412c8e-8b7f-4f31-9512-2fcb501d4432

📥 Commits

Reviewing files that changed from the base of the PR and between 8be6f16 and 8a6a335.

📒 Files selected for processing (5)
  • pkg/api/types.go
  • pkg/steps/multi_stage/gen.go
  • pkg/steps/multi_stage/gen_test.go
  • pkg/steps/multi_stage/testdata/zz_fixture_TestGeneratePods_service_account_token_projection.yaml
  • pkg/validation/test.go

Copy link
Copy Markdown

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/load/load.go`:
- Around line 324-331: The YAML loader currently uses yaml.Unmarshal into
[]api.AllowedAudienceDetails and allows unknown fields and silent duplicate
audience entries; change it to use a yaml.Decoder with KnownFields(true) (or
equivalent strict unmarshalling) when decoding configContents into a slice of
api.AllowedAudienceDetails to reject unknown fields, and when building
allowedAudiencesMap check for existing keys and return an error if a duplicate
a.Audience is found instead of overwriting; reference symbols: yaml.Unmarshal,
api.AllowedAudienceDetails, audiencesList, allowedAudiencesMap, and the loop
that assigns allowedAudiencesMap[a.Audience] = a.

In `@pkg/validation/test.go`:
- Around line 950-968: The validateServiceAccountTokens function currently only
rejects empty or exact-duplicate mount paths; update it to apply the same
absolute-path and overlap validation used in validateCredentials: after
verifying token.MountPath is non-empty, ensure it is an absolute, cleaned path
(starts with "/" and normalized via path.Clean) and then check for parent/child
overlaps against existing mountPaths (not just exact duplicates) using the same
prefix/ancestor logic from validateCredentials; use mountPaths (and the same
representation used by validateCredentials) to detect overlaps and append
appropriate fmt.Errorf messages when a mount path is not absolute or overlaps an
existing mount.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4a95ce9-2562-4760-9911-8d1625818e98

📥 Commits

Reviewing files that changed from the base of the PR and between 8a6a335 and 3bdbb2c.

📒 Files selected for processing (6)
  • cmd/ci-operator-checkconfig/main.go
  • pkg/api/types.go
  • pkg/load/load.go
  • pkg/validation/config.go
  • pkg/validation/test.go
  • pkg/validation/test_test.go

Comment thread pkg/load/load.go
Comment thread pkg/validation/test.go Outdated
Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
pkg/validation/test.go (1)

950-988: SA token mount paths are validated only against each other, not against credentials.

The overlap detection correctly prevents conflicts between service_account_tokens entries, but does not cross-validate against step.Credentials mount paths. Since both are mounted into the same container (per gen.go:661-665), overlapping paths could still cause runtime mount conflicts.

If this is intentional scope for this PR, consider adding cross-validation in a follow-up.

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

In `@pkg/validation/test.go` around lines 950 - 988, Service account token mount
paths in validateServiceAccountTokens are only checked against other
service_account_tokens and not against step.Credentials, allowing
cross-conflicts; extend validation to also compare each token.MountPath against
all credentials mount paths (the same way other tokens are checked) and report
errors using the same fieldPath formatting (e.g., "%s.mount_path: ...") when a
token path is equal to, under, or contains any credentials mount path from
step.Credentials; locate validateServiceAccountTokens and the credentials source
(step.Credentials in gen.go) and reuse the same filepath.Rel logic and
duplicate/under-path error messages to perform the cross-validation before
inserting into mountPaths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/validation/test.go`:
- Around line 992-995: The verifyAudienceOwnership function currently returns
nil when metadata (m) is nil or m.Org is empty which is inconsistent with
verifyClusterProfileOwnership; change verifyAudienceOwnership to reject
validation in that case by returning an error (matching the behavior/pattern
used in verifyClusterProfileOwnership) and update the error message to clearly
state metadata is required for ownership checks; if the permissive return was
intentional for registry reference flows, instead add a comment in
verifyAudienceOwnership explaining why nil/empty metadata should be allowed.

---

Nitpick comments:
In `@pkg/validation/test.go`:
- Around line 950-988: Service account token mount paths in
validateServiceAccountTokens are only checked against other
service_account_tokens and not against step.Credentials, allowing
cross-conflicts; extend validation to also compare each token.MountPath against
all credentials mount paths (the same way other tokens are checked) and report
errors using the same fieldPath formatting (e.g., "%s.mount_path: ...") when a
token path is equal to, under, or contains any credentials mount path from
step.Credentials; locate validateServiceAccountTokens and the credentials source
(step.Credentials in gen.go) and reuse the same filepath.Rel logic and
duplicate/under-path error messages to perform the cross-validation before
inserting into mountPaths.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16777245-7313-413b-b82a-1d580b155598

📥 Commits

Reviewing files that changed from the base of the PR and between 3013ef6 and 6329cba.

📒 Files selected for processing (1)
  • pkg/validation/test.go

Comment thread pkg/validation/test.go
patjlm and others added 4 commits April 9, 2026 18:21
Add a `service_account_tokens` field to LiteralTestStep that allows
step authors to mount projected service account tokens with custom
audiences into their test containers. The kubelet handles the token
request transparently — no additional RBAC is required.

This enables workloads that need to exchange tokens with external
identity providers (e.g., GCP Workload Identity Federation, Vault)
without requiring `create` permission on `serviceaccounts/token`.

Each entry specifies:
- audience: the intended recipient of the token
- mount_path: where the token file is mounted
- expiration_seconds: token TTL (default 3600, min 600)

Example usage in step config:
  service_account_tokens:
  - audience: gcp-hcp-ci-wif
    mount_path: /var/run/secrets/wif
    expiration_seconds: 3600

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add an allowed-audiences-config mechanism that restricts which
org/repo combinations can use specific service account token
audiences. This prevents unauthorized CI configs from using
audiences belonging to other teams' WIF pools.

Follows the existing cluster profile ownership pattern:
- Config file maps audiences to allowed org/repo owners
- ci-operator-checkconfig validates at PR time
- Unlisted audiences are unrestricted (permissive by default)
- Missing config file is treated as empty (no restrictions)

New flag: --allowed-audiences-config (optional)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Regenerate deepcopy functions for new types
- Use strict YAML unmarshal to catch typos
- Reject empty or duplicate audience entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…aths

Match the validation behavior of validateCredentials: reject relative
paths and detect parent/child mount path overlaps that would shadow
each other at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@patjlm patjlm force-pushed the support-projected-sa-token-volumes branch from 6329cba to a7c96e9 Compare April 9, 2026 16:25
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/steps/multi_stage/gen.go`:
- Line 252: When adding projected token volumes via
addServiceAccountTokenVolumes(step.ServiceAccountTokens, pod), ensure we do not
clear the job's service account when ServiceAccountTokens is non-empty: if
step.ServiceAccountTokens has entries, set pod.Spec.ServiceAccountName = s.name
(so tokens are minted for the intended SA) and also set
pod.Spec.AutomountServiceAccountToken = pointer to false to avoid legacy
automount; only clear ServiceAccountName in the no_kubeconfig branch when
ServiceAccountTokens is empty. Update the logic around
addServiceAccountTokenVolumes and the previous no_kubeconfig branch to follow
this conditional.

In `@pkg/validation/test.go`:
- Around line 950-988: The current validateServiceAccountTokens function only
checks mount-path collisions among service_account_tokens; extend
validateServiceAccountTokens to also accept the step credentials slice (e.g.,
add a credentials parameter) and for each token compare token.MountPath against
every credential.MountPath to reject exact matches and parent/child overlaps
using the same filepath.Rel + ".." logic used for tokens, and ensure you still
track mountPaths as before; finally update the call site that invokes
validateServiceAccountTokens to pass step.Credentials so the new credential
checks run.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0dbcf363-f75d-4247-b6da-d00d39085770

📥 Commits

Reviewing files that changed from the base of the PR and between 6329cba and a7c96e9.

📒 Files selected for processing (10)
  • cmd/ci-operator-checkconfig/main.go
  • pkg/api/types.go
  • pkg/api/zz_generated.deepcopy.go
  • pkg/load/load.go
  • pkg/steps/multi_stage/gen.go
  • pkg/steps/multi_stage/gen_test.go
  • pkg/steps/multi_stage/testdata/zz_fixture_TestGeneratePods_service_account_token_projection.yaml
  • pkg/validation/config.go
  • pkg/validation/test.go
  • pkg/validation/test_test.go
✅ Files skipped from review due to trivial changes (4)
  • pkg/validation/test_test.go
  • pkg/steps/multi_stage/gen_test.go
  • pkg/steps/multi_stage/testdata/zz_fixture_TestGeneratePods_service_account_token_projection.yaml
  • pkg/api/zz_generated.deepcopy.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/load/load.go

Comment thread pkg/steps/multi_stage/gen.go
Comment thread pkg/validation/test.go Outdated
@openshift-merge-bot
Copy link
Copy Markdown
Contributor

Scheduling tests matching the pipeline_run_if_changed or not excluded by pipeline_skip_if_only_changed parameters:
/test e2e

patjlm and others added 2 commits April 9, 2026 20:08
- Keep ServiceAccountName when ServiceAccountTokens is non-empty, even
  for no_kubeconfig steps, so tokens are minted for the correct SA
- Cross-validate SA token mount paths against credential mount paths
  to detect collisions and parent/child overlaps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@patjlm
Copy link
Copy Markdown
Author

patjlm commented Apr 9, 2026

/retest

@openshift-merge-bot
Copy link
Copy Markdown
Contributor

Scheduling tests matching the pipeline_run_if_changed or not excluded by pipeline_skip_if_only_changed parameters:
/test e2e

@openshift-ci
Copy link
Copy Markdown
Contributor

openshift-ci Bot commented Apr 9, 2026

@patjlm: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/breaking-changes 787192e link false /test breaking-changes

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

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