Skip to content

fix(security/onboarding): add External ID collection + trust-policy snippet to bastion auth mode (#129) - #157

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/bastion-external-id
Apr 28, 2026
Merged

fix(security/onboarding): add External ID collection + trust-policy snippet to bastion auth mode (#129)#157
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/bastion-external-id

Conversation

@cristim

@cristim cristim commented Apr 27, 2026

Copy link
Copy Markdown
Member

Summary

When the AWS account modal is in bastion auth mode, the frontend
never collected an aws_external_id and never rendered a trust-policy
snippet (only role_arn mode did). The user has confirmed this is an
oversight, not a deliberate scope decision — bastion mode also
calls sts:AssumeRole into the customer's role (just via the bastion's
STS client instead of ambient credentials), so the same sts:ExternalId
confused-deputy guard applies.

This PR builds on PR #146 (External ID hardening for role_arn
mode — #126/#127/#128/#130) and reuses every primitive it landed:

  • the secure crypto.getRandomValues generator (no parallel generator)
  • the cudly:aws-account-modal:draft-external-id sessionStorage draft
    key (shared across both modes — the value is fungible, so a single
    source of truth keeps the two readonly inputs in sync when an operator
    toggles auth modes mid-edit)
  • the validateAWSExternalID backend charset + length validator
  • the partition-aware ARN rendering (aws / aws-cn / aws-us-gov)
  • the #130a race short-circuit when auth mode changes mid-fetch

What changed

Frontend (frontend/src/index.html, frontend/src/settings.ts):

  • #account-aws-bastion-fields now hosts a readonly External ID input
    • Copy button + trust-policy <pre> block + Copy button, mirroring
      the role_arn block.
  • populateAwsAccountFields writes the same draft value to both
    account-aws-external-id and account-aws-external-id-bastion.
  • buildAccountRequest's bastion branch now collects
    aws_external_id (undefined when blank).
  • New renderAwsBastionTrustPolicy() mirrors renderAwsTrustPolicy
    but sets Principal to the bastion account root (looked up via
    listAccounts matched on the dropdown selection). Re-renders on
    auth-mode change and on bastion dropdown change.

Backend (internal/api/handler_accounts.go):

  • validateAWSAuthMode now also runs validateAWSExternalID for
    bastion mode when aws_role_arn is non-empty, parallel to role_arn.
  • workload_identity_federation stays exempt (OIDC verifies identity
    via the token subject claim — stscreds.WebIdentityRoleOptions has
    no ExternalID field), and so does access_keys (no AssumeRole at
    all). The empty-role-ARN exemption mirrors role_arn's self-account
    onboarding shortcut.

Resolver (internal/credentials/resolver.go): no change needed —
resolveBastionProvider already delegates to resolveRoleARNProvider,
which has always read account.AWSExternalID and threaded it into
stscreds.AssumeRoleOptions. The missing piece was frontend
collection + backend validation, not the resolver.

Tests

  • frontend/src/__tests__/html.test.ts — assert the bastion section
    has the new input, copy button, and trust-policy block.
  • frontend/src/__tests__/settings-external-id.test.ts — new suite
    (4 cases): bastion shows the same draft as role_arn; selecting a
    bastion renders a snippet with the bastion's account root as
    Principal; buildAccountRequest sends aws_external_id; toggling
    modes preserves the draft across close/reopen.
  • frontend/src/__tests__/settings-accounts.test.ts — DOM scaffold
    extended with the new bastion-mode fields.
  • internal/api/handler_accounts_external_id_test.go — bastion now
    enforced in dedicated cases (BastionRequiresExternalID,
    BastionHappyPath, BastionNoRoleArnExempt) and removed from the
    "no-validation-needed" loop.

Test plan

  • go build ./... clean
  • go test ./... — all packages pass
  • npx jest — 39 suites / 1325 tests pass (was 1308 on main; +17
    new cases here)
  • npx tsc --noEmit && npm run build — typecheck + production
    bundle clean
  • Pre-commit hooks pass: gofmt, go vet, gocyclo, git-secrets,
    gosec, trivy, frontend tests, frontend build
  • Deployed verification on the preview Lambda URL
    (https://33pz7pombdqwu3bdlxp4lqxyra0bsriy.lambda-url.us-east-1.on.aws/)
    — post-merge.

Out of scope

Closes #129.

…nippet to bastion auth mode

When the AWS account modal is in bastion auth mode, the frontend never
collected an aws_external_id and never rendered a trust-policy snippet
(only role_arn mode did). The user has confirmed this is an oversight,
not a deliberate scope decision: bastion mode also calls sts:AssumeRole
into the customer's role (just via the bastion's STS client instead of
ambient credentials), so the same sts:ExternalId confused-deputy guard
applies.

Frontend (frontend/src/index.html, frontend/src/settings.ts):
  - Add a readonly External ID input + copy button + IAM trust-policy
    <pre> block + Copy button inside #account-aws-bastion-fields,
    mirroring the role_arn block. The two External ID inputs share the
    same draft value (resolved via the existing
    resolveAwsExternalIdForModal + AWS_DRAFT_EXTERNAL_ID_KEY from
    PR #146 — no parallel generator) so toggling auth modes mid-edit
    never surfaces diverging values.
  - buildAccountRequest's bastion branch now collects aws_external_id
    from the new bastion-mode input, undefined when blank.
  - New renderAwsBastionTrustPolicy() mirrors renderAwsTrustPolicy but
    sets Principal to the *bastion* account root (looked up via
    listAccounts → matched on the dropdown selection), with the same
    partition awareness (#130c) and race short-circuit (#130a) as the
    role_arn renderer. Re-renders on auth-mode change and on bastion
    dropdown change.

Backend (internal/api/handler_accounts.go):
  - validateAWSAuthMode now also runs validateAWSExternalID for bastion
    mode when aws_role_arn is non-empty, parallel to role_arn.
    workload_identity_federation stays exempt (OIDC verifies identity
    via the token subject claim — stscreds.WebIdentityRoleOptions has
    no ExternalID field), and so does access_keys (no AssumeRole at
    all). The empty-role-ARN exemption mirrors role_arn's self-account
    onboarding shortcut.
  - Updated the "required" error message to mention both modes.
  - The credential resolver was already correct: resolveBastionProvider
    delegates to resolveRoleARNProvider, which has always read
    account.AWSExternalID and threaded it into stscreds.AssumeRoleOptions
    — so the missing piece was the frontend collection + backend
    validation, not the resolver.

Tests:
  - frontend/src/__tests__/html.test.ts — assert the bastion section
    has the new input, copy button, and trust-policy block.
  - frontend/src/__tests__/settings-external-id.test.ts — new suite
    covering: bastion shows the same draft as role_arn; selecting a
    bastion renders a snippet with the bastion's account root as
    Principal; buildAccountRequest sends aws_external_id; toggling
    modes preserves the draft.
  - frontend/src/__tests__/settings-accounts.test.ts — DOM scaffold
    extended with the new bastion-mode fields.
  - internal/api/handler_accounts_external_id_test.go — bastion now
    enforced in dedicated tests (BastionRequiresExternalID,
    BastionHappyPath, BastionNoRoleArnExempt) and removed from the
    "no-validation-needed" loop.

Builds on PR #146 (#126/#127/#128/#130) — reuses the secure CSPRNG
generator, sessionStorage draft persistence, validateAWSExternalID
charset/length validator, and partition-aware ARN rendering.

Closes #129.
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@cristim has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 48 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e55fc48-fd42-4245-9dfd-7a7156dcf451

📥 Commits

Reviewing files that changed from the base of the PR and between 3ccc042 and e30e932.

📒 Files selected for processing (7)
  • frontend/src/__tests__/html.test.ts
  • frontend/src/__tests__/settings-accounts.test.ts
  • frontend/src/__tests__/settings-external-id.test.ts
  • frontend/src/index.html
  • frontend/src/settings.ts
  • internal/api/handler_accounts.go
  • internal/api/handler_accounts_external_id_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/bastion-external-id

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

@cristim

cristim commented Apr 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Apr 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/feat New capability labels Apr 28, 2026
@cristim

cristim commented Apr 28, 2026

Copy link
Copy Markdown
Member Author

P1: closes a confused-deputy gap (#129) for bastion auth mode — same ExternalID hardening already shipped for role_arn (#146). CI green, mergeable, awaiting final CodeRabbit pass. Next up after any unblocked P0 work. (triage agent wave2-F)

@cristim
cristim merged commit 3190ef5 into feat/multicloud-web-frontend Apr 28, 2026
3 checks passed
@cristim
cristim deleted the fix/bastion-external-id branch April 29, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant