Skip to main content

max / makenotwork

6.9 KB · 224 lines History Blame Raw
1 # Makenotwork Caddy Configuration
2 # Place in /etc/caddy/Caddyfile on the server
3 #
4 # TLS: Cloudflare Origin CA cert (wildcard *.makenot.work + makenot.work)
5 # All HTTPS traffic routed through Cloudflare proxy (origin IP hidden).
6 # Authenticated Origin Pulls: only Cloudflare can reach the origin.
7 # git.makenot.work redirects browser visits to the web UI.
8 # SSH clone uses ssh.makenot.work (proxy OFF in Cloudflare).
9 #
10 # Custom domains: on-demand TLS via Let's Encrypt (ACME HTTP-01).
11 # The ask endpoint validates that the domain is verified before issuing a cert.
12 # makenot.work subdomains remain protected by Cloudflare mTLS even with ports open.
13
14 {
15 on_demand_tls {
16 ask http://localhost:3000/api/domains/caddy-ask
17 }
18 }
19
20 # Shared TLS config: Origin CA cert + Authenticated Origin Pulls (mTLS)
21 (cloudflare_tls) {
22 tls /etc/caddy/cloudflare-origin.pem /etc/caddy/cloudflare-origin-key.pem {
23 client_auth {
24 mode require_and_verify
25 trusted_ca_cert_file /etc/caddy/cloudflare-authenticated-origin-pull-ca.pem
26 }
27 }
28 }
29
30 makenot.work {
31 import cloudflare_tls
32
33 # Block internal API from external access (CLI uses localhost directly)
34 @internal path /api/internal/*
35 respond @internal 404
36
37 # Reverse proxy to application (includes /docs routes)
38 reverse_proxy localhost:3000
39
40 # Security headers (CSP is set by the app — do not duplicate here)
41 header {
42 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
43 }
44
45 # Static error pages when app is down
46 handle_errors {
47 @404 expression {err.status_code} == 404
48 handle @404 {
49 root * /opt/makenotwork/error-pages
50 rewrite * /404.html
51 file_server
52 }
53 @500 expression {err.status_code} == 500
54 handle @500 {
55 root * /opt/makenotwork/error-pages
56 rewrite * /500.html
57 file_server
58 }
59 handle {
60 root * /opt/makenotwork/error-pages
61 rewrite * /502.html
62 file_server
63 }
64 }
65
66 encode gzip zstd
67
68 log {
69 output file /var/log/caddy/makenotwork.log
70 format json
71 }
72 }
73
74 # Creator custom pages (u.makenot.work).
75 # Same app process as the apex — the server's host-dispatch middleware renders
76 # sanitized creator HTML/CSS here, keeps this origin cookieless, and sets its own
77 # strict CSP (do not duplicate CSP here). TLS is the wildcard *.makenot.work
78 # Origin CA cert via cloudflare_tls.
79 #
80 # This explicit block is REQUIRED: without it, u.makenot.work falls into the
81 # on-demand-TLS catch-all (:443) below, whose ask endpoint refuses a cert
82 # because u.makenot.work is not a verified custom domain.
83 #
84 # Requires a Cloudflare DNS record for u.makenot.work (proxy ON), or coverage by
85 # a *.makenot.work wildcard DNS record.
86 u.makenot.work {
87 import cloudflare_tls
88
89 # Internal API is unreachable here anyway (host dispatch only serves custom
90 # pages + /static), but block it explicitly as defense in depth.
91 @internal path /api/internal/*
92 respond @internal 404
93
94 reverse_proxy localhost:3000
95
96 header {
97 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
98 }
99
100 encode gzip zstd
101
102 log {
103 output file /var/log/caddy/userpages.log
104 format json
105 }
106 }
107
108 # Multithreaded forum
109 forums.makenot.work {
110 import cloudflare_tls
111
112 reverse_proxy localhost:3400
113
114 header {
115 X-Frame-Options "SAMEORIGIN"
116 X-Content-Type-Options "nosniff"
117 X-XSS-Protection "1; mode=block"
118 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
119 Permissions-Policy "camera=(), microphone=(), geolocation=()"
120 Referrer-Policy "strict-origin-when-cross-origin"
121 Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'self'; form-action 'self' https://makenot.work"
122 }
123
124 encode gzip zstd
125
126 log {
127 output file /var/log/caddy/forums.log
128 format json
129 }
130 }
131
132 # CDN for free content downloads — reverse-proxies to Hetzner Object Storage.
133 # Cloudflare caches responses at the edge (free egress). Origin only hit on cache miss.
134 # Requires: S3 bucket policy allowing public s3:GetObject, Cloudflare DNS A record (proxy ON).
135 cdn.makenot.work {
136 import cloudflare_tls
137
138 # Only allow GET (downloads). Block mutations.
139 @not_get not method GET HEAD
140 respond @not_get 405
141
142 # Prepend bucket name to URI path and proxy to Hetzner Object Storage.
143 # Replace BUCKET_NAME with the actual S3 bucket name.
144 rewrite * /BUCKET_NAME{uri}
145 reverse_proxy https://fsn1.your-objectstorage.com {
146 header_up Host fsn1.your-objectstorage.com
147 }
148
149 header {
150 X-Content-Type-Options "nosniff"
151 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
152 Access-Control-Allow-Origin "https://makenot.work"
153 Access-Control-Allow-Methods "GET, HEAD"
154 # Cache-Control is set on the S3 objects themselves (immutable).
155 # Cloudflare respects the origin's Cache-Control header.
156 }
157
158 log {
159 output file /var/log/caddy/cdn.log
160 format json
161 }
162 }
163
164 # dl.maxj.phd download host retired 2026-06-09 — MNW now serves all downloads
165 # (creator product pages / makenot.work DMGs). The maxjphd_tls mTLS snippet and
166 # the dl.maxj.phd file_server block were removed with it; the /etc/caddy/maxj-phd-origin*
167 # cert/key are now unused on prod and can be deleted there.
168
169 # Redirect www to canonical domain
170 # Note: makenotwork.com and www.makenotwork.com redirects are handled by
171 # Cloudflare Redirect Rules (edge-level, no origin hit needed).
172 # Those domains are not covered by the *.makenot.work Origin CA cert.
173 # Redirect git subdomain browser visits to web UI
174 git.makenot.work {
175 import cloudflare_tls
176 redir https://makenot.work/git permanent
177 }
178
179 www.makenot.work {
180 import cloudflare_tls
181 redir https://makenot.work{uri} permanent
182 }
183
184 # Custom domains — on-demand TLS via Let's Encrypt.
185 # Caddy calls /api/domains/caddy-ask before issuing a cert for any domain.
186 # makenot.work subdomains are unaffected (matched by explicit blocks above
187 # which use Cloudflare Origin CA + mTLS).
188 :443 {
189 tls {
190 on_demand
191 }
192
193 # Custom domains connect directly to the origin (no Cloudflare mTLS in front),
194 # so any client-supplied CF-Connecting-IP / X-Forwarded-For is forgeable. The
195 # app trusts CF-Connecting-IP for rate-limiting, lockouts, and audit logs, so
196 # overwrite it with the real TCP peer and strip XFF before proxying — a client
197 # can no longer mint fake source IPs to evade per-IP throttles or poison logs.
198 reverse_proxy localhost:3000 {
199 # Set (replace) CF-Connecting-IP to the real TCP peer — overwrites any
200 # value the client sent. Strip X-Forwarded-For so no forged value reaches
201 # the app (the app ignores XFF anyway; this is hygiene).
202 header_up CF-Connecting-IP {http.request.remote.host}
203 header_up -X-Forwarded-For
204 }
205
206 header {
207 X-Content-Type-Options "nosniff"
208 Strict-Transport-Security "max-age=31536000; includeSubDomains"
209 Referrer-Policy "strict-origin-when-cross-origin"
210 }
211
212 encode gzip zstd
213
214 log {
215 output file /var/log/caddy/custom-domains.log
216 format json
217 }
218 }
219
220 # HTTP catch-all — redirect to HTTPS (also needed for ACME HTTP-01 challenges)
221 :80 {
222 redir https://{host}{uri} permanent
223 }
224