With the Fly secret FORGE_MIRROR_TOKEN set, the edge sends it as X-Forge-Mirror-Token on each request for the offline copy, so a Cloudflare WAF rule in front of the bucket can later refuse requests without it. Without the secret the header is empty. Scenario: the bucket sees the token, and the reply doesn't carry it.
- commit
- 3e6ffab7b6c67950bc8c4dc702a6e0d275ddd489
- parent
- 835172a22647d43ff8752262a9148c26cf5b2e5e
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 120s, signed by cat
- kind
- feat risk score 1.0 of 3, Jev's estimate
- review
- #30 Security: sign-in, invites, owner pages, lake, caps and the edge
ci attestation
forge-attestation 1 type ci commit 3e6ffab7b6c67950bc8c4dc702a6e0d275ddd489 tree f3797edcbabad265b5cb33bb718374fc7d63c6dc script .forge/ci 8d2000a416f8160bcf710d98b72af9eb76287f58 result pass exit 0 started 2026-10-19T08:00:12Z duration 120s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:bf5a81f934e4aee612b2a8504022d153b14c9096de9b80a50f6ada0efeab7c80 325 lines |pushed wip/i18n |e86ab4e running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |e86ab4e pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |78b1eb6 running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |78b1eb6 fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |b2b9445 running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |b2b9445 pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed 4f60fa6 -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |48aef83 running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |48aef83 pass in 0s, signed |pushed main |ff22780 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |ff22780 pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/ |site guide: deployed 58aa462 -> /sites/dami/forge/guide/ |(node:54203) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.GT5BJv/wt/demo/root/.forge/www |(node:54377) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.GT5BJv/wt/demo/root/.forge/www |(node:54564) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.GT5BJv/wt/demo/root/.forge/www |(node:54744) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.GT5BJv/wt/demo/root/.forge/www |(node:55460) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.GT5BJv/wt/demo/root/.forge/www |235 of 235 pages render the same without 11ty |guide: 8 files, links ok |ok
Signature: good, cat
| M | README.md | 4 | +++- |
| M | deploy/ | 4 | ++++ |
| M | deploy/ | 3 | +++ |
| M | deploy/ | 2 | ++ |
| A | test/ | 33 | ++++++++++++++++++++++++++++++ |
@@ -954,7 +954,9 @@ updates and pushes need slate; they get a 503, or for ssh no connection,
954954 until it's back. Caddy checks slate every 10 seconds, so only the first955955 request after it goes away waits. `deploy/edge/fly.toml` sets `FORGE_DOMAIN`956956 (only that host falls back) and `FORGE_MIRROR_URL`, the mirror's public URL;957-without it there is no fallback.957+without it there is no fallback. With the Fly secret `FORGE_MIRROR_TOKEN`958+set, the edge sends it to the bucket as `X-Forge-Mirror-Token`, so a rule in959+front of the bucket can refuse requests without it.958960 959961 On slate, everything lives in `/srv/forge` (`src/`, `data/`, `forge.env`):960962 @@ -8,6 +8,9 @@
88 # FORGE_DOMAIN the forge's hostname, e.g. src.rest; only it falls back99 # FORGE_MIRROR_URL the mirror bucket's public URL, e.g. https://mirror.src.rest;1010 # without it there is no fallback11+# FORGE_MIRROR_TOKEN (a Fly secret, optional) sent to the bucket as12+# X-Forge-Mirror-Token, so a rule in front of it (a13+# Cloudflare WAF rule) can turn away requests without it1114 {1215 admin off1316 auto_https off@@ -64,6 +67,7 @@
6467 rewrite @file {path}?6568 reverse_proxy {$FORGE_MIRROR_URL:https://mirror.invalid} {6669 header_up Host {upstream_hostport}70+ header_up X-Forge-Mirror-Token "{$FORGE_MIRROR_TOKEN:}"6771 header_down -Set-Cookie6872 # Not in the copy: a page that isn't there, or one the copy6973 # leaves out (a file's or a folder's; lib/mirror.ts). The@@ -5,6 +5,9 @@
55 # fly certs add src.rest && fly certs add pages.src.rest66 # While slate is down, pages come from the R2 mirror (Caddyfile): set77 # FORGE_MIRROR_URL below to the mirror bucket's public URL. ssh just fails.8+# With fly secrets set FORGE_MIRROR_TOKEN=..., the edge sends that as9+# X-Forge-Mirror-Token to the bucket, so a WAF rule there can refuse10+# requests without it.811 app = "dami-forge"912 primary_region = "dfw"1013 @@ -2,6 +2,8 @@
22 # TAILCAT_CLIENT_KEY (secret): this edge's client key file contents.33 # FORGE_TAILCAT_ADDR (secret): the forge container's tailcat address.44 # FORGE_DOMAIN, FORGE_MIRROR_URL (fly.toml env): the offline copy (Caddyfile).5+# FORGE_MIRROR_TOKEN (secret, optional): sent to the mirror bucket with each6+# request for the offline copy, for a rule in front of the bucket to require.57 set -eu68 : "${TAILCAT_CLIENT_KEY:?set the edge client key secret}"79 : "${FORGE_TAILCAT_ADDR:?set the forge tailcat address secret}"@@ -0,0 +1,33 @@
1+# With FORGE_MIRROR_TOKEN set (a Fly secret), the edge sends it to the2+# mirror bucket as X-Forge-Mirror-Token on each request for the offline3+# copy, so a rule in front of the bucket can refuse requests without it.4+. "$(dirname "$0")/../lib.sh"5+command -v caddy >/dev/null 2>&1 || { note "caddy is not installed: skipped running it"; exit 0; }6+# The bucket: answers every request, and writes down the token it came7+# with. It takes a port of its own and says which.8+node -e '9+const fs = require("fs");10+const s = require("http").createServer((req, res) => {11+ fs.appendFileSync(process.argv[1], `${req.url} ${req.headers["x-forge-mirror-token"] ?? "-"}\n`);12+ res.writeHead(200, { "Content-Type": "text/html" }).end("<p>copy</p>");13+}).listen(0, "127.0.0.1", () => fs.writeFileSync(process.argv[2], String(s.address().port)));14+' "$T/seen" "$T/mport" &15+T_PIDS="$T_PIDS $!"16+n=017+until [ -s "$T/mport" ] || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done18+mport=$(cat "$T/mport")19+eport=$(free_port)20+dead=$(free_port)21+# On 127.0.0.1 only, so no other scenario's server can take the port there.22+sed "s/^:8080 {/:$eport {/; s/127\.0\.0\.1:8081/127.0.0.1:$dead/" "$T_HOME/deploy/edge/Caddyfile" |23+ awk '{ print } $0 == "\tadmin off" { print "\tdefault_bind 127.0.0.1" }' >"$T/Caddyfile.edge"24+FORGE_DOMAIN=src.test FORGE_MIRROR_URL=http://127.0.0.1:$mport FORGE_MIRROR_TOKEN=s3cret-token XDG_DATA_HOME=$T/xdg XDG_CONFIG_HOME=$T/xdg \25+ caddy run --config "$T/Caddyfile.edge" --adapter caddyfile >"$T/caddy.edge.log" 2>&1 &26+T_PIDS="$T_PIDS $!"27+wait_http "http://127.0.0.1:$mport/"28+wait_http "http://127.0.0.1:$eport/"29+http -s -i -H 'Host: src.test' "http://127.0.0.1:$eport/dami/greet/" | tr -d '\r' >"$T/res"30+has "$T/res" '^X-Forge-Offline: mirror'31+has "$T/res" '<p>copy</p>'32+has "$T/seen" '^/dami/greet/index\.html s3cret-token$'33+lacks "$T/res" 's3cret-token'