infra: region-remote-host module — data bucket + scoped off-cloud principal (pi now, hq later)
4 files changed,
+125
−0
infra/modules/region-remote-host/README.md+21 −0infra/modules/region-remote-host/main.tf+84 −0infra/modules/region-remote-host/outputs.tf+6 −0infra/modules/region-remote-host/variables.tf+14 −0
diff --git a/infra/modules/region-remote-host/README.md b/infra/modules/region-remote-host/README.md| new file mode 100644 |
| index 000000000..eeee954e8 |
| --- /dev/null |
| +++ b/infra/modules/region-remote-host/README.md |
| @@ -0,0 +1,21 @@ |
| +# region-remote-host |
| + |
| +Provisions what an off-cloud CARLOS host needs when there is no EC2 instance |
| +profile to lean on: a region data bucket plus an IAM user carrying a scoped |
| +inline policy and access key, limited to exactly the deployment bucket and |
| +this region's data bucket (wrong-region-fails-at-IAM, the same residency |
| +property `region-host` gives cloud hosts). |
| + |
| +Two consumers: the dev Raspberry Pi (`region_code = "pi"`) landing in this |
| +PR, and tito's hq box (`region_code = "hq"`) in a later step of the |
| +remote-host plan. The module is reusable as-is for both — they differ only |
| +in account and variable values, not shape. |
| + |
| +The S3 action list in the inline policy is not independently chosen: it is |
| +copied verbatim from `infra/modules/region-host/main.tf`'s |
| +`aws_iam_role_policy.host_buckets` (the authority for what this principal |
| +shape may do to S3), split the same way region-host splits it — object |
| +actions (`GetObject`/`PutObject`/`DeleteObject`) scoped to the object ARNs, |
| +and bucket-level actions (`ListBucket`/`GetBucketLocation`) scoped to the |
| +bucket ARNs. If region-host's action list changes, this module's should |
| +change with it. |
diff --git a/infra/modules/region-remote-host/main.tf b/infra/modules/region-remote-host/main.tf| new file mode 100644 |
| index 000000000..4e85d35e4 |
| --- /dev/null |
| +++ b/infra/modules/region-remote-host/main.tf |
| @@ -0,0 +1,84 @@ |
| +# region-remote-host: what an off-cloud CARLOS host (a Raspberry Pi today, |
| +# tito's hq box later) needs from AWS — a region data bucket plus a scoped |
| +# IAM principal, since there is no EC2 instance profile to lean on off-cloud. |
| +# |
| +# Residency is enforced the same way as region-host: the principal reaches |
| +# the deployment bucket plus ITS OWN region's data bucket and nothing else. |
| +# A wrong-region host fails loudly at the IAM layer, same property as every |
| +# cloud host. |
| + |
| +terraform { |
| + required_version = ">= 1.10" |
| + required_providers { |
| + aws = { |
| + source = "hashicorp/aws" |
| + version = ">= 6.0" |
| + } |
| + } |
| +} |
| + |
| +resource "aws_s3_bucket" "data" { |
| + bucket = var.data_bucket_name |
| +} |
| + |
| +resource "aws_s3_bucket_public_access_block" "data" { |
| + bucket = aws_s3_bucket.data.id |
| + block_public_acls = true |
| + block_public_policy = true |
| + ignore_public_acls = true |
| + restrict_public_buckets = true |
| +} |
| + |
| +# The off-cloud host principal. No instance profile exists off EC2; this |
| +# user + access key is the C6 decision-6 shape (IAM Roles Anywhere is the |
| +# later hardening — the policy below is what either principal would carry). |
| +resource "aws_iam_user" "host" { |
| + name = "carlos-remote-host-${var.region_code}" |
| +} |
| + |
| +resource "aws_iam_access_key" "host" { |
| + user = aws_iam_user.host.name |
| +} |
| + |
| +# Scope: the deployment bucket + THIS region's data bucket and nothing |
| +# else — wrong-region-fails-at-IAM, same property as every cloud host. |
| +# |
| +# The action list is not independently chosen: region-host's |
| +# aws_iam_role_policy.host_buckets (infra/modules/region-host/main.tf) is |
| +# the authority for what this principal shape may do to S3, and this |
| +# policy copies it verbatim — object actions (GetObject/PutObject/ |
| +# DeleteObject) scoped to the object ARNs, list/location actions |
| +# (ListBucket/GetBucketLocation) scoped to the bucket ARNs. |
| +resource "aws_iam_user_policy" "host" { |
| + name = "carlos-remote-host-${var.region_code}" |
| + user = aws_iam_user.host.name |
| + policy = jsonencode({ |
| + Version = "2012-10-17" |
| + Statement = [ |
| + { |
| + Sid = "DeploymentBucketObjects" |
| + Effect = "Allow" |
| + Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] |
| + Resource = ["${var.deployment_bucket_arn}/*"] |
| + }, |
| + { |
| + Sid = "DeploymentBucketList" |
| + Effect = "Allow" |
| + Action = ["s3:ListBucket", "s3:GetBucketLocation"] |
| + Resource = [var.deployment_bucket_arn] |
| + }, |
| + { |
| + Sid = "OwnRegionDataObjects" |
| + Effect = "Allow" |
| + Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] |
| + Resource = ["${aws_s3_bucket.data.arn}/*"] |
| + }, |
| + { |
| + Sid = "OwnRegionDataList" |
| + Effect = "Allow" |
| + Action = ["s3:ListBucket", "s3:GetBucketLocation"] |
| + Resource = [aws_s3_bucket.data.arn] |
| + }, |
| + ] |
| + }) |
| +} |
diff --git a/infra/modules/region-remote-host/outputs.tf b/infra/modules/region-remote-host/outputs.tf| new file mode 100644 |
| index 000000000..4a277965e |
| --- /dev/null |
| +++ b/infra/modules/region-remote-host/outputs.tf |
| @@ -0,0 +1,6 @@ |
| +output "bucket_name" { value = aws_s3_bucket.data.bucket } |
| +output "access_key_id" { value = aws_iam_access_key.host.id } |
| +output "secret_access_key" { |
| + value = aws_iam_access_key.host.secret |
| + sensitive = true |
| +} |
diff --git a/infra/modules/region-remote-host/variables.tf b/infra/modules/region-remote-host/variables.tf| new file mode 100644 |
| index 000000000..8b1240278 |
| --- /dev/null |
| +++ b/infra/modules/region-remote-host/variables.tf |
| @@ -0,0 +1,14 @@ |
| +variable "region_code" { |
| + description = "Short region code this host serves (e.g. pi, hq)." |
| + type = string |
| +} |
| + |
| +variable "data_bucket_name" { |
| + description = "Name of this region's data bucket (created here)." |
| + type = string |
| +} |
| + |
| +variable "deployment_bucket_arn" { |
| + description = "ARN of the existing deployment bucket the host reads releases/status from and writes heartbeats to." |
| + type = string |
| +} |