# deployment-buckets: the durable heart of a CARLOS deployment — the # deployment bucket (apps, releases, channels, instance records, host # config; routing metadata only, nothing customer-shaped) paired with one # regional data bucket (parked databases, WAL streams, leases, blobs — # the residency promise). Boxes are disposable; these buckets are not, # hence prevent_destroy, versioning, SSE, and a full public-access block # on both. # The deployment bucket: apps, releases, channels, instance records, host # config. Routing metadata only — nothing customer-shaped rests here. resource "aws_s3_bucket" "deployment" { bucket = var.deployment_bucket_name lifecycle { prevent_destroy = true } } # The regional data bucket: parked databases, WAL streams, leases, blobs. # This bucket is the residency promise. resource "aws_s3_bucket" "data" { bucket = var.data_bucket_name lifecycle { prevent_destroy = true } } resource "aws_s3_bucket_versioning" "deployment" { bucket = aws_s3_bucket.deployment.id versioning_configuration { status = "Enabled" } } # GeoIP databases (platform geoip spec 2026-10-06 §15 step 0, D1): a deleted # or superseded blob under control/geoip/ must not live on as a noncurrent # version, because MaxMind's GeoLite EULA requires destroying superseded # editions. Forwarded db reads (db region forward spec §5.7, J4): every # object under db-reads/ expires. Each rule is scoped to its own prefix; # nothing else in this bucket expires. resource "aws_s3_bucket_lifecycle_configuration" "deployment" { bucket = aws_s3_bucket.deployment.id rule { id = "geoip-noncurrent" status = "Enabled" filter { prefix = "control/geoip/" } noncurrent_version_expiration { noncurrent_days = 1 } abort_incomplete_multipart_upload { days_after_initiation = 1 } } # Forwarded db reads (db region forward spec §5.7, J4): every object under # db-reads/ (requests, queue entries, picked markers, sealed chunks, # results, cancels) expires a day after it is written, each noncurrent # version a day after it becomes noncurrent, and expired delete markers # go. The protocol deletes nothing, so these rules are its only deletion; # a read's ciphertext lives until they act, typically two to four days. rule { id = "db-reads-expire" status = "Enabled" filter { prefix = "db-reads/" } expiration { days = 1 } noncurrent_version_expiration { noncurrent_days = 1 } abort_incomplete_multipart_upload { days_after_initiation = 1 } } # S3 refuses an expired-object-delete-marker expiration beside days in one # rule, so the markers have a rule of their own. rule { id = "db-reads-delete-markers" status = "Enabled" filter { prefix = "db-reads/" } expiration { expired_object_delete_marker = true } } depends_on = [aws_s3_bucket_versioning.deployment] } resource "aws_s3_bucket_versioning" "data" { bucket = aws_s3_bucket.data.id versioning_configuration { status = "Enabled" } } # Old versions in the data bucket expire (platform #336). The bucket is # versioned and litestream rewrites and compacts its files constantly, so # without this every replaced or deleted object lived on as a noncurrent # version forever: on 2026-10-24 one flagship data bucket held 14.5 GB of # current objects and was billed for 1,657 GB. A noncurrent version expires # data_noncurrent_days after it is replaced (7 by default, the restore # window), delete markers with nothing behind them go, and abandoned # multipart uploads go after 7 days. Current objects never expire. resource "aws_s3_bucket_lifecycle_configuration" "data" { bucket = aws_s3_bucket.data.id rule { id = "expire-noncurrent" status = "Enabled" filter {} noncurrent_version_expiration { noncurrent_days = var.data_noncurrent_days } expiration { expired_object_delete_marker = true } abort_incomplete_multipart_upload { days_after_initiation = 7 } } depends_on = [aws_s3_bucket_versioning.data] } resource "aws_s3_bucket_public_access_block" "deployment" { bucket = aws_s3_bucket.deployment.id block_public_acls = true ignore_public_acls = true block_public_policy = true restrict_public_buckets = true } resource "aws_s3_bucket_public_access_block" "data" { bucket = aws_s3_bucket.data.id block_public_acls = true ignore_public_acls = true block_public_policy = true restrict_public_buckets = true } resource "aws_s3_bucket_server_side_encryption_configuration" "deployment" { bucket = aws_s3_bucket.deployment.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } resource "aws_s3_bucket_server_side_encryption_configuration" "data" { bucket = aws_s3_bucket.data.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } # --- Box evidence (region HA spec 1 §5.1, §5.9, §6) ------------------------ # control/boxes/.json is a box's AWS-signed identity, the evidence its # enrollment reads. control/box-volumes/, control/box-enrollment/ and # control/box-fences/ are the volume records, membership confirmations # and termination fence records regional workers write. Every one is # written once and never changed or deleted. Storage enforces that, so no # bug or stolen box credential can rewrite the evidence behind a decision. # (control/boxes/*.bind.json is no longer written: enrollment replaced the # operator bind. Its statement stays harmless until operator_role_arns is # retired with it.) # # EVERY statement here names one of these prefixes and nothing wider. A # broad Deny on this bucket has broken lease releases before. locals { box_evidence_prefixes = ["control/boxes", "control/box-volumes", "control/box-enrollment", "control/box-fences"] } resource "aws_s3_bucket_policy" "deployment_box_records" { bucket = aws_s3_bucket.deployment.id depends_on = [aws_s3_bucket_public_access_block.deployment] policy = jsonencode({ Version = "2012-10-17" Statement = [ { # Evidence may be created, never replaced. The qualifier leaves # multipart preparation alone and still covers its completion. Sid = "BoxRecordsAreCreateOnly" Effect = "Deny" Principal = "*" Action = "s3:PutObject" Resource = [for p in local.box_evidence_prefixes : "${aws_s3_bucket.deployment.arn}/${p}/*"] Condition = { Null = { "s3:if-none-match" = "true" } Bool = { "s3:ObjectCreationOperation" = "true" } } }, { Sid = "BoxRecordsAreNeverDeleted" Effect = "Deny" Principal = "*" Action = ["s3:DeleteObject", "s3:DeleteObjectVersion"] Resource = [for p in local.box_evidence_prefixes : "${aws_s3_bucket.deployment.arn}/${p}/*"] }, { Sid = "OnlyBoxRolesWriteTerminationFences" Effect = "Deny" Principal = "*" Action = "s3:PutObject" Resource = "${aws_s3_bucket.deployment.arn}/control/box-fences/*" Condition = { ArnNotEquals = { "aws:PrincipalArn" = length(var.box_fence_writer_role_arns) > 0 ? var.box_fence_writer_role_arns : ["arn:aws:iam::000000000000:role/nobody"] } } }, { # With no operators named, the list is one ARN nobody has, so the # Deny covers everyone. Sid = "OnlyOperatorsWriteBinds" Effect = "Deny" Principal = "*" Action = "s3:PutObject" Resource = "${aws_s3_bucket.deployment.arn}/control/boxes/*.bind.json" Condition = { ArnNotLike = { "aws:PrincipalArn" = length(var.operator_role_arns) > 0 ? var.operator_role_arns : ["arn:aws:iam::000000000000:role/nobody"] } } }, ] }) }