# ๐Ÿค– The server-release-feed mirror (issue #550 Part 12): a second, boring # home for the signed update feed โ€” manifest.json plus the per-version # binaries. GitHub's asset CDN has served freshly-clobbered manifests stale # (and brand-new binaries 404) for minutes after a publish โ€” documented # operator pain โ€” and that feed is the entire fleet's update path. # deploy/release-server.sh copies every publish/promote here # (LCHAT_RELEASE_MIRROR_BUCKET); updaters fall back to it # (LCHAT_RELEASE_MIRROR_URL). Versioning doubles as the rollback story for a # clobbered manifest.json. # # NOT YET APPLIED โ€” needs a deliberate, named # `AWS_PROFILE=โ€ฆ tofu apply` in infra/prod; plan only until then. resource "aws_s3_bucket" "releases" { bucket = "eleven-releases-${local.account_id}" } resource "aws_s3_bucket_versioning" "releases" { bucket = aws_s3_bucket.releases.id versioning_configuration { status = "Enabled" } } # Manifest clobbers are tiny and binaries land under fresh server-/ keys, # so noncurrent versions exist only as the manifest's rollback window โ€” 90 # days is generous without growing forever. resource "aws_s3_bucket_lifecycle_configuration" "releases" { bucket = aws_s3_bucket.releases.id rule { id = "expire-noncurrent-90d" status = "Enabled" filter {} noncurrent_version_expiration { noncurrent_days = 90 } } } # This bucket is deliberately PUBLIC-READ โ€” the one bucket in the account # where that is the point, against the everything-else-blocked posture # (backup.tf, hibernate.tf). Instances and self-hosted boxes must fetch # updates anonymously, and the content is public, non-secret, and # trust-anchored elsewhere: the manifest is Ed25519-signed against a key baked # into every binary, and binaries are sha256-pinned by that signed manifest, # so the mirror provides availability only โ€” a tampered object can only cause # a refusal, never an install (docs/server-releases.md ยง Trust model). ACLs # stay fully blocked; only the bucket *policy* door is opened, and the # GET-only policy below is the entire public surface (no anonymous List). # Writes stay IAM-only via the release-publisher user. resource "aws_s3_bucket_public_access_block" "releases" { bucket = aws_s3_bucket.releases.id block_public_acls = true ignore_public_acls = true block_public_policy = false restrict_public_buckets = false } data "aws_iam_policy_document" "releases_public_read" { statement { sid = "PublicReadReleaseObjects" actions = ["s3:GetObject"] resources = ["${aws_s3_bucket.releases.arn}/*"] principals { type = "*" identifiers = ["*"] } } # Public means anonymous, not unencrypted: deny any request over plain # HTTP. Updaters fetch the mirror via its https URL already (the manifest # is Ed25519-signed regardless โ€” TLS here is defence in depth, not the # trust anchor). statement { sid = "DenyInsecureTransport" effect = "Deny" actions = ["s3:*"] resources = [ aws_s3_bucket.releases.arn, "${aws_s3_bucket.releases.arn}/*", ] principals { type = "*" identifiers = ["*"] } condition { test = "Bool" variable = "aws:SecureTransport" values = ["false"] } } } resource "aws_s3_bucket_policy" "releases" { bucket = aws_s3_bucket.releases.id policy = data.aws_iam_policy_document.releases_public_read.json # The policy grants public access, so the access block must relax # block_public_policy before it can be attached. depends_on = [aws_s3_bucket_public_access_block.releases] } # The publisher's credentials: object put + list on this bucket, nothing else # in the account โ€” and no DeleteObject, so a leaked publisher key can # overwrite the current manifest (noncurrent versions survive, versioning is # the rollback) but never destroy history. Mint the access key by hand # (aws iam create-access-key --user-name release-publisher) โ€” keys never land # in tofu state. resource "aws_iam_user" "release_publisher" { name = "release-publisher" } data "aws_iam_policy_document" "release_publisher" { statement { sid = "ListReleasesBucket" actions = ["s3:ListBucket"] resources = [aws_s3_bucket.releases.arn] } statement { sid = "WriteReleaseObjects" actions = [ "s3:GetObject", "s3:PutObject", ] resources = ["${aws_s3_bucket.releases.arn}/*"] } } resource "aws_iam_user_policy" "release_publisher" { name = "release-publisher-bucket" user = aws_iam_user.release_publisher.name policy = data.aws_iam_policy_document.release_publisher.json } output "releases_bucket" { value = aws_s3_bucket.releases.bucket } # What instances set as LCHAT_RELEASE_MIRROR_URL. output "releases_mirror_url" { value = "https://${aws_s3_bucket.releases.bucket_regional_domain_name}/releases" }