Skip to content

sec(iac/aws): require OIDC subject claim in the federation bundle generator - #1691

Merged
cristim merged 3 commits into
mainfrom
sec/1640-bundle-subject-claim
Aug 4, 2026
Merged

cristim merged 3 commits into
mainfrom
sec/1640-bundle-subject-claim

Conversation

@cristim

@cristim cristim commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Closes #1640

PR #1602 made OIDCSubjectClaim a required CloudFormation parameter with no
default, closing the subject-less AWS trust policy hole for the checked-in
template (iac/federation/aws-target/cloudformation/template.yaml). The
internal/iacfiles generator that produces the customer-facing bundle was
left untouched at the time — internal/ was owned by concurrent in-flight
branches — so three bundle formats still shipped a subject-less policy or a
broken deploy.

Reachability

Established this concretely rather than inheriting the issue's framing (this
backlog has had p0s whose threat model turned out to be narrower than
claimed). It is not narrower here — for target=aws, source=gcp (the
CLI bundle's most common shape), the pre-fix trust policy without :sub is:

{"StringEquals": {"accounts.google.com:aud": "sts.amazonaws.com"}}

accounts.google.com is GCP's own identity-token issuer, used broadly by any
GCP customer's service account requesting an ID token with a custom audience
(gcloud auth print-identity-token --audiences=sts.amazonaws.com is a
standard, documented GCP→AWS federation pattern, not something specific to
CUDly). Without :sub, this condition alone is satisfied by any GCP service
account anywhere that mints a token with that audience — an unauthenticated-
in-practice bypass, exactly as severe as the CloudFormation hole #1543
already established. For target=aws, source=azure, the bypass is scoped to
whichever Azure tenant ends up in OIDC_ISSUER_URL (the generic self-service
render leaves a <AZURE_TENANT_ID> placeholder for the operator to fill in),
matching the same shape the CloudFormation template had before #1543 — still
a real bypass for every identity in that tenant, not narrower.

Gaps closed

  1. aws-wif-cli.sh.tmpl (the exploitable one) — documented
    OIDC_SUBJECT_CLAIM as "Optional" and built a trust policy with only the
    :aud condition when it was unset. format=cli is a first-class
    user-selectable download that never goes through CloudFormation, so
    nothing else in the pipeline rejected this. Fixed by dropping the
    subject-less else branch entirely and validating OIDC_SUBJECT_CLAIM
    (non-empty, no whitespace/$/* — the same characters PR sec(iac/aws): require OIDC subject claim in aws-target CloudFormation #1602 rejects
    in the CloudFormation parameter, for the same IAM-policy-variable-expansion
    reason) before any aws CLI call, so a misconfigured run fails loud
    instead of leaving a partially-configured OIDC provider behind.
  2. aws-cfn-deploy.sh.tmpl / buildCFParamsJSON / aws-wif-cf-params.json.tmpl
    — never forwarded OIDCSubjectClaim, so the CFN deploy already failed at
    change-set creation with Parameters: [OIDCSubjectClaim] must have values
    once sec(iac/aws): require OIDC subject claim in aws-target CloudFormation #1602 made the parameter required. Fixed by threading a new
    federationIaCData.OIDCSubjectClaim field through all three.
  3. aws-wif.tfvars.tmpl — emitted oidc_subject_claim commented out and
    labelled "Optional", contradicting the Terraform module's required,
    no-default variable. Fixed: emitted uncommented, "Optional" label removed.

federationIaCData.OIDCSubjectClaim stays empty from every generic-bundle
builder (buildGenericIaCData) by design: unlike OIDCIssuerURL/
OIDCAudience, which are derivable from target/source alone, CUDly's
server has no generic way to know the calling workload's real subject claim.
Every artifact now requires it explicitly (fails loud / prompts / rejects
empty at deploy-or-run time) instead of defaulting to a
working-but-insecure empty value — the same "generic bundle, filled in by the
operator at apply time" pattern this codebase already uses for
OIDCSubjectClaim in the CloudFormation and Terraform paths.

The standalone scripts/generate-federation-iac.go mirror (explicitly
documented as needing to stay in sync with internal/iacfiles/templates/)
gains the same field plus an --oidc-subject-claim flag.

What I deliberately did NOT fix here (filed separately)

Verification

  • bash -n and shellcheck 0.11.0 on the rendered aws-wif-cli.sh.tmpl and
    aws-cfn-deploy.sh.tmpl (Go templates rendered with sample data first,
    since {{...}} isn't valid bash) — both exit 0, no findings.
  • New regression tests, each confirmed to fail on the pre-fix code and pass
    post-fix
    (reverted the relevant files to origin/main and re-ran):
    • internal/iacfiles/templates_test.go:
      TestAWSWIFCLI_SubjectClaimRequired — asserts the rendered CLI script
      has exactly one :sub condition (never zero), contains no subject-less
      StringEquals block, rejects an empty OIDC_SUBJECT_CLAIM before the
      first aws call, and rejects whitespace/$/*.
    • internal/api/handler_federation_test.go:
      TestGetFederationIaC_AWSWIF_SubjectClaimThreaded — asserts the CFN
      params JSON, CFN deploy script, and bundle tfvars all carry
      OIDCSubjectClaim/oidc_subject_claim explicitly and uncommented.
  • go build ./... and go vet ./internal/api/... ./internal/iacfiles/...
    clean; go test ./internal/api/... ./internal/iacfiles/... green.
  • gofmt -l clean on all touched Go files.
  • Full pre-commit hook suite passed (gofmt, go vet, cyclomatic complexity,
    gosec, Trivy, markdown lint, etc.) — no --no-verify.
  • known_issues/13_iac_aws_target.md updated: the OIDCSubjectClaim entry
    marked resolved with a description of the fix; a new entry added for the
    thumbprint gap (#1689) so it isn't silently dropped from the doc.

Summary by CodeRabbit

  • New Features

    • AWS federation infrastructure templates now require an OIDC subject claim where applicable.
    • The claim is consistently propagated across CLI, CloudFormation, Terraform, and generated deployment bundles.
    • Trust policies now always restrict access to the specified workload subject.
  • Bug Fixes

    • Added validation for missing, whitespace-containing, overlong, or unsafe claims before deployment.
    • Resolved inconsistent subject-claim handling across AWS federation artifacts.
  • Documentation

    • Updated generation guidance, examples, and issue tracking, including the remaining OIDC thumbprint availability issue.

@cristim cristim added triaged Item has been triaged priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens urgency/now Drop other things impact/all-users Affects every user effort/s Hours type/security Security finding labels Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

AWS federation IaC generation now validates and propagates OIDC subject claims across CLI, CloudFormation, Terraform, and standalone generator outputs. AWS WIF trust policies always include a subject condition.

Changes

AWS OIDC subject claim enforcement

Layer / File(s) Summary
Subject claim propagation across bundle formats
internal/api/handler_federation.go, internal/iacfiles/templates/aws-wif-cf-params.json.tmpl, internal/iacfiles/templates/aws-cfn-deploy.sh.tmpl, internal/iacfiles/templates/aws-wif.tfvars.tmpl, internal/api/handler_federation_test.go
Federation data now includes OIDCSubjectClaim. CloudFormation and Terraform artifacts emit the claim. Regression tests verify the generated values.
CLI validation and subject-restricted trust policy
internal/iacfiles/templates/aws-wif-cli.sh.tmpl, internal/iacfiles/templates_test.go
The CLI rejects missing, whitespace-containing, $, and * subject claims. Generated trust policies always include a :sub condition.
Standalone generator validation and wiring
scripts/generate-federation-iac.go, scripts/generate_federation_iac_test.go
The standalone generator validates claim applicability and safe characters before rendering. Integration tests cover invalid, valid, bundle, and cross-account cases.
Documentation and audit updates
internal/iacfiles/templates/README.md, known_issues/13_iac_aws_target.md
Documentation describes subject-claim requirements. The audit records the resolved propagation issue and a remaining thumbprint issue.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant FederationGenerator
  participant AWSWIFBundle
  participant AWSIAM
  Operator->>FederationGenerator: Provide --oidc-subject-claim
  FederationGenerator->>FederationGenerator: Validate claim and target/source combination
  FederationGenerator->>AWSWIFBundle: Render validated claim
  AWSWIFBundle->>AWSIAM: Create resources with :sub-restricted trust policy
Loading

Possibly related issues

  • LeanerCloud/CUDly#1690: Both changes add OIDCSubjectClaim to federation IaC template data and rendered AWS artifacts.

Possibly related PRs

  • LeanerCloud/CUDly#1602: Added related AWS OIDC subject-claim validation and unconditional :sub trust-policy enforcement.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: requiring an OIDC subject claim in the AWS federation bundle generator.
Linked Issues check ✅ Passed The changes address all coding objectives in issue #1640, including required claims, validation, propagation, fail-closed policies, and regression tests.
Out of Scope Changes check ✅ Passed The implementation, tests, documentation, and issue-status updates directly support the linked issue and stated pull request objectives.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.
✨ 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 sec/1640-bundle-subject-claim

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

@cristim

cristim commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 5 minutes.

@cristim
cristim force-pushed the sec/1640-bundle-subject-claim branch from 65fefaa to 1d9e85b Compare August 3, 2026 11:55

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
known_issues/13_iac_aws_target.md (1)

5-57: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the resolved #1640 record to known_issues/resolved/.

Lines 44-57 mark #1640 as resolved, but its record remains under known_issues/. Extract the resolved record into known_issues/resolved/ and retain the active #1689 finding in this document.

As per coding guidelines, “When a referenced GitHub issue closes, move its known-issue document to known_issues/resolved/ in the same PR; do not delete it.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@known_issues/13_iac_aws_target.md` around lines 5 - 57, Move the resolved
`#1640` record from this document into a separate document under
known_issues/resolved/, preserving its full content and resolution details.
Update this document to retain only the active `#1689` finding, without deleting
the original issue record.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/iacfiles/templates_test.go`:
- Around line 209-247: Extend TestAWSWIFCLI_SubjectClaimRequired to execute the
rendered Bash script for empty, whitespace, $, and * OIDC_SUBJECT_CLAIM values
using a temporary file and a mock aws executable. Assert each run exits non-zero
and the mock aws command is never invoked, while preserving the existing
rendered-text assertions.

In `@scripts/generate-federation-iac.go`:
- Around line 287-292: Update populateData and the standalone generator’s
--oidc-subject-claim handling to reject empty values and any whitespace, $, or *
characters before storing the input. Preserve the raw claim for JSON and
Terraform rendering, but provide a shell-escaped copy to the AWS CLI template,
following the existing pattern in handler_federation.go so embedded quotes and
backticks cannot execute commands.

---

Outside diff comments:
In `@known_issues/13_iac_aws_target.md`:
- Around line 5-57: Move the resolved `#1640` record from this document into a
separate document under known_issues/resolved/, preserving its full content and
resolution details. Update this document to retain only the active `#1689`
finding, without deleting the original issue record.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e790e147-7a70-42f5-b03f-0ad716101497

📥 Commits

Reviewing files that changed from the base of the PR and between cd4ee03 and 1d9e85b.

📒 Files selected for processing (9)
  • internal/api/handler_federation.go
  • internal/api/handler_federation_test.go
  • internal/iacfiles/templates/aws-cfn-deploy.sh.tmpl
  • internal/iacfiles/templates/aws-wif-cf-params.json.tmpl
  • internal/iacfiles/templates/aws-wif-cli.sh.tmpl
  • internal/iacfiles/templates/aws-wif.tfvars.tmpl
  • internal/iacfiles/templates_test.go
  • known_issues/13_iac_aws_target.md
  • scripts/generate-federation-iac.go

Comment thread internal/iacfiles/templates_test.go
Comment thread scripts/generate-federation-iac.go Outdated
cristim added 3 commits August 3, 2026 18:10
…erator

PR #1602 made OIDCSubjectClaim a required CloudFormation parameter with no
default, closing the subject-less AWS trust policy hole for the checked-in
template. The internal/iacfiles generator that produces the customer-facing
bundle was left untouched at the time (internal/ was owned by concurrent
in-flight branches), so three bundle formats still shipped a subject-less
policy or a broken deploy:

- aws-wif-cli.sh.tmpl documented OIDC_SUBJECT_CLAIM as "Optional" and built
  a trust policy with only the :aud condition when it was unset. This path
  never goes through CloudFormation and format=cli is a first-class
  user-selectable download, so it was the one exploitable gap: any identity
  the documented issuer (accounts.google.com) can mint could assume the role
  and place irreversible multi-year commitment purchases.
- aws-cfn-deploy.sh.tmpl and buildCFParamsJSON never forwarded
  OIDCSubjectClaim, so the CFN deploy failed at change-set creation with
  "Parameters: [OIDCSubjectClaim] must have values" once the parameter
  became required.
- aws-wif.tfvars.tmpl emitted oidc_subject_claim commented out and labelled
  "Optional", contradicting the Terraform module's required, no-default
  variable.

Fix: aws-wif-cli.sh.tmpl drops the subject-less else branch entirely and
validates OIDC_SUBJECT_CLAIM (non-empty, no whitespace/$/*) before making any
AWS call, mirroring the guard PR #1602 added to the CloudFormation parameter.
federationIaCData gains an OIDCSubjectClaim field threaded through
shellEscapeData, buildCFParamsJSON, aws-wif-cf-params.json.tmpl,
aws-cfn-deploy.sh.tmpl's --parameter-overrides, and aws-wif.tfvars.tmpl
(uncommented, "Optional" label removed). The standalone
scripts/generate-federation-iac.go mirror gains the same field plus an
--oidc-subject-claim flag.

CUDly's server has no generic way to know the calling workload's real
subject claim, unlike OIDCIssuerURL/OIDCAudience which are derivable from
target/source alone, so the value stays operator-supplied by design — every
artifact now requires it explicitly instead of defaulting to a
working-but-insecure empty value.

Closes #1640.
Follow-up to d4776cc — the known_issues entry was written before the PR
existed and used a placeholder.
The standalone federation IaC generator copied --oidc-subject-claim straight
onto iacData with no validation and no escaping. The value is then interpolated
verbatim into three different grammars the script renders:

  - Bash: aws-cfn-deploy.sh.tmpl builds a "OIDCSubjectClaim=<value>" double-quoted
    word passed to `aws cloudformation deploy`, so a command substitution or a
    quote executes when the operator runs the generated deploy-cfn.sh.
  - JSON: aws-wif-cf-params.json.tmpl, where a quote injects extra parameter keys
    into the CloudFormation parameters file (a second RoleName entry, for one).
  - HCL: aws-wif.tfvars.tmpl, where a quote breaks out of the attribute value.

Escaping is the wrong primary control here: no single escaping helper is correct
for all three grammars, and for the ${VAR:-<value>} interpolation contexts the
in-script guard runs after the assignment that embeds the value, so a command
substitution has already executed by the time it is inspected. Validate at the
Go boundary instead, before anything is rendered.

The check is a positive allowlist, ^[A-Za-z0-9][A-Za-z0-9._:/@=+-]*$ with a
255-byte cap. It is deliberately stricter than the ^[^\s*$]+$ AllowedPattern the
CloudFormation template and Terraform module enforce on the same value: those
validate a value that is already a typed parameter and only need to reject what
IAM mishandles, while this one validates a value about to become shell, JSON and
HCL source. The narrowing costs nothing here because awsOIDCIssuer emits only
accounts.google.com, login.microsoftonline.com, or "" for an unrecognised
source, so the subjects reachable through this script are a numeric string and a
UUID.

Also close the inverse fail-quiet case found while wiring this: the flag was only
read on the AWS arm, so passing it with --target gcp, or with the aws->aws
cross-account combination whose role is trusted by source account plus external
ID rather than by an OIDC :sub condition, discarded it silently. An operator who
typos --target would get an artifact pinning nothing while believing
otherwise. A subjectClaimMode enum now distinguishes required from
not-applicable, with the strict value as the zero value so an unset mode fails
closed, and populateData returns error rather than bool. --target is checked
against an allowlist before the claim is, so a typo there is still reported as a
bad --target rather than as an inapplicable flag.

internal/iacfiles/templates/README.md is updated to match: its four AWS-WIF
examples were copy-pasteable invocations that are now hard errors. It also spells
out that the flag not applying to a combination does not mean that bundle needs
no pinning, with a per-source table of the required variable each GCP target
emits (aws_role_name, oidc_subject, or source_service_account).

Tests, both confirmed to fail against the pre-fix code:

  - scripts/generate_federation_iac_test.go compiles the //go:build ignore script
    once and exercises it as a subprocess. 16 hostile values, the missing-value
    case, bundle mode, the four not-applicable combinations, and 6 positive
    controls. Ordering is proved directly by pointing --templates-dir at an empty
    directory: a render attempt would fail with "no such file", so the validation
    error instead is proof nothing was read.
  - internal/iacfiles/templates_test.go now executes the rendered aws-wif-cli.sh
    against a recording stub `aws` placed on the child's PATH, asserting both a
    non-zero exit and zero AWS invocations for every invalid OIDC_SUBJECT_CLAIM,
    plus a positive control that checks the accepted value actually reaches the
    trust policy's :sub condition. Against the pre-fix template this shows a role
    being created with a :aud-only condition, which is #1640 end to end.

Adjacent pre-existing gaps found and filed on #1690 rather than fixed here:
iacData is missing four fields the templates reference, shellEscape does not
escape '}', and two server-side render paths emit shell/HCL from unescaped data.

Refs #1640
@cristim
cristim force-pushed the sec/1640-bundle-subject-claim branch from 1d9e85b to 68a47c0 Compare August 3, 2026 16:11
@cristim

cristim commented Aug 3, 2026 •

Copy link
Copy Markdown
Member Author

Rebased onto current main, plus both CodeRabbit threads addressed

1. Rebase (fixes the red pre-commit check)

1d9e85bfa (old head) -> 68a47c00d (pushed head, rebased onto 02702a108). The failure was not in this branch's code: the run log shows

Unable to find image 'ghcr.io/hadolint/hadolint:latest'

which is the floating-tag problem #1697 fixed on main. The branch predated that fix, and re-running the job would not have helped because it reuses the same merge commit. main has the SHA-pinned ghcr.io/hadolint/hadolint:v2.15.1@sha256:32dac941... entry now.

The rebase replayed cleanly with no conflicts. Verified nothing was dropped rather than assuming it:

  • Same 9-file change set before and after (the CR fixes below then added two more files, so the PR now shows 11).
  • 8 of the 9 file blobs are byte-identical to their pre-rebase hashes.
  • The 9th, internal/iacfiles/templates_test.go, is the only file main also touched (via 34d85bc). Its content changed, as it must; what matters is that the branch's own delta is unchanged. Diffing the added/removed lines of git diff <old-base> <old-head> against git diff origin/main <new-head> for that file returns empty, and main's new TestGCPWIFTfvarsMarksPinnedIdentityRequired now sits alongside this branch's TestAWSWIFCLI_SubjectClaimRequired, both present.
  • Both test functions the branch adds (TestAWSWIFCLI_SubjectClaimRequired, TestGetFederationIaC_AWSWIF_SubjectClaimThreaded) still exist.

2. 🔴 scripts/generate-federation-iac.go — validate --oidc-subject-claim before rendering

Full response on the thread. Summary: the flag went onto iacData with no validation and no escaping, and this script interpolates it verbatim into three grammars, not one: Bash (aws-cfn-deploy.sh.tmpl builds a "OIDCSubjectClaim=<value>" word for aws cloudformation deploy), JSON (aws-wif-cf-params.json.tmpl) and HCL (aws-wif.tfvars.tmpl).

Escaping was rejected as the primary control because no one escaping helper is correct for all three. Concretely, escaping only the shell template would have left this working:

$ ... --format cf-params --oidc-subject-claim 'x", "ParameterKey": "RoleName", "ParameterValue": "AdminRole'
  { "ParameterKey": "OIDCSubjectClaim", "ParameterValue": "x", "ParameterKey": "RoleName", "ParameterValue": "AdminRole" },

a second RoleName injected into the CloudFormation parameters file with no shell involved. Validation at the Go boundary is one control that covers all three, and it is the only one that runs before the value becomes source text.

The check is a positive allowlist, ^[A-Za-z0-9][A-Za-z0-9._:/@=+-]*$ with a 255-byte cap, deliberately stricter than the ^[^\s*$]+$ the CFN template and TF module enforce on the same value. Those validate a typed parameter and only need to reject what IAM mishandles; this validates a value about to become shell, JSON and HCL source. The divergence and its cost are documented in the code.

Also closed the mirror-image bug found while wiring it: the flag was read only on the AWS arm, so passing it with --target gcp, or with the aws -> aws cross-account combination, discarded it silently. A typo'd --target would have produced an artifact pinning nothing while the operator believed otherwise. That is now an explicit error, via a subjectClaimMode enum whose zero value is the strict one. --target is validated against an allowlist first, so a typo there is still diagnosed as a bad --target.

internal/iacfiles/templates/README.md is updated to match: four of its copy-pasteable invocations were AWS-WIF examples that this change turns into hard errors. It is also explicit that "this flag does not apply here" is not "this bundle needs no pinning" — each GCP-target combination has its own required variable (aws_role_name, oidc_subject, or source_service_account depending on --source), enumerated in a table rather than generalised.

3. 🟡 internal/iacfiles/templates_test.go — actually execute the invalid-subject paths

Implemented as suggested. There was no stub-on-PATH harness in the repo, so one is added: render to a temp file, write a recording aws stub, set PATH on the child via exec.Cmd.Env (never the parent process), run under bash, assert non-zero exit and zero recorded aws invocations. Ten reject cases and a positive control that checks the accepted value actually reaches the trust policy's :sub condition.

Against origin/main's pre-fix template this fails and shows the original bug end to end: an unset OIDC_SUBJECT_CLAIM created a role whose only condition was {"StringEquals": {"accounts.google.com:aud": ""}}, with no :sub at all.

Regression proof

Both new test files were confirmed to fail against the pre-fix code, not merely to pass against the fixed code:

  • With the validateOIDCSubjectClaim call removed, all 5 generator guard tests FAIL and both positive controls still PASS.
  • With origin/main's aws-wif-cli.sh.tmpl swapped in, all 10 bash reject cases FAIL and the positive control still PASSes.

Gates

Command Exit
go build ./... 0
go vet ./... 0
go test -count=1 ./internal/iacfiles/... ./internal/api/... ./scripts/... 0 (2093 tests)
golangci-lint run --timeout=10m ./... (CI-pinned v2.10.1) 0, 0 issues.
gocyclo -over 10 $(git ls-files "*.go" | grep -v _test.go | grep -v vendor/) 0, no output
bash -n on all 18 rendered scripts 0
shellcheck -s bash on all 18 rendered scripts 0

populateData is at complexity 8, validateOIDCSubjectClaim at 6.

Out of scope, filed on LeanerCloud/cloud-commitments-platform#153

Three adjacent pre-existing gaps, all reproducible on origin/main and all deliberately left alone here: iacData is missing CUDlyAPIURL / ContactEmail / SourceAccountID / OIDCIssuerHost, so every .tfvars path of this script currently fails to render; shellEscape does not escape }, which matters for the ${VAR:-<value>} interpolation contexts; and two server-side render paths (azure-wif-bicep-deploy.sh.tmpl, the .auto.tfvars templates) emit shell and HCL from unescaped data. Details and reproductions in https://github.com/LeanerCloud/CUDly/issues/1690#issuecomment-5168468307.

Not merging; leaving that to a human as usual.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
internal/iacfiles/templates/README.md (1)

19-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add OIDCSubjectClaim to the template variables list.

This table documents every field the templates receive. scripts/generate-federation-iac.go adds OIDCSubjectClaim to iacData, and three templates (aws-cfn-deploy.sh.tmpl, aws-wif-cf-params.json.tmpl, aws-wif.tfvars.tmpl) now read it, but the table still omits it. A reader relying on this table to understand available template fields will miss the new one.

📝 Proposed fix
 OIDCIssuerURL       — OIDC issuer URL (AWS target only)
 OIDCAudience        — OIDC audience (AWS target only)
+OIDCSubjectClaim    — OIDC subject (sub) claim pinning the AWS trust policy (AWS target, non-AWS source only)
 SubscriptionID      — Azure subscription ID (azure target only)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/iacfiles/templates/README.md` around lines 19 - 35, Add
OIDCSubjectClaim to the Template variables list in the README, documenting its
purpose and applicable target/source scope consistently with the existing
OIDCIssuerURL and OIDCAudience entries.
🧹 Nitpick comments (2)
internal/iacfiles/templates_test.go (2)

478-522: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split this test file to stay under the 500-line limit.

The file now exceeds 500 lines. Move the AWS WIF execution harness (awsStubScript, runRenderedWIFScript) and its tests into a separate file in the same package, for example templates_aws_wif_test.go.

As per coding guidelines: "Follow Domain-Driven Design with bounded contexts, keep files under 500 lines, and use typed interfaces for public APIs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/iacfiles/templates_test.go` around lines 478 - 522, Split
internal/iacfiles/templates_test.go to keep it below 500 lines by moving the AWS
WIF execution harness symbols awsStubScript and runRenderedWIFScript, along with
their associated tests, into a new same-package test file such as
templates_aws_wif_test.go. Preserve the existing test behavior and shared
package-level helpers, leaving non-AWS template tests in templates_test.go.

Source: Coding guidelines


304-323: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fail the test when the context timeout kills the script.

If the 30-second timeout fires, cmd.Run returns an *exec.ExitError for the signal kill. errors.As matches it, so the helper returns exit code -1 with zero recorded AWS calls. Every reject case then passes, because it only requires a non-zero exit and no AWS call. A hanging script would report success.

Check ctx.Err() after the run and fail explicitly.

♻️ Proposed fix to detect timeout kills
 	var exitErr *exec.ExitError
 	if err := cmd.Run(); err != nil && !errors.As(err, &exitErr) {
 		t.Fatalf("run rendered script: %v (stderr: %s)", err, errBuf.String())
 	}
+	if ctx.Err() != nil {
+		t.Fatalf("rendered script did not finish before the timeout: %v (stderr: %s)", ctx.Err(), errBuf.String())
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/iacfiles/templates_test.go` around lines 304 - 323, Update the
command execution flow around cmd.Run to check ctx.Err() immediately after the
run completes, and fail the test explicitly when the context has been canceled
or timed out. Preserve the existing *exec.ExitError handling for ordinary
non-zero script exits, but do not allow a timeout-killed script to be treated as
a valid rejection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/iacfiles/templates/README.md`:
- Around line 19-35: Add OIDCSubjectClaim to the Template variables list in the
README, documenting its purpose and applicable target/source scope consistently
with the existing OIDCIssuerURL and OIDCAudience entries.

---

Nitpick comments:
In `@internal/iacfiles/templates_test.go`:
- Around line 478-522: Split internal/iacfiles/templates_test.go to keep it
below 500 lines by moving the AWS WIF execution harness symbols awsStubScript
and runRenderedWIFScript, along with their associated tests, into a new
same-package test file such as templates_aws_wif_test.go. Preserve the existing
test behavior and shared package-level helpers, leaving non-AWS template tests
in templates_test.go.
- Around line 304-323: Update the command execution flow around cmd.Run to check
ctx.Err() immediately after the run completes, and fail the test explicitly when
the context has been canceled or timed out. Preserve the existing
*exec.ExitError handling for ordinary non-zero script exits, but do not allow a
timeout-killed script to be treated as a valid rejection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47a335dc-35b6-40db-8699-73c23830f9da

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9e85b and 68a47c0.

📒 Files selected for processing (11)
  • internal/api/handler_federation.go
  • internal/api/handler_federation_test.go
  • internal/iacfiles/templates/README.md
  • internal/iacfiles/templates/aws-cfn-deploy.sh.tmpl
  • internal/iacfiles/templates/aws-wif-cf-params.json.tmpl
  • internal/iacfiles/templates/aws-wif-cli.sh.tmpl
  • internal/iacfiles/templates/aws-wif.tfvars.tmpl
  • internal/iacfiles/templates_test.go
  • known_issues/13_iac_aws_target.md
  • scripts/generate-federation-iac.go
  • scripts/generate_federation_iac_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/api/handler_federation.go
  • internal/iacfiles/templates/aws-wif-cf-params.json.tmpl
  • internal/iacfiles/templates/aws-cfn-deploy.sh.tmpl
  • known_issues/13_iac_aws_target.md
  • internal/iacfiles/templates/aws-wif-cli.sh.tmpl
  • internal/iacfiles/templates/aws-wif.tfvars.tmpl
  • internal/api/handler_federation_test.go

@cristim

cristim commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Adversarial review — round 6 (final): clean, ship it

Verdict: no actionable findings. This was the sixth and last round on this PR. Recording it here because the preceding five rounds materially changed the change and existed only in out-of-band messages — reviews that alter an outcome should leave a trace on the artifact.

What the six rounds found

Round Finding
1 Original defect: the CLI bundle could emit a subject-less trust policy. Fixed by adding validation.
2 Four findings, all in the newly added validation machinery — notably that four of eight documented copy-pasteable README invocations had become hard errors.
3 The round-2 reorder recreated the round-2 misdirection one layer down: a length error was reported where applicability was the real fault.
4 The round-3 wording overcorrected in all three places it was applied. Plus one genuine user-facing bug: a wrong filename in the README.
5 The round-4 replacement paragraph was wrong again — --target gcp --source gcp routes to gcp-sa-impersonation.tfvars.tmpl and never produces the file the paragraph named.
6 Clean.

Zero findings across all six rounds were in the original defect. Every one was in explanatory prose describing which --target/--source combination renders which artifact, or in machinery added to fix an earlier round.

The root cause was not the prose. It was the quantifier. Each attempt compressed three distinct combinations into one universal statement, and each correction fixed one combination while breaking another: round 3's "non-gcp source" correctly excluded gcp/gcp but wrongly lumped in the aws branch; round 5's "always" correctly split the aws branch but swept gcp/gcp back in. The fix that finally held was to remove the quantifier by enumerating all three rows explicitly, which is what the doc header now does.

Module-level verification

The templates label aws_role_name and oidc_subject REQUIRED. Rendering the templates confirms the label is printed; it does not confirm the label is true. That is a claim about the consuming Terraform module, so this round checked the module. Verified against origin/main:

  • iac/federation/gcp-target/terraform/variables.tf:77 — aws_role_name carries default = "".
  • iac/federation/gcp-target/terraform/variables.tf:96 — oidc_subject carries default = "".
  • Neither is required by its declaration. Both are required only via lifecycle { precondition } blocks at main.tf:179 and :183, which fail the apply when provider_type is aws / oidc respectively and the value is empty.
  • By contrast source_service_account (iac/federation/gcp-sa-impersonation/terraform/variables.tf) has no default, so it is the only one of the three required by Terraform itself.

The labels are correct; the mechanism is one level below where the templates could show it. Rendering the templates would not have surfaced this — the difference between checking the artifact and checking the thing the artifact asserts.

Quantifier sweep

Every universal or exclusive statement in the changed files was extracted and checked — 31 in total, including the CFN/TF AllowedPattern comparison, all eight README examples re-run against the built binary, and the four test doc comments. All now hold.

The one that had been false in rounds 3, 4 and 5 — "each GCP target combination emits its own required pin" — is true only because it is now backed by exhaustive three-row enumeration rather than a quantifier.

Divergence from the gcp-target module's own regex (intentional)

iac/federation/gcp-target/terraform/variables.tf:107 validates a subject with:

^[A-Za-z0-9][-A-Za-z0-9._:/@=+~|]*$

which deliberately permits | for Auth0/Okta-style subjects (google-oauth2|123). The generator's oidcSubjectClaimRE rejects |.

Both are correct, because the sinks differ. In the Terraform module the value lands in a single-quoted CEL string literal and an IAM principal identifier, where | is inert. In the generator the value is interpolated verbatim into three grammars — Bash (aws-cfn-deploy.sh.tmpl), JSON (aws-wif-cf-params.json.tmpl) and HCL (aws-wif.tfvars.tmpl) — where a shell metacharacter is not inert. A stricter allowlist at the wider sink is the right asymmetry, not an inconsistency.

Process note

No edits or commits were made by this reviewer at any point across the six rounds. All changes were authored separately; this role was verification only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/all-users Affects every user priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens triaged Item has been triaged type/security Security finding urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sec(iac): federation bundle generator still emits a subject-less AWS trust policy (CLI bundle exploitable)

1 participant