Caddy adds Strict-Transport-Security to every response on the forge's hostname, and to its pages (not /sites/ or /previews/, which are anyone's) X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin and a CSP of frame-ancestors 'self', base-uri 'none' and object-src 'none'. Each is set only if the response has none, so the sign-in and private pages keep their stricter ones. Nothing scripts, styles, fonts or wasm load is restricted.
- commit
- fe26ac5eb0af68c410079aa849dc38bb8da598c4
- parent
- ebe809d2761d6e5fae0ff25ad968cf28f64399a2
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 116s, signed by cat
- kind
- feat risk score 1.5 of 3, Jev's estimate
- review
- #29 Security: go get's reflected markup, and headers on the forge's pages
ci attestation
forge-attestation 1 type ci commit fe26ac5eb0af68c410079aa849dc38bb8da598c4 tree 488519c8dc7176e0dc2e5d0dee9d07b1c4b1ee61 script .forge/ci 8d2000a416f8160bcf710d98b72af9eb76287f58 result pass exit 0 started 2026-10-19T07:39:55Z duration 116s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:37db71267a7f5a396a5929d1ad5f983ffe46ddee536bcbc35c57d393f2f4ba91 312 lines |pushed wip/i18n |44fe96f running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |44fe96f pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |4b9d238 running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |4b9d238 fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |eb217fb running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |eb217fb 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 d466c56 -> /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 |2e6afba running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |2e6afba pass in 0s, signed |pushed main |0ec6b30 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |0ec6b30 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 6f84fc0 -> /sites/dami/forge/guide/ |(node:86508) 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.tHfp5E/wt/demo/root/.forge/www |(node:86668) 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.tHfp5E/wt/demo/root/.forge/www |(node:86841) 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.tHfp5E/wt/demo/root/.forge/www |(node:87007) 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 2s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.tHfp5E/wt/demo/root/.forge/www |(node:87712) 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 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.tHfp5E/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 | 2 | +- |
| M | bin/ | 13 | +++++++++++++ |
| A | test/ | 30 | ++++++++++++++++++++++++++++++ |
@@ -97,7 +97,7 @@ review page, signed in ─── HTTPS ────────────> the
9797 | Process | Why |9898 | --- | --- |9999 | sshd | git over ssh. Every key is forced through `forge shell <name>`. |100-| Caddy | Serves `.forge/www` as plain files, read-only HTTPS clones (`git http-backend` through fcgiwrap; nothing else inside a bare repo is served; a private repo takes a token) and `go get` meta tags; proxies `/events/`, every path `www` has no file for (to the private server) and, with a pages domain, sites. |100+| Caddy | Serves `.forge/www` as plain files, read-only HTTPS clones (`git http-backend` through fcgiwrap; nothing else inside a bare repo is served; a private repo takes a token) and `go get` meta tags, with security headers (HSTS, nosniff, a referrer policy and a CSP against framing) on the forge's own pages; proxies `/events/`, every path `www` has no file for (to the private server) and, with a pages domain, sites. |101101 | `forge build` | Runs 11ty after each push, in the background and one repo at a time, then swaps the new pages into `www/`. Unchanged pages are hard links to the last build's. First it adds the push's commits to the repo's commit-graph (see [Cleaning up](#cleaning-up)). |102102 | `lib/events-server.ts` | Tails `.forge/events.jsonl`, which hooks and builds append to, and streams it as server-sent events at `/events/<owner>/<repo>/`. Open pages say who pushed and swap in fresh content when the build finishes. Without it pages still work; their status line says offline. A private repo streams only to its readers, signed in. |103103 | `lib/private-server.ts` | Serves private repos' pages to their readers, signed in with a passkey or holding a token, the sign-in pages (`/login/`, `/private/`), and the 404 of every page that isn't there; takes comments and approvals posted from review pages (see [Comments and approvals](#comments-and-approvals)); answers Caddy whether a token may clone a private repo over HTTPS (see [Private pages](#private-pages)). |@@ -1076,6 +1076,19 @@ cmd_server_caddyfile() {
10761076 [ -n "$gohost" ] || gohost={host}10771077 cat <<EOF10781078 $addr {1079+ # The forge's own pages (not sites, which are anyone's and keep their1080+ # own) say not to sniff types, not to be framed elsewhere, and to send1081+ # only the origin to other sites. A response that sets one of these1082+ # itself (the private server's sign-in pages) keeps its own. HSTS is1083+ # the hostname's, so every response has it; browsers ignore it over1084+ # plain HTTP.1085+ header ?Strict-Transport-Security "max-age=31536000"1086+ @chrome not path /sites/* /previews/*1087+ header @chrome {1088+ ?X-Content-Type-Options nosniff1089+ ?Referrer-Policy strict-origin-when-cross-origin1090+ ?Content-Security-Policy "frame-ancestors 'self'; base-uri 'none'; object-src 'none'"1091+ }10791092 # Compress pages and assets, never the event stream (it would buffer),10801093 # code.db, which code search reads in byte ranges of the file as stored,10811094 # or release downloads, which are mostly compressed already.@@ -0,0 +1,30 @@
1+# The forge's own pages, through Caddy, carry its security headers: HSTS,2+# no sniffing, the origin alone as referrer, and a CSP that keeps them out3+# of other sites' frames. A site (anyone's pages) gets only HSTS, which is4+# the hostname's, and the sign-in page keeps its own stricter policy.5+. "$(dirname "$0")/../lib.sh"6+user dami7+repo dami/greet8+clone dami dami/greet "$T/w/d"9+at "$T/w/d" commit "first"10+push_as dami "$T/w/d" origin main11+mkdir -p "$T/site" && printf '<h1>docs</h1>\n' >"$T/site/index.html"12+FORGE_USER=dami ok at "$T/w/d" "$FORGE" site push "$T/site" docs13+build14+caddy_server15+for p in / /dami/greet/ /dami/nope/; do16+ http -s -i "$C$p" | tr -d '\r' >"$T/res"17+ has "$T/res" '^Strict-Transport-Security: max-age=31536000$'18+ has "$T/res" '^X-Content-Type-Options: nosniff$'19+ has "$T/res" '^Referrer-Policy: strict-origin-when-cross-origin$'20+ has "$T/res" "^Content-Security-Policy: frame-ancestors 'self'; base-uri 'none'; object-src 'none'$"21+done22+http -s -i "$C/sites/dami/greet/docs/" | tr -d '\r' >"$T/site.res"23+has "$T/site.res" '^HTTP/1.1 200 '24+has "$T/site.res" '<h1>docs</h1>'25+has "$T/site.res" '^Strict-Transport-Security: '26+lacks "$T/site.res" '^(Content-Security-Policy|X-Content-Type-Options|Referrer-Policy):'27+http -s -i "$C/login/" | tr -d '\r' >"$T/login"28+eq "$(grep -c '^Content-Security-Policy:' "$T/login")" 129+has "$T/login" "^Content-Security-Policy: default-src 'self'.*frame-ancestors 'none'"30+has "$T/login" '^Referrer-Policy: same-origin$'