carlos / platform Public

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
Paul Campbell pushed by paul@keymail.dev bef83662244ac451465e812dbc4d3654fbc83e3a parent f44f9dc
4 files changed, +49 −4
  • infra/modules/region-host/main.tf +19 −2
  • infra/modules/region-host/variables.tf +6 −0
  • infra/modules/region-remote-host/main.tf +18 −2
  • infra/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 = []
+}