infra: sibling_staging_arns — the push-only moves/* grant on both host modules (#331)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEypt7de2X1G84aQ9rNkCf
4 files changed,
+49
−4
infra/modules/region-host/main.tf+19 −2infra/modules/region-host/variables.tf+6 −0infra/modules/region-remote-host/main.tf+18 −2infra/modules/region-remote-host/variables.tf+6 −0
diff --git a/infra/modules/region-host/main.tf b/infra/modules/region-host/main.tf| index ae5afb52f..deb71e96f 100644 |
| --- a/infra/modules/region-host/main.tf |
| +++ b/infra/modules/region-host/main.tf |
| @@ -78,7 +78,12 @@ resource "aws_iam_role_policy" "host_buckets" { |
| role = aws_iam_role.host.id |
| policy = jsonencode({ |
| Version = "2012-10-17" |
| - Statement = [ |
| + # The two sibling-staging statement groups (instance-move spec, issue |
| + # #331) are appended only when sibling_staging_arns is non-empty: |
| + # push-only into moves/* — a box can stage a residence move TOWARD a |
| + # sibling region but can never read that region's data or touch its |
| + # live instances/* replica lineage. |
| + Statement = concat([ |
| { |
| Effect = "Allow" |
| Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] |
| @@ -131,7 +136,19 @@ resource "aws_iam_role_policy" "host_buckets" { |
| ] |
| Resource = "*" |
| }, |
| - ] |
| + ], |
| + [for arn in var.sibling_staging_arns : { |
| + Effect = "Allow" |
| + Action = ["s3:PutObject", "s3:DeleteObject"] |
| + Resource = "${arn}/moves/*" |
| + }], |
| + [for arn in var.sibling_staging_arns : { |
| + Effect = "Allow" |
| + Action = ["s3:ListBucket"] |
| + Resource = arn |
| + Condition = { StringLike = { "s3:prefix" = "moves/*" } } |
| + }] |
| + ) |
| }) |
| } |
diff --git a/infra/modules/region-host/variables.tf b/infra/modules/region-host/variables.tf| index 3f0a898c0..b508af535 100644 |
| --- a/infra/modules/region-host/variables.tf |
| +++ b/infra/modules/region-host/variables.tf |
| @@ -114,3 +114,9 @@ variable "alerts_email" { |
| type = string |
| default = "" |
| } |
| + |
| +variable "sibling_staging_arns" { |
| + description = "Sibling regions' data-bucket ARNs this host may STAGE residence-moves into (instance-move spec, issue #331). The grant is moves/*-scoped push-only: Put/Delete on the staging objects and a prefix-conditioned ListBucket — never instances/*, never GetObject, so residency-at-rest holds while a move's delta rounds run. Empty (the default): no cross-bucket statement at all." |
| + type = list(string) |
| + default = [] |
| +} |
diff --git a/infra/modules/region-remote-host/main.tf b/infra/modules/region-remote-host/main.tf| index ce7d0ecf5..d18648e76 100644 |
| --- a/infra/modules/region-remote-host/main.tf |
| +++ b/infra/modules/region-remote-host/main.tf |
| @@ -74,7 +74,11 @@ resource "aws_iam_user_policy" "host" { |
| user = aws_iam_user.host.name |
| policy = jsonencode({ |
| Version = "2012-10-17" |
| - Statement = [ |
| + # Sibling staging (instance-move spec, issue #331) — same push-only |
| + # moves/* shape as region-host's host_buckets policy; keep the two in |
| + # step (this module's README already carries that rule for the S3 |
| + # action list). |
| + Statement = concat([ |
| { |
| Sid = "OwnRegionDataObjects" |
| Effect = "Allow" |
| @@ -87,7 +91,19 @@ resource "aws_iam_user_policy" "host" { |
| Action = ["s3:ListBucket", "s3:GetBucketLocation"] |
| Resource = [aws_s3_bucket.data.arn] |
| }, |
| - ] |
| + ], |
| + [for arn in var.sibling_staging_arns : { |
| + Effect = "Allow" |
| + Action = ["s3:PutObject", "s3:DeleteObject"] |
| + Resource = "${arn}/moves/*" |
| + }], |
| + [for arn in var.sibling_staging_arns : { |
| + Effect = "Allow" |
| + Action = ["s3:ListBucket"] |
| + Resource = arn |
| + Condition = { StringLike = { "s3:prefix" = "moves/*" } } |
| + }] |
| + ) |
| }) |
| } |
diff --git a/infra/modules/region-remote-host/variables.tf b/infra/modules/region-remote-host/variables.tf| index 2096ca518..9fe68ecd6 100644 |
| --- a/infra/modules/region-remote-host/variables.tf |
| +++ b/infra/modules/region-remote-host/variables.tf |
| @@ -13,3 +13,9 @@ variable "deployment_bucket_arn" { |
| type = string |
| default = "" |
| } |
| + |
| +variable "sibling_staging_arns" { |
| + description = "Sibling regions' data-bucket ARNs this host may STAGE residence-moves into (instance-move spec, issue #331). moves/*-scoped push-only, exactly region-host's statement shape: Put/Delete on staging objects + prefix-conditioned ListBucket; never instances/*, never GetObject. Empty (the default): no cross-bucket statement." |
| + type = list(string) |
| + default = [] |
| +} |