Repository navigation
fix(credentials): assume bastion targets with the bastion's credentials, never the host's - #1556
Conversation
…ls, never the host's resolveBastionProvider fell back to assuming a bastion account's target role with the caller's STS client whenever AccountLookup or STSClientFactory was nil. No production caller set either option, so every bastion account was reached with the CUDly host's credentials and the bastion was never used (audit finding A03-011). Every AWS resolver caller now passes both options: the scheduler, the purchase manager, the commitment-options probe, and the API's marketplace, account-test and org-discovery paths. The lookup is the config store's GetCloudAccount; the factory builds an STS client from the host's AWS config signed with the bastion's credentials. The fallback is removed: the resolver now returns an error when either option is nil. The bastion's own credentials are resolved with the caller's ambient options, so a bastion that is the host account still works once the host check passes. Behaviour change: a bastion account whose target role trusts the CUDly host instead of the bastion now fails at AssumeRole until its trust policy allows the bastion. TestResolveBastionProvider_LegacyFallback is now an error assertion, and each caller has a test that it passes both options. Closes LeanerCloud#263 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the git-secrets account-ID detector Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
govulncheck in the Docker image build now fails on these two advisories, both fixed in x/net v0.62.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale: "Pass-after ( Reviewer: rG4. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rH3. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on |
…ons at 02:00 UTC (#1580) * fix(iac): fire delayed purchases by default; collect AWS recommendations at 02:00 UTC Owner decision D33 (#712): the fire_scheduled_purchases sweep is now on by default on every platform. enable_fire_scheduled_purchases_schedule defaults to true in the aws, azure and gcp environment roots and in the Lambda, Fargate, Cloud Run, GKE, Container Apps and AKS modules, and the CloudFormation EnableFireScheduledPurchasesSchedule parameter defaults to "true". The sweep only executes purchases already approved for a later time, and the 24h due-row age cap (#1549) refuses and alerts on anything older, so the first run cannot buy a stale backlog. The process sweep and every other opt-in toggle stay off. The Go policy test now treats fire as default-on (it would flag a committed AWS tfvars that turns it off), and the module terraform tests expect the fire job by default and cover the opt-out. Owner decision D32 (#756): the AWS recommendation_schedule default moves from rate(1 day) to cron(0 2 * * ? *), 02:00 UTC like GCP and Azure, in the env root, the Lambda and Fargate modules, the github-*.tfvars and the example tfvars. A rate() schedule has no known phase, so the freshness endpoint's next_collection_at was always null on AWS. A new test reads every default and committed value and checks the API's NextCollectionAt returns the next 02:00 UTC for each. Refs #712 Refs #756 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): note that bastion target roles must trust the bastion #1556 assumes a bastion account's target role with the bastion's own credentials and no longer falls back to the CUDly host's, so a target role that trusts the host instead of the bastion now gets AccessDenied until its trust policy names the bastion. Refs #712 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * revert(cfn): leave the CloudFormation install stack to its retirement #1432 (D29) deletes cloudformation/stacks/CUDly/ and scheduled_task_cfn_test.go, so the CloudFormation edits in this branch would only cause modify/delete conflicts. Restore both files to main and drop CloudFormation from the CHANGELOG entry. The fire sweep's default changes on the Terraform platforms only. Until #1432 lands, TestCloudFormationSchedulesEveryRequiredTask and the "opt-in task on by default" negative in that file fail on this branch alone, because the policy now treats fire as default-on and the retiring template still defaults it off; #1432 removes both tests. Refs #712 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(iac): give the schedule-default test its own default regex TestRecommendationScheduleDefaultsReportNextCollection borrowed tfStringDefaultRe from scheduled_task_cfn_test.go, which #1432 deletes; the combined tree would not compile. Declare a local copy instead. Refs #712 Refs #756 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
This finishes the last open item of #263: turning
TestResolveBastionProvider_LegacyFallbackinto an error assertion once the fallback is gone (audit finding A03-011). #1152 handled the other items. This PR removes the fallback, wires the bastion options into every AWS resolver caller, and adds the tests.Behaviour change (owner sign-off needed)
A bastion-mode account whose target role trusts the CUDly host instead of the bastion stops working. Until now CUDly assumed every bastion account's target role directly with its own host credentials (see Root cause). So a target role whose trust policy names the CUDly host's role or account worked by accident. After this PR the target role is assumed by the bastion account. That role's
AssumeRolecall now fails withAccessDenied. The account test endpoint reports it asassume role denied for <role ARN>, and collection and purchases for that account fail with the same STS error.What an affected customer has to change:
arn:aws:iam::<bastion account ID>:rootor the bastion's specific IAM user or role, with the samests:ExternalIdcondition as before. Settings > Accounts already renders exactly this snippet for bastion mode, so they can copy it from there.sts:AssumeRoleon the target role ARN.role_arnmode), the host's cross-account grant must list the bastion's account ID (cross_account_target_account_ids/CrossAccountTargetAccountIds).docs/DEPLOYMENT.mdalready says this. The target account no longer needs to be listed.Setups that followed the UI snippet and
docs/DEPLOYMENT.mdalready match this and need no change. In fact, before this PR such a setup could fail, because the host tried to assume a target account its own IAM grant does not list. Unchanged: a bastion that is the CUDly host account itself (role_arnwith no role ARN), which still resolves to the host credentials after the host-identity check.Root cause
resolveBastionProviderloads the bastion account, resolves its credentials, builds an STS client from them and assumes the target role with it, but only whenAWSResolveOptions.AccountLookupandSTSClientFactoryare both set. When either was nil it fell back toresolveRoleARNProviderwith the caller's STS client, which carries the CUDly host's credentials. No production caller set either option. Two callers used the no-optionsResolveAWSCredentialProvider, and four passed only the ambient options. So every bastion account in production went through the fallback, and the bastion was never used.known_issues/03marked this entry RESOLVED, but only the plumbing had landed.Fix
internal/credentials/resolver.go:resolveBastionProvidernow returns an error when either option is nil, so it never falls back to the host's STS client. The bastion's own credentials are now resolved with the caller's ambient options (the bastion lookup and factory are still dropped, so a bastion can't chain to another). Without this, a bastion that is the host account would have stopped working when the fallback went.internal/credentials/bastion_options.go(new):AccountLookupFromStore(config storeGetCloudAccount; a nil store gives a nil lookup, which fails closed) andNewSTSClientFactory(base). The factory builds an STS client from the host's AWS config (region, endpoint, HTTP client, retryer) signed with the bastion's credentials. Those credentials are wrapped in anaws.CredentialsCache(once), so a role-based bastion is not re-assumed on every call.internal/scheduler/scheduler.go:collectAWSForAccount(was the no-options call). NewSchedulerConfig.BastionSTSFactoryandAmbientAWSCreds, so a bastion that is the host account still works.internal/purchase/execution.go/manager.go:resolveAWSProvider, with the newManagerConfig.BastionSTSFactory.internal/server/app.go: wires the factory and ambient credentials into the re-initialized manager and scheduler.newCommitmentProbeConfigBuildernow takes the account store and the hostaws.Config.internal/api/handler_marketplace.go,handler_accounts_awsprobe.go,handler_accounts.go(buildOrgRootAWSConfig, was the no-options call): all use the newHandler.bastionResolveOptions(base).stscreds.NewAssumeRoleProvideroutsideinternal/credentials.known_issues/03_credentials_resolver.md: the entry now records that the first fix left the fallback live, what this PR changes, the behaviour change, and the tests that lock it down.No DB change, no migration, no concurrency surface. Each resolution builds its own STS client, and the factory only reads the base config.
Closes #263
Verification
All of this is local unit, mock and fixture evidence. No real STS calls. Callers in
credentials,schedulerandpurchaseget in-process STS fakes. Theapiandservertests use the real SDK STS client against a fakeHTTPClientthat answersAssumeRoleand records which access key signed each request (SigV4Credential=). So they prove the targetAssumeRolewas signed with the bastion's keys and not the host's. All runs usedAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true.New and changed tests:
TestResolveBastionProvider_LegacyFallback(now: no options, lookup only, factory only, ambient only all error; the host STS client is never used),_TargetAssumedWithBastionSTS,_HostAccountBastion(host: ambient keys; non-host:ErrNotHostAccount, no client built),_WithBastionAndRoleARN/_NoRoleARN(moved to the options path),TestAccountLookupFromStore,TestNewSTSClientFactory_SignsWithGivenProviderTestScheduler_CollectAWSForAccount_BastionUsesBastionCredentials,_BastionWithoutFactoryFailsClosed,_HostAccountBastionUsesAmbientTestResolveAWSProvider_BastionUsesBastionCredentials,_BastionWithoutFactoryFailsClosedTestCommitmentProbeConfigBuilder_BastionAssumesTargetAsBastion,_BastionWithoutAccountStoreFailsClosed(existing_HostIdentityCheckupdated for the new signature)TestAPIAWSCallers_BastionAssumesTargetAsBastion,_BastionWithoutConfigStoreFailsClosedTestTestAccountCredentials_AWSValidConfigsProbeCallerIdentitynow asserts the AssumeRole signer is the bastion's key; the fixture store returns a real bastion rowFail-before. I put the old behaviour back in place (legacy fallback restored, each caller back to its
mainoptions) and kept the new types and fields so the tests compiled:A mutation probe removed
AmbientProviderfrom the scheduler's options, andTestScheduler_CollectAWSForAccount_HostAccountBastionUsesAmbientfailed (aws_role_arn is empty and no ambient credentials available).Pass-after (
b4799568):No integration tests were added: the change has no DB-level behaviour.
Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree:
queue/platform@400d256b= origin/main7b86e386plus these heads, merged in this order:Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers. The full suite ran onf7c48020, which differs from this tree only by test-only commits on fix/issue-30, fix/issue-713 and fix/issue-263 that replace placeholder account-number literals with named constants:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....--build-tags integration.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1):internal/analytics,internal/api,internal/auth,internal/config,internal/database/postgres/migrations,internal/purchase,internal/schedulerandinternal/serverall pass. Migrations 000149, 000150 and 000151 apply in order on this tree.approval-details,settings-accounts,api) pass (221 tests).accountkey); fixed with named constants. Re-run on this tree: git-secrets, using the repo'sscripts/setup-git-secrets.shpatterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 183 PASS, 0 FAIL..tftest.hclfiles were not run locally (CI runs them). Nothing was applied to any cloud.Out of scope
buildOrgRootAWSConfig(org discovery) still passes no ambient options, as onmain. So an org root that is arole_arnaccount with no role ARN still fails discovery, and so does a bastion-mode org root whose bastion is the host account. Adding ambient options there would make a host-account org root usable, which is a separate behaviour change. Left for a follow-up if wanted.known_issues/03_credentials_resolver.mdstays inknown_issues/: every entry in it is already resolved, but the file is not tracked by test(auth): carve-out, recovery-code, deactivation and reset-token failures have no test #263 alone. Moving it toknown_issues/resolved/can happen in the next sweep.Frontend follow-up for the UI owner
None needed. The bastion trust-policy snippet in Settings > Accounts already uses the bastion account as Principal, which is what the backend now requires. Optional: release notes could point bastion users at that snippet (see the behaviour change above).
Maintainer: please mirror #263's triage labels on this PR:
priority/p2,severity/medium,impact/internal,type/chore,urgency/this-quarter,effort/m,triaged.🤖 Generated with Claude Code