| 1 | # deployment-buckets: the durable heart of a CARLOS deployment — the |
| 2 | # deployment bucket (apps, releases, channels, instance records, host |
| 3 | # config; routing metadata only, nothing customer-shaped) paired with one |
| 4 | # regional data bucket (parked databases, WAL streams, leases, blobs — |
| 5 | # the residency promise). Boxes are disposable; these buckets are not, |
| 6 | # hence prevent_destroy, versioning, SSE, and a full public-access block |
| 7 | # on both. |
| 8 | |
| 9 | # The deployment bucket: apps, releases, channels, instance records, host |
| 10 | # config. Routing metadata only — nothing customer-shaped rests here. |
| 11 | resource "aws_s3_bucket" "deployment" { |
| 12 | bucket = var.deployment_bucket_name |
| 13 | lifecycle { prevent_destroy = true } |
| 14 | } |
| 15 | |
| 16 | # The regional data bucket: parked databases, WAL streams, leases, blobs. |
| 17 | # This bucket is the residency promise. |
| 18 | resource "aws_s3_bucket" "data" { |
| 19 | bucket = var.data_bucket_name |
| 20 | lifecycle { prevent_destroy = true } |
| 21 | } |
| 22 | |
| 23 | resource "aws_s3_bucket_versioning" "deployment" { |
| 24 | bucket = aws_s3_bucket.deployment.id |
| 25 | versioning_configuration { status = "Enabled" } |
| 26 | } |
| 27 | |
| 28 | # GeoIP databases (platform geoip spec 2026-10-06 §15 step 0, D1): a deleted |
| 29 | # or superseded blob under control/geoip/ must not live on as a noncurrent |
| 30 | # version, because MaxMind's GeoLite EULA requires destroying superseded |
| 31 | # editions. Forwarded db reads (db region forward spec §5.7, J4): every |
| 32 | # object under db-reads/ expires. Each rule is scoped to its own prefix; |
| 33 | # nothing else in this bucket expires. |
| 34 | resource "aws_s3_bucket_lifecycle_configuration" "deployment" { |
| 35 | bucket = aws_s3_bucket.deployment.id |
| 36 | |
| 37 | rule { |
| 38 | id = "geoip-noncurrent" |
| 39 | status = "Enabled" |
| 40 | filter { |
| 41 | prefix = "control/geoip/" |
| 42 | } |
| 43 | noncurrent_version_expiration { |
| 44 | noncurrent_days = 1 |
| 45 | } |
| 46 | abort_incomplete_multipart_upload { |
| 47 | days_after_initiation = 1 |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | # Forwarded db reads (db region forward spec §5.7, J4): every object under |
| 52 | # db-reads/ (requests, queue entries, picked markers, sealed chunks, |
| 53 | # results, cancels) expires a day after it is written, each noncurrent |
| 54 | # version a day after it becomes noncurrent, and expired delete markers |
| 55 | # go. The protocol deletes nothing, so these rules are its only deletion; |
| 56 | # a read's ciphertext lives until they act, typically two to four days. |
| 57 | rule { |
| 58 | id = "db-reads-expire" |
| 59 | status = "Enabled" |
| 60 | filter { |
| 61 | prefix = "db-reads/" |
| 62 | } |
| 63 | expiration { |
| 64 | days = 1 |
| 65 | } |
| 66 | noncurrent_version_expiration { |
| 67 | noncurrent_days = 1 |
| 68 | } |
| 69 | abort_incomplete_multipart_upload { |
| 70 | days_after_initiation = 1 |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | # S3 refuses an expired-object-delete-marker expiration beside days in one |
| 75 | # rule, so the markers have a rule of their own. |
| 76 | rule { |
| 77 | id = "db-reads-delete-markers" |
| 78 | status = "Enabled" |
| 79 | filter { |
| 80 | prefix = "db-reads/" |
| 81 | } |
| 82 | expiration { |
| 83 | expired_object_delete_marker = true |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | depends_on = [aws_s3_bucket_versioning.deployment] |
| 88 | } |
| 89 | |
| 90 | resource "aws_s3_bucket_versioning" "data" { |
| 91 | bucket = aws_s3_bucket.data.id |
| 92 | versioning_configuration { status = "Enabled" } |
| 93 | } |
| 94 | |
| 95 | resource "aws_s3_bucket_public_access_block" "deployment" { |
| 96 | bucket = aws_s3_bucket.deployment.id |
| 97 | block_public_acls = true |
| 98 | ignore_public_acls = true |
| 99 | block_public_policy = true |
| 100 | restrict_public_buckets = true |
| 101 | } |
| 102 | |
| 103 | resource "aws_s3_bucket_public_access_block" "data" { |
| 104 | bucket = aws_s3_bucket.data.id |
| 105 | block_public_acls = true |
| 106 | ignore_public_acls = true |
| 107 | block_public_policy = true |
| 108 | restrict_public_buckets = true |
| 109 | } |
| 110 | |
| 111 | resource "aws_s3_bucket_server_side_encryption_configuration" "deployment" { |
| 112 | bucket = aws_s3_bucket.deployment.id |
| 113 | rule { |
| 114 | apply_server_side_encryption_by_default { sse_algorithm = "AES256" } |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | resource "aws_s3_bucket_server_side_encryption_configuration" "data" { |
| 119 | bucket = aws_s3_bucket.data.id |
| 120 | rule { |
| 121 | apply_server_side_encryption_by_default { sse_algorithm = "AES256" } |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | # --- Box evidence (region HA spec 1 §5.1, §5.9, §6) ------------------------ |
| 126 | # control/boxes/<ref>.json is a box's AWS-signed identity, the evidence its |
| 127 | # enrollment reads. control/box-volumes/, control/box-enrollment/ and |
| 128 | # control/box-fences/ are the volume records, membership confirmations |
| 129 | # and termination fence records regional workers write. Every one is |
| 130 | # written once and never changed or deleted. Storage enforces that, so no |
| 131 | # bug or stolen box credential can rewrite the evidence behind a decision. |
| 132 | # (control/boxes/*.bind.json is no longer written: enrollment replaced the |
| 133 | # operator bind. Its statement stays harmless until operator_role_arns is |
| 134 | # retired with it.) |
| 135 | # |
| 136 | # EVERY statement here names one of these prefixes and nothing wider. A |
| 137 | # broad Deny on this bucket has broken lease releases before. |
| 138 | |
| 139 | locals { |
| 140 | box_evidence_prefixes = ["control/boxes", "control/box-volumes", "control/box-enrollment", "control/box-fences"] |
| 141 | } |
| 142 | |
| 143 | resource "aws_s3_bucket_policy" "deployment_box_records" { |
| 144 | bucket = aws_s3_bucket.deployment.id |
| 145 | depends_on = [aws_s3_bucket_public_access_block.deployment] |
| 146 | policy = jsonencode({ |
| 147 | Version = "2012-10-17" |
| 148 | Statement = [ |
| 149 | { |
| 150 | # Evidence may be created, never replaced. The qualifier leaves |
| 151 | # multipart preparation alone and still covers its completion. |
| 152 | Sid = "BoxRecordsAreCreateOnly" |
| 153 | Effect = "Deny" |
| 154 | Principal = "*" |
| 155 | Action = "s3:PutObject" |
| 156 | Resource = [for p in local.box_evidence_prefixes : "${aws_s3_bucket.deployment.arn}/${p}/*"] |
| 157 | Condition = { |
| 158 | Null = { "s3:if-none-match" = "true" } |
| 159 | Bool = { "s3:ObjectCreationOperation" = "true" } |
| 160 | } |
| 161 | }, |
| 162 | { |
| 163 | Sid = "BoxRecordsAreNeverDeleted" |
| 164 | Effect = "Deny" |
| 165 | Principal = "*" |
| 166 | Action = ["s3:DeleteObject", "s3:DeleteObjectVersion"] |
| 167 | Resource = [for p in local.box_evidence_prefixes : "${aws_s3_bucket.deployment.arn}/${p}/*"] |
| 168 | }, |
| 169 | { |
| 170 | Sid = "OnlyBoxRolesWriteTerminationFences" |
| 171 | Effect = "Deny" |
| 172 | Principal = "*" |
| 173 | Action = "s3:PutObject" |
| 174 | Resource = "${aws_s3_bucket.deployment.arn}/control/box-fences/*" |
| 175 | Condition = { |
| 176 | ArnNotEquals = { |
| 177 | "aws:PrincipalArn" = length(var.box_fence_writer_role_arns) > 0 ? var.box_fence_writer_role_arns : ["arn:aws:iam::000000000000:role/nobody"] |
| 178 | } |
| 179 | } |
| 180 | }, |
| 181 | { |
| 182 | # With no operators named, the list is one ARN nobody has, so the |
| 183 | # Deny covers everyone. |
| 184 | Sid = "OnlyOperatorsWriteBinds" |
| 185 | Effect = "Deny" |
| 186 | Principal = "*" |
| 187 | Action = "s3:PutObject" |
| 188 | Resource = "${aws_s3_bucket.deployment.arn}/control/boxes/*.bind.json" |
| 189 | Condition = { |
| 190 | ArnNotLike = { |
| 191 | "aws:PrincipalArn" = length(var.operator_role_arns) > 0 ? var.operator_role_arns : ["arn:aws:iam::000000000000:role/nobody"] |
| 192 | } |
| 193 | } |
| 194 | }, |
| 195 | ] |
| 196 | }) |
| 197 | } |
| 198 | |