You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(iac): grant ec2:CreateTags to member-account purchase roles so the idempotency tag is written #702
The member-account IAM templates do not grant ec2:CreateTags (and, for Redshift, Describe/CreateTags). Outside the host account the purchase idempotency tag is therefore never written, and the failure is only logged, so the pre-buy tag guard cannot detect a prior purchase.
iac/federation/aws-target/terraform/main.tf:62-75 (plus the CloudFormation and cross-account variants of the same template)
Swallowed failure in cloud-commitments-go ec2/client.go:203-205
Failure scenario
A purchase runs against a member account assumed through the federation role. The EC2 or Redshift buy succeeds, the CreateTags call is denied, and the error is only logged. A later retry or re-drive checks the tag guard, finds no tag, and buys again. Found by source reading; not reproduced live.
Suggested fix
Add a scoped ec2:CreateTags (and the Redshift Describe/CreateTags actions) to every target template. Surface a tag-write failure on the execution record instead of only logging it.
Severity
High: a double purchase of a reserved instance is real money, and the guard that is meant to prevent it silently does nothing in member accounts.
Related: #157 (deploy-role grant gaps only, not the member-account purchase role).
Reviewer ID: MON-03 (money H3)
Found by the 2026-10-08 review fleet.
PR 773 covers the IaC half: scoped ec2:CreateTags and redshift tag actions for all member-account purchase templates. Not in that PR, so this issue stays open: (1) surfacing a tag-write failure on the execution record (the swallow is in cloud-commitments-go ec2/client.go:203-205 plus platform handling); (2) the host runtime role has the scoped ec2:CreateTags already but no redshift:CreateTags/DescribeTags, so host-account Redshift buys are untagged. Existing member accounts need their template re-applied.
Follow-up filed: #782 (the iacfiles IAM scoping test only inspects Terraform statements that have a Sid, so a Sid-less ec2:CreateTags on * would pass; found in the coordinator's delta review of PR 773).
The EC2 half is merged at 7e2a3d8 (PR #773). The Redshift half stays open, pending owner decision 29 (sandbox tag test). Follow-ups: #779, #782, #42. Not closing this issue.
Released: the ec2:CreateTags part of this issue is already fixed by #773 (7e2a3d8, "Refs #702"). The rest is outside what a platform PR can do right now.
Already fixed (checked on main):
Every member-account purchase role has a separate ec2:CreateTags statement scoped to arn:aws:ec2:*:*:reserved-instances/*. That covers aws-target and aws-cross-account, in both Terraform and CloudFormation, plus both *-cli.sh.tmpl templates and CUDly-CrossAccount.
The host roles (CUDly CFN, Lambda, Fargate) have the same grant.
TestAWSCLITemplatesTagReservedInstancesOnly pins the grant in all seven policy sources, and scripts/check-aws-iam-parity.sh passes.
Not done, and why:
Redshift DescribeTags/CreateTags: your fix(iac): let member-account roles tag purchased EC2 reservations #773 follow-up removed these on purpose, and templates_aws_tagging_test.go:174 blocks re-adding them until decision 29 (the sandbox tag test). Adding them here would override that decision.
Reporting a failed tag write on the execution record: the error is dropped in cloud-commitments-go (providers/aws/services/ec2/client.go:207-208 at the pinned version; Redshift does the same), and common.PurchaseResult has no field for it. That needs a library change first.
Possibly relevant to decision 29: I found this by reading the code; it hasn't been checked against a real account. Neither the member roles nor the host roles grant redshift:DescribeTags. The platform always sets an idempotency token (internal/purchase/execution.go:680), and the library's Redshift pre-buy dedupe refuses to buy when DescribeTags fails. So Redshift reserved-node purchases are probably refused every time before buying. That rules out a Redshift double-buy, but it likely blocks Redshift purchases entirely.
You may want to narrow this issue to the Redshift/decision-29 work and the library change, or split those out and close this one.
Owner approved option A (a live sandbox Redshift tag test) on 2026-10-17. Result:
Live account check (read only): the sandbox account has 1 Redshift cluster (us-east-1) and 0 reserved nodes. Option A can't run there without buying a reserved node, which would need a separate spend approval.
AWS documentation answers the question without a purchase: the Amazon Redshift Management Guide, "Tag resources in Amazon Redshift" (https://docs.aws.amazon.com/redshift/latest/mgmt/amazon-redshift-tagging.html), lists every taggable resource type: CIDR/IP, Cluster, Cluster security group, Cluster security group ingress rule, Amazon EC2 security group, HSM connection, HSM client certificate, Parameter group, Snapshot, Subnet group, Integration. Reserved nodes are not on the list. The CreateTags API reference describes the call as "Adds tags to a cluster".
Conclusion: granting redshift:CreateTags/DescribeTags (option B) would not make reserved-node purchases taggable, so this goes with option C: change cloud-commitments-go so Redshift pre-buy dedupe does not depend on DescribeTags. That is filed in the go repo (linked below). The template guard at internal/iacfiles/templates_aws_tagging_test.go:174 stays as it is. This issue stays open for the platform pin bump once the library fix ships.
Until then, Redshift reserved-node purchases are expected to be refused (per code reading: no role grants redshift:DescribeTags, and the dedupe fails closed). That has not been verified against a real purchase.
Go side of option C is planned in LeanerCloud/cloud-commitments-go#509: Redshift reserved-node dedupe moves to DescribeReservedNodes (owned state, same offering ID and node count, start time no earlier than five minutes before the first dispatch; more than one match refuses), plus recovery after an unknown outcome. It needs a new common.PurchaseOptions.RequestedAt.
For it to take effect, the platform half needs:
Take Redshift out of awsPurchaseHasNoDuplicateGuard / recRedriveRefusalReason (internal/purchase/manager.go), otherwise every Redshift re-drive is still refused before the provider runs.
Persist a first-dispatch time per purchase lineage, written before the buy (PurchaseExecution has only CreatedAt and ScheduledDate today), and pass it as RequestedAt; it must stay stable across re-drives. A tokenized purchase with a zero RequestedAt will refuse.
Keep the execution claim as the guard against two dispatchers buying concurrently; the provider matcher only detects an earlier purchase, it is not a lock.
Delay re-drives long enough for DescribeReservedNodes to list a just-bought node.
IAM: redshift:DescribeReservedNodes is already granted (lambda and fargate terraform, both CLI templates); DescribeTags is no longer needed.
Claiming: opencode-cli (session: opencode/space-bunny-free). Plan: the EC2 half is already done on main (both aws-target variants grant a scoped ec2:CreateTags, added by #773); the remaining gap is Redshift, which no target grants. The go client calls redshift:CreateTags post-purchase and redshift:DescribeTags for the idempotency-token guard, so in member accounts both are denied and a re-drive can buy again. I will add a RedshiftReservedNodeTagging statement to BOTH iac/federation/aws-target/terraform/main.tf and iac/federation/aws-target/cloudformation/template.yaml, scoped to arn:aws:redshift:*:*:reservednode:* to match the ARN the client builds, mirroring the existing EC2ReservedInstanceTagging pattern; and verify scripts/check-aws-iam-parity.sh (CFN/TF parity), the generator tests that read these files, and the iac pre-commit checks. The 'surface the failure on the execution record' half already holds: the EC2 client wraps a non-retryable CreateTags error in retry.ErrPermanent and the Redshift client does the same, so neither only logs it.
Evidence for the deliberate guard in internal/iacfiles/templates_aws_tagging_test.go (TestMemberAccountPoliciesTagReservedInstancesOnly), which pins redshift:CreateTags out of all seven member-account policies. I attempted the scoped grant and reverted it; here is why the guard is right.
AWS Service Authorization Reference, Amazon Redshift, action CreateTags:
CreateTags | Grants permission to add one or more tags to a specified resource | Access level: Tagging | Resource types: cluster, eventsubscription, hsmclientcertificate, hsmconfiguration, integration, parametergroup, securitygroup, securitygroupingress-cidr
reserved-node* is not among them. The reference states that when the Resource types column is empty you must use "*", and when it lists types you may only use those. So redshift:CreateTags cannot be scoped to arn:aws:redshift:*:*:reservednode:*; a policy written that way matches no supported resource type and the tag write still fails with AccessDenied. Granting it would require Resource: "*" (optionally with condition keys), which is exactly the wider grant the guard is refusing. This is consistent with the guard's comment, "it cannot be scoped below '*'", and it is now sourced rather than assumed.
Also unverified and still open: whether Redshift accepts a tag on a reserved node at all. The API reference documents CreateTags with a cluster ARN example (arn:aws:redshift:us-east-2:123456789:cluster:t1) and a ReservedNodes resource type is not listed for it. The go client builds arn:aws:redshift:<region>:<account>:reservednode:<nodeID> (no hyphen) and calls CreateTags on it, which nothing in the docs confirms is taggable.
So the Redshift half of this issue stays open behind two real questions, neither of which source reading can settle:
Does redshift:CreateTags work on a reserved node ARN? Needs an actual member-account call, not a unit test.
If yes, is Resource: "*" plus condition keys acceptable, given it is broader than the EC2 grant the same issue added?
redshift:DescribeTags (the idempotency-token guard) needs the same scoping check before it is granted; it was not verified here.
The EC2 half of #702 is complete on main: both aws-target and cross-account variants, in Terraform, CloudFormation and both CLI onboarding templates, grant ec2:CreateTags scoped to arn:aws:ec2:*:*:reserved-instances/*, and scripts/check-aws-iam-parity.sh holds them in parity.
Released: opencode-cli. No code change; the working tree is back at origin/main (8caed39) and no branch was pushed. [opencode/space-bunny-free]
@IAmJSD a scope question on the platform half now that cloud-commitments-go#515 has merged (aws aeb8a6f7ff51). I'll go ahead with the default unless you say otherwise.
What I checked (origin/main d9b2aa7, go aws@aeb8a6f7ff51):
None of the platform half is done.
Redshift reserved-node buys are refused today: the pinned go still needs redshift:DescribeTags, and no role grants it.
After the pin bump, every tokenized Redshift buy refuses unless PurchaseOptions.RequestedAt is set. The platform always sends a token (internal/purchase/execution.go:680).
Every Redshift re-drive and retry is already refused by the platform (awsPurchaseHasNoDuplicateGuard, armedRedriveRefusal, the Retry handler). That means the go caller contract (no second same-tuple dispatch while an earlier lineage can be re-driven) is satisfied as long as those guards stay.
Options
A (default): restore Redshift buying now and keep re-drive refused.
One small PR: bump the go pin and pass RequestedAt (the dispatch time) on each purchase call.
No migration and no store change.
First-dispatch dedupe and go's in-call recovery after an unknown outcome both work.
A failed Redshift purchase still needs a manual check in AWS, as it does today.
B: also make Redshift purchases retryable. This is the full design from your 2026-10-17 comment: a persisted first-dispatch time per lineage, a cross-lineage conflict check, a settle delay before re-drive, and lifting the guard. That is about 1000 lines over three stacked PRs.
An adversarial plan review found real gaps to close first: same-family sibling retries, the stamp/check race needing a symmetric rule or an advisory lock, a 24h block with no release action, and pre-change chains.
B also needs decisions on:
the block horizon;
the settle wait (AWS listing lag is undocumented);
a release action for a failed lineage;
how fan-out rows with no account match other purchases.
Known gap under both options: go's match window has no upper bound. A second plan buying the same account, region, offering and node count within about 5 minutes of a completed purchase would adopt that node and report success without buying. This needs two plans with an identical Redshift recommendation on the same tick. I'll record it in known_issues/ with option A unless you want it guarded now.
Option A merged in #1138 (e304ebea). The go pin moves to aws aeb8a6f7ff51 / pkg 541e05e5fd29, and RequestedAt is now set on every purchase dispatch, so Redshift reserved-node buys go through again with DescribeReservedNodes dedupe. Redshift re-drive and retry stay refused. known_issues/39_redshift_dedupe_window.md records the residual risks: the unbounded match window, concurrent same-tuple purchases, and adoption recorded as a normal purchase. This issue stays open for option B (Redshift retry with a persisted first-dispatch time); its plan and review findings are kept for when the owner picks it up.
Activity
fix(iac): let member-account roles tag purchased reservations
cristim commentedon Oct 9, 2026
PR 773 covers the IaC half: scoped ec2:CreateTags and redshift tag actions for all member-account purchase templates. Not in that PR, so this issue stays open: (1) surfacing a tag-write failure on the execution record (the swallow is in cloud-commitments-go ec2/client.go:203-205 plus platform handling); (2) the host runtime role has the scoped ec2:CreateTags already but no redshift:CreateTags/DescribeTags, so host-account Redshift buys are untagged. Existing member accounts need their template re-applied.
fix(iac): drop the Redshift tag grant and pin EC2 tag scoping in all …
cristim commentedon Oct 9, 2026
Follow-up filed: #782 (the iacfiles IAM scoping test only inspects Terraform statements that have a Sid, so a Sid-less ec2:CreateTags on * would pass; found in the coordinator's delta review of PR 773).
fix(iac): let member-account roles tag purchased EC2 reservations (#773)
cristim commentedon Oct 9, 2026
The EC2 half is merged at 7e2a3d8 (PR #773). The Redshift half stays open, pending owner decision 29 (sandbox tag test). Follow-ups: #779, #782, #42. Not closing this issue.
IAmJSD commentedon Oct 16, 2026
Released: EC2 part already fixed by #773; the rest is blocked on decision 29 or needs a library change (details below)
IAmJSD commentedon Oct 16, 2026
Released: the
ec2:CreateTagspart of this issue is already fixed by #773 (7e2a3d8, "Refs #702"). The rest is outside what a platform PR can do right now.Already fixed (checked on main):
ec2:CreateTagsstatement scoped toarn:aws:ec2:*:*:reserved-instances/*. That covers aws-target and aws-cross-account, in both Terraform and CloudFormation, plus both*-cli.sh.tmpltemplates andCUDly-CrossAccount.CUDlyCFN, Lambda, Fargate) have the same grant.TestAWSCLITemplatesTagReservedInstancesOnlypins the grant in all seven policy sources, andscripts/check-aws-iam-parity.shpasses.Not done, and why:
DescribeTags/CreateTags: your fix(iac): let member-account roles tag purchased EC2 reservations #773 follow-up removed these on purpose, andtemplates_aws_tagging_test.go:174blocks re-adding them until decision 29 (the sandbox tag test). Adding them here would override that decision.providers/aws/services/ec2/client.go:207-208at the pinned version; Redshift does the same), andcommon.PurchaseResulthas no field for it. That needs a library change first.Possibly relevant to decision 29: I found this by reading the code; it hasn't been checked against a real account. Neither the member roles nor the host roles grant
redshift:DescribeTags. The platform always sets an idempotency token (internal/purchase/execution.go:680), and the library's Redshift pre-buy dedupe refuses to buy whenDescribeTagsfails. So Redshift reserved-node purchases are probably refused every time before buying. That rules out a Redshift double-buy, but it likely blocks Redshift purchases entirely.You may want to narrow this issue to the Redshift/decision-29 work and the library change, or split those out and close this one.
cristim commentedon Oct 17, 2026
Owner approved option A (a live sandbox Redshift tag test) on 2026-10-17. Result:
Conclusion: granting redshift:CreateTags/DescribeTags (option B) would not make reserved-node purchases taggable, so this goes with option C: change cloud-commitments-go so Redshift pre-buy dedupe does not depend on DescribeTags. That is filed in the go repo (linked below). The template guard at internal/iacfiles/templates_aws_tagging_test.go:174 stays as it is. This issue stays open for the platform pin bump once the library fix ships.
Until then, Redshift reserved-node purchases are expected to be refused (per code reading: no role grants redshift:DescribeTags, and the dedupe fails closed). That has not been verified against a real purchase.
cristim commentedon Oct 17, 2026
Library fix filed: LeanerCloud/cloud-commitments-go#509 (P1).
cristim commentedon Oct 17, 2026
Go side of option C is planned in LeanerCloud/cloud-commitments-go#509: Redshift reserved-node dedupe moves to
DescribeReservedNodes(owned state, same offering ID and node count, start time no earlier than five minutes before the first dispatch; more than one match refuses), plus recovery after an unknown outcome. It needs a newcommon.PurchaseOptions.RequestedAt.For it to take effect, the platform half needs:
awsPurchaseHasNoDuplicateGuard/recRedriveRefusalReason(internal/purchase/manager.go), otherwise every Redshift re-drive is still refused before the provider runs.RequestedAt; it must stay stable across re-drives. A tokenized purchase with a zero RequestedAt will refuse.DescribeReservedNodesto list a just-bought node.IAM:
redshift:DescribeReservedNodesis already granted (lambda and fargate terraform, both CLI templates);DescribeTagsis no longer needed.cristim commentedon Oct 18, 2026
Claiming: opencode-cli (session: opencode/space-bunny-free). Plan: the EC2 half is already done on main (both aws-target variants grant a scoped
ec2:CreateTags, added by #773); the remaining gap is Redshift, which no target grants. The go client callsredshift:CreateTagspost-purchase andredshift:DescribeTagsfor the idempotency-token guard, so in member accounts both are denied and a re-drive can buy again. I will add aRedshiftReservedNodeTaggingstatement to BOTHiac/federation/aws-target/terraform/main.tfandiac/federation/aws-target/cloudformation/template.yaml, scoped toarn:aws:redshift:*:*:reservednode:*to match the ARN the client builds, mirroring the existingEC2ReservedInstanceTaggingpattern; and verifyscripts/check-aws-iam-parity.sh(CFN/TF parity), the generator tests that read these files, and the iac pre-commit checks. The 'surface the failure on the execution record' half already holds: the EC2 client wraps a non-retryable CreateTags error inretry.ErrPermanentand the Redshift client does the same, so neither only logs it.cristim commentedon Oct 18, 2026
Evidence for the deliberate guard in
internal/iacfiles/templates_aws_tagging_test.go(TestMemberAccountPoliciesTagReservedInstancesOnly), which pinsredshift:CreateTagsout of all seven member-account policies. I attempted the scoped grant and reverted it; here is why the guard is right.AWS Service Authorization Reference, Amazon Redshift, action
CreateTags:reserved-node*is not among them. The reference states that when the Resource types column is empty you must use"*", and when it lists types you may only use those. Soredshift:CreateTagscannot be scoped toarn:aws:redshift:*:*:reservednode:*; a policy written that way matches no supported resource type and the tag write still fails with AccessDenied. Granting it would requireResource: "*"(optionally with condition keys), which is exactly the wider grant the guard is refusing. This is consistent with the guard's comment, "it cannot be scoped below '*'", and it is now sourced rather than assumed.Also unverified and still open: whether Redshift accepts a tag on a reserved node at all. The API reference documents
CreateTagswith a cluster ARN example (arn:aws:redshift:us-east-2:123456789:cluster:t1) and aReservedNodesresource type is not listed for it. The go client buildsarn:aws:redshift:<region>:<account>:reservednode:<nodeID>(no hyphen) and callsCreateTagson it, which nothing in the docs confirms is taggable.So the Redshift half of this issue stays open behind two real questions, neither of which source reading can settle:
redshift:CreateTagswork on a reserved node ARN? Needs an actual member-account call, not a unit test.Resource: "*"plus condition keys acceptable, given it is broader than the EC2 grant the same issue added?redshift:DescribeTags(the idempotency-token guard) needs the same scoping check before it is granted; it was not verified here.The EC2 half of #702 is complete on main: both aws-target and cross-account variants, in Terraform, CloudFormation and both CLI onboarding templates, grant
ec2:CreateTagsscoped toarn:aws:ec2:*:*:reserved-instances/*, andscripts/check-aws-iam-parity.shholds them in parity.Released: opencode-cli. No code change; the working tree is back at origin/main (8caed39) and no branch was pushed. [opencode/space-bunny-free]
cristim commentedon Oct 19, 2026
@IAmJSD a scope question on the platform half now that cloud-commitments-go#515 has merged (aws
aeb8a6f7ff51). I'll go ahead with the default unless you say otherwise.What I checked (origin/main d9b2aa7, go aws@aeb8a6f7ff51):
redshift:DescribeTags, and no role grants it.PurchaseOptions.RequestedAtis set. The platform always sends a token (internal/purchase/execution.go:680).awsPurchaseHasNoDuplicateGuard,armedRedriveRefusal, the Retry handler). That means the go caller contract (no second same-tuple dispatch while an earlier lineage can be re-driven) is satisfied as long as those guards stay.Options
RequestedAt(the dispatch time) on each purchase call.Known gap under both options: go's match window has no upper bound. A second plan buying the same account, region, offering and node count within about 5 minutes of a completed purchase would adopt that node and report success without buying. This needs two plans with an identical Redshift recommendation on the same tick. I'll record it in
known_issues/with option A unless you want it guarded now.docs(known-issues): record concurrent Redshift dedupe and invisible a…
fix(purchase): pass RequestedAt so Redshift reserved-node buys work w…
cristim commentedon Oct 20, 2026
Option A merged in #1138 (
e304ebea). The go pin moves to awsaeb8a6f7ff51/ pkg541e05e5fd29, andRequestedAtis now set on every purchase dispatch, so Redshift reserved-node buys go through again withDescribeReservedNodesdedupe. Redshift re-drive and retry stay refused.known_issues/39_redshift_dedupe_window.mdrecords the residual risks: the unbounded match window, concurrent same-tuple purchases, and adoption recorded as a normal purchase. This issue stays open for option B (Redshift retry with a persisted first-dispatch time); its plan and review findings are kept for when the owner picks it up.