| 1 | # region-remote-host: what an off-cloud CARLOS host (a Raspberry Pi today, |
| 2 | # tito's hq box later) needs from AWS — a region data bucket plus a scoped |
| 3 | # IAM principal, since there is no EC2 instance profile to lean on off-cloud. |
| 4 | # |
| 5 | # Residency is enforced the same way as region-host: the principal reaches |
| 6 | # ITS OWN region's data bucket and nothing else. A wrong-region host fails |
| 7 | # loudly at the IAM layer, same property as every cloud host. The deployment |
| 8 | # bucket is grandfathered-optional (var.deployment_bucket_arn, default "") |
| 9 | # — a fleet-API box needs no platform-bucket grant at all; see the |
| 10 | # aws_iam_user_policy.host_deployment_bucket comment below. |
| 11 | # |
| 12 | # The data bucket follows the deployment-buckets convention for this class |
| 13 | # of bucket (infra/modules/README.md, infra/modules/deployment-buckets/ |
| 14 | # main.tf): boxes are disposable, this bucket is not, hence versioning, |
| 15 | # AES256 SSE, a full public-access block, and prevent_destroy. |
| 16 | |
| 17 | terraform { |
| 18 | required_version = ">= 1.10" |
| 19 | required_providers { |
| 20 | aws = { |
| 21 | source = "hashicorp/aws" |
| 22 | version = ">= 6.0" |
| 23 | } |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | resource "aws_s3_bucket" "data" { |
| 28 | bucket = var.data_bucket_name |
| 29 | lifecycle { prevent_destroy = true } |
| 30 | } |
| 31 | |
| 32 | resource "aws_s3_bucket_versioning" "data" { |
| 33 | bucket = aws_s3_bucket.data.id |
| 34 | versioning_configuration { status = "Enabled" } |
| 35 | } |
| 36 | |
| 37 | resource "aws_s3_bucket_public_access_block" "data" { |
| 38 | bucket = aws_s3_bucket.data.id |
| 39 | block_public_acls = true |
| 40 | block_public_policy = true |
| 41 | ignore_public_acls = true |
| 42 | restrict_public_buckets = true |
| 43 | } |
| 44 | |
| 45 | resource "aws_s3_bucket_server_side_encryption_configuration" "data" { |
| 46 | bucket = aws_s3_bucket.data.id |
| 47 | rule { |
| 48 | apply_server_side_encryption_by_default { sse_algorithm = "AES256" } |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | # The off-cloud host principal. No instance profile exists off EC2; this |
| 53 | # user + access key is the C6 decision-6 shape (IAM Roles Anywhere is the |
| 54 | # later hardening — the policy below is what either principal would carry). |
| 55 | resource "aws_iam_user" "host" { |
| 56 | name = "carlos-remote-host-${var.region_code}" |
| 57 | } |
| 58 | |
| 59 | resource "aws_iam_access_key" "host" { |
| 60 | user = aws_iam_user.host.name |
| 61 | } |
| 62 | |
| 63 | # Scope: THIS region's data bucket, always — wrong-region-fails-at-IAM, same |
| 64 | # property as every cloud host. |
| 65 | # |
| 66 | # The action list is not independently chosen: region-host's |
| 67 | # aws_iam_role_policy.host_buckets (infra/modules/region-host/main.tf) is |
| 68 | # the authority for what this principal shape may do to S3, and this |
| 69 | # policy copies it verbatim — object actions (GetObject/PutObject/ |
| 70 | # DeleteObject) scoped to the object ARNs, list/location actions |
| 71 | # (ListBucket/GetBucketLocation) scoped to the bucket ARNs. |
| 72 | resource "aws_iam_user_policy" "host" { |
| 73 | name = "carlos-remote-host-${var.region_code}" |
| 74 | user = aws_iam_user.host.name |
| 75 | policy = jsonencode({ |
| 76 | Version = "2012-10-17" |
| 77 | # Sibling staging (instance-move spec, issue #331) — same push-only |
| 78 | # moves/* shape as region-host's host_buckets policy; keep the two in |
| 79 | # step (this module's README already carries that rule for the S3 |
| 80 | # action list). |
| 81 | Statement = concat([ |
| 82 | { |
| 83 | Sid = "OwnRegionDataObjects" |
| 84 | Effect = "Allow" |
| 85 | Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] |
| 86 | Resource = ["${aws_s3_bucket.data.arn}/*"] |
| 87 | }, |
| 88 | { |
| 89 | Sid = "OwnRegionDataList" |
| 90 | Effect = "Allow" |
| 91 | Action = ["s3:ListBucket", "s3:GetBucketLocation"] |
| 92 | Resource = [aws_s3_bucket.data.arn] |
| 93 | }, |
| 94 | # Host lifecycle claim, Plan 2 (spec C3, §6.2, §6.5): exact-version |
| 95 | # list, read and delete on this box's OWN data bucket. Sibling-region |
| 96 | # moves/* grants are unchanged: the destination region retires its |
| 97 | # own staging (spec N13). |
| 98 | { |
| 99 | Sid = "PullDoorObjectVersions" |
| 100 | Effect = "Allow" |
| 101 | Action = ["s3:GetObjectVersion"] |
| 102 | Resource = ["${aws_s3_bucket.data.arn}/*"] |
| 103 | }, |
| 104 | { |
| 105 | Effect = "Allow" |
| 106 | Action = ["s3:DeleteObjectVersion"] |
| 107 | Resource = ["${aws_s3_bucket.data.arn}/*"] |
| 108 | }, |
| 109 | { |
| 110 | Sid = "PullDoorVersionList" |
| 111 | Effect = "Allow" |
| 112 | Action = ["s3:ListBucketVersions"] |
| 113 | Resource = [aws_s3_bucket.data.arn] |
| 114 | }, |
| 115 | ], |
| 116 | [for arn in var.sibling_staging_arns : { |
| 117 | Effect = "Allow" |
| 118 | Action = ["s3:PutObject", "s3:DeleteObject"] |
| 119 | Resource = "${arn}/moves/*" |
| 120 | }], |
| 121 | [for arn in var.sibling_staging_arns : { |
| 122 | Effect = "Allow" |
| 123 | Action = ["s3:ListBucket"] |
| 124 | Resource = arn |
| 125 | Condition = { StringLike = { "s3:prefix" = "moves/*" } } |
| 126 | }] |
| 127 | ) |
| 128 | }) |
| 129 | } |
| 130 | |
| 131 | # Deployment-bucket grant: GRANDFATHERED, direct-S3 boxes only. A fleet-API |
| 132 | # box (issue #73) never needs this — it reaches the platform's deployment |
| 133 | # bucket through the console's edge, over the same tunnel token it dials |
| 134 | # with, so var.deployment_bucket_arn is left "" and this whole statement |
| 135 | # (and the IAM policy resource carrying it) is omitted. count, not a dynamic |
| 136 | # block, matches the same optional-statement pattern used by |
| 137 | # aws_iam_role_policy.host_route53 in the region-host module. |
| 138 | resource "aws_iam_user_policy" "host_deployment_bucket" { |
| 139 | count = var.deployment_bucket_arn != "" ? 1 : 0 |
| 140 | name = "carlos-remote-host-${var.region_code}-deployment-bucket" |
| 141 | user = aws_iam_user.host.name |
| 142 | policy = jsonencode({ |
| 143 | Version = "2012-10-17" |
| 144 | Statement = [ |
| 145 | { |
| 146 | Sid = "DeploymentBucketObjects" |
| 147 | Effect = "Allow" |
| 148 | Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] |
| 149 | Resource = ["${var.deployment_bucket_arn}/*"] |
| 150 | }, |
| 151 | { |
| 152 | Sid = "DeploymentBucketList" |
| 153 | Effect = "Allow" |
| 154 | Action = ["s3:ListBucket", "s3:GetBucketLocation"] |
| 155 | Resource = [var.deployment_bucket_arn] |
| 156 | }, |
| 157 | ] |
| 158 | }) |
| 159 | } |
| 160 | |