eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– The public status bucket (issue #550 Parts 11/12): world-readable
2# dashboards β€” fleet.json + books.json (published every 5 minutes by
3# deploy/status-publish.sh on the router box) and the two static viewer pages
4# (web/status/). Everything in it is AGGREGATE BY DESIGN: fleet counts and
5# version distribution with no hostnames, and Stripe income as totals only β€”
6# decision #19 (income publishes aggregated, never per-payer; expenses are the
7# deliberate mirror image and publish itemized per provider) and invariant 12
8# (no PII in our stores) are enforced upstream in the generator; this bucket
9# only ever receives what passed that gate. docs/status.md is the guide.
10#
11# NOT YET APPLIED β€” needs a deliberate, named
12# `AWS_PROFILE=… tofu apply` in infra/prod; plan only until then.
13
14resource "aws_s3_bucket" "status" {
15 bucket = "eleven-status-${local.account_id}"
16}
17
18resource "aws_s3_bucket_versioning" "status" {
19 bucket = aws_s3_bucket.status.id
20
21 versioning_configuration {
22 status = "Enabled"
23 }
24}
25
26# The JSONs are clobbered every 5 minutes (β‰ˆ576 noncurrent versions/day), so
27# versions are pure rollback/forensic noise past a short window β€” 7 days is
28# plenty to reconstruct "what did the page say when".
29resource "aws_s3_bucket_lifecycle_configuration" "status" {
30 bucket = aws_s3_bucket.status.id
31
32 rule {
33 id = "expire-noncurrent-7d"
34 status = "Enabled"
35
36 filter {}
37
38 noncurrent_version_expiration {
39 noncurrent_days = 7
40 }
41 }
42}
43
44# Deliberately PUBLIC-READ, like the releases bucket (releases.tf) and against
45# the everything-else-blocked posture (backup.tf, hibernate.tf): a public
46# status page is the point β€” the content is aggregates that were reviewed for
47# publication before the generator ever wrote them (no hostnames, no per-payer
48# data, no PII exists upstream to leak). ACLs stay fully blocked; only the
49# bucket *policy* door is opened, and the GET-only policy below is the entire
50# public surface (no anonymous List β€” object names are known, enumeration
51# isn't offered).
52resource "aws_s3_bucket_public_access_block" "status" {
53 bucket = aws_s3_bucket.status.id
54
55 block_public_acls = true
56 ignore_public_acls = true
57 block_public_policy = false
58 restrict_public_buckets = false
59}
60
61data "aws_iam_policy_document" "status_public_read" {
62 statement {
63 sid = "PublicReadStatusObjects"
64 actions = ["s3:GetObject"]
65 resources = ["${aws_s3_bucket.status.arn}/*"]
66
67 principals {
68 type = "*"
69 identifiers = ["*"]
70 }
71 }
72
73 # Public means anonymous, not unencrypted: deny any request over plain
74 # HTTP. The viewer pages and the publisher both use the https endpoint
75 # already; this closes the door on downgraded fetches.
76 statement {
77 sid = "DenyInsecureTransport"
78 effect = "Deny"
79 actions = ["s3:*"]
80 resources = [
81 aws_s3_bucket.status.arn,
82 "${aws_s3_bucket.status.arn}/*",
83 ]
84
85 principals {
86 type = "*"
87 identifiers = ["*"]
88 }
89
90 condition {
91 test = "Bool"
92 variable = "aws:SecureTransport"
93 values = ["false"]
94 }
95 }
96}
97
98resource "aws_s3_bucket_policy" "status" {
99 bucket = aws_s3_bucket.status.id
100 policy = data.aws_iam_policy_document.status_public_read.json
101
102 # The policy grants public access, so the access block must relax
103 # block_public_policy before it can be attached.
104 depends_on = [aws_s3_bucket_public_access_block.status]
105}
106
107# The publisher's credentials: object put/get + list on this bucket plus
108# read-only Cost Explorer, nothing else in the account β€” and no DeleteObject,
109# so a leaked publisher key can overwrite the current dashboards (noncurrent
110# versions survive; versioning is the rollback) but never destroy history.
111# Mint the access key by hand
112# (aws iam create-access-key --user-name status-publisher) β€” keys never land
113# in tofu state; they go in /data/status.env on the router box.
114resource "aws_iam_user" "status_publisher" {
115 name = "status-publisher"
116}
117
118data "aws_iam_policy_document" "status_publisher" {
119 statement {
120 sid = "ListStatusBucket"
121 actions = ["s3:ListBucket"]
122 resources = [aws_s3_bucket.status.arn]
123 }
124
125 statement {
126 sid = "WriteStatusObjects"
127 actions = [
128 "s3:GetObject",
129 "s3:PutObject",
130 ]
131 resources = ["${aws_s3_bucket.status.arn}/*"]
132 }
133
134 # Read-only access to the mesh's stats side-channel in the hibernation
135 # bucket (hibernate.tf's prod_instances) β€” deploy/status-publish.sh's
136 # mesh_peer_boxes reads every live mesh peer's self-published capacity
137 # snapshot (band words + counts only β€” never raw ratios, meshstats.go) to
138 # build fleet.json's multi-host hosts.boxes array. Scoped to the "*-stats/"
139 # sibling prefix internal/activator/heartbeat.go's PutStats writes to, not
140 # the whole bucket: the publisher must never be able to read or restore
141 # actual (ciphertext) instance replicas, only the small stats objects next
142 # to them. No s3:PutObject/DeleteObject here β€” this credential only reads.
143 statement {
144 sid = "ListMeshStatsPrefix"
145 actions = ["s3:ListBucket"]
146 resources = [aws_s3_bucket.prod_instances.arn]
147 condition {
148 test = "StringLike"
149 variable = "s3:prefix"
150 values = ["*-stats/*"]
151 }
152 }
153
154 statement {
155 sid = "ReadMeshStatsObjects"
156 actions = ["s3:GetObject"]
157 resources = ["${aws_s3_bucket.prod_instances.arn}/*-stats/*"]
158 }
159
160 # The books page's expenses section: the publisher asks Cost Explorer for
161 # the account's month-to-date spend (deploy/status-publish.sh, cached β€” CE
162 # bills per request). ce:GetCostAndUsage has no resource-level scoping, so
163 # "*" is the narrowest possible grant; it exposes only read-only cost
164 # metadata, which is exactly what we deliberately publish anyway (decision
165 # #19: expenses itemize).
166 statement {
167 sid = "ReadCostAndUsage"
168 actions = ["ce:GetCostAndUsage"]
169 resources = ["*"]
170 }
171}
172
173resource "aws_iam_user_policy" "status_publisher" {
174 name = "status-publisher-bucket"
175 user = aws_iam_user.status_publisher.name
176 policy = data.aws_iam_policy_document.status_publisher.json
177}
178
179output "status_bucket" {
180 value = aws_s3_bucket.status.bucket
181}
182
183# Where the dashboards live once published.
184output "status_url" {
185 value = "https://${aws_s3_bucket.status.bucket_regional_domain_name}/index.html"
186}
187
188# ---- ledger streams (issue #1224) ---------------------------------------------
189# The append-only, hash-chained ledger streams under ledgers/<name>/ β€” writes
190# stream live from contributor machines (tools/ledger-append.mjs /
191# tools/music-ledger.mjs --stream) instead of riding the release train; the
192# viewer pages read them through the same public GET door as everything else
193# (no anonymous List β€” pages fetch HEAD.json + derived views by known key,
194# and tools/ledger-verify.mjs walks the prev-hash chain, so enumeration stays
195# unoffered). Tamper-EVIDENT, not tamper-proof: the chain (plus a future
196# anchor of the head hash into the public releases repo) is the guarantee;
197# this credential simply can't delete, and versioning keeps a short forensic
198# window like the rest of the bucket.
199# Mint the access key by hand (aws iam create-access-key --user-name
200# status-ledger-writer) β€” keys never land in tofu state; store it in
201# Bitwarden per deploy/vault-items.sh and hand it only to contributor
202# machines that stream ledgers.
203resource "aws_iam_user" "ledger_writer" {
204 name = "status-ledger-writer"
205}
206
207data "aws_iam_policy_document" "ledger_writer" {
208 # PutObject only, and only under ledgers/ β€” this key can append entries,
209 # blobs, and derived views, and nothing else: no reads beyond the public
210 # door everyone has, no deletes, no touching the dashboards' own JSONs.
211 statement {
212 sid = "AppendLedgerObjects"
213 actions = ["s3:PutObject"]
214 resources = ["${aws_s3_bucket.status.arn}/ledgers/*"]
215 }
216}
217
218resource "aws_iam_user_policy" "ledger_writer" {
219 name = "status-ledger-writer-append"
220 user = aws_iam_user.ledger_writer.name
221 policy = data.aws_iam_policy_document.ledger_writer.json
222}
223