Skip to main content

max / makenotwork

6.1 KB · 215 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 # Multithreaded forum
75 forums.makenot.work {
76 import cloudflare_tls
77
78 reverse_proxy localhost:3400
79
80 header {
81 X-Frame-Options "SAMEORIGIN"
82 X-Content-Type-Options "nosniff"
83 X-XSS-Protection "1; mode=block"
84 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
85 Permissions-Policy "camera=(), microphone=(), geolocation=()"
86 Referrer-Policy "strict-origin-when-cross-origin"
87 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"
88 }
89
90 encode gzip zstd
91
92 log {
93 output file /var/log/caddy/forums.log
94 format json
95 }
96 }
97
98 # CDN for free content downloads — reverse-proxies to Hetzner Object Storage.
99 # Cloudflare caches responses at the edge (free egress). Origin only hit on cache miss.
100 # Requires: S3 bucket policy allowing public s3:GetObject, Cloudflare DNS A record (proxy ON).
101 cdn.makenot.work {
102 import cloudflare_tls
103
104 # Only allow GET (downloads). Block mutations.
105 @not_get not method GET HEAD
106 respond @not_get 405
107
108 # Prepend bucket name to URI path and proxy to Hetzner Object Storage.
109 # Replace BUCKET_NAME with the actual S3 bucket name.
110 rewrite * /BUCKET_NAME{uri}
111 reverse_proxy https://fsn1.your-objectstorage.com {
112 header_up Host fsn1.your-objectstorage.com
113 }
114
115 header {
116 X-Content-Type-Options "nosniff"
117 Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
118 Access-Control-Allow-Origin "https://makenot.work"
119 Access-Control-Allow-Methods "GET, HEAD"
120 # Cache-Control is set on the S3 objects themselves (immutable).
121 # Cloudflare respects the origin's Cache-Control header.
122 }
123
124 log {
125 output file /var/log/caddy/cdn.log
126 format json
127 }
128 }
129
130 # maxj.phd TLS config: separate Origin CA cert + Authenticated Origin Pulls (mTLS)
131 (maxjphd_tls) {
132 tls /etc/caddy/maxj-phd-origin.pem /etc/caddy/maxj-phd-origin-key.pem {
133 client_auth {
134 mode require_and_verify
135 trusted_ca_cert_file /etc/caddy/cloudflare-authenticated-origin-pull-ca.pem
136 }
137 }
138 }
139
140 # Static file downloads (audiofiles binaries, etc.)
141 dl.maxj.phd {
142 import maxjphd_tls
143
144 root * /opt/downloads
145 file_server browse
146
147 header {
148 X-Content-Type-Options "nosniff"
149 Strict-Transport-Security "max-age=31536000; includeSubDomains"
150 }
151
152 encode gzip zstd
153
154 log {
155 output file /var/log/caddy/dl-maxjphd.log
156 format json
157 }
158 }
159
160 # Redirect www to canonical domain
161 # Note: makenotwork.com and www.makenotwork.com redirects are handled by
162 # Cloudflare Redirect Rules (edge-level, no origin hit needed).
163 # Those domains are not covered by the *.makenot.work Origin CA cert.
164 # Redirect git subdomain browser visits to web UI
165 git.makenot.work {
166 import cloudflare_tls
167 redir https://makenot.work/git permanent
168 }
169
170 www.makenot.work {
171 import cloudflare_tls
172 redir https://makenot.work{uri} permanent
173 }
174
175 # Custom domains — on-demand TLS via Let's Encrypt.
176 # Caddy calls /api/domains/caddy-ask before issuing a cert for any domain.
177 # makenot.work subdomains are unaffected (matched by explicit blocks above
178 # which use Cloudflare Origin CA + mTLS).
179 :443 {
180 tls {
181 on_demand
182 }
183
184 # Custom domains connect directly to the origin (no Cloudflare mTLS in front),
185 # so any client-supplied CF-Connecting-IP / X-Forwarded-For is forgeable. The
186 # app trusts CF-Connecting-IP for rate-limiting, lockouts, and audit logs, so
187 # overwrite it with the real TCP peer and strip XFF before proxying — a client
188 # can no longer mint fake source IPs to evade per-IP throttles or poison logs.
189 reverse_proxy localhost:3000 {
190 # Set (replace) CF-Connecting-IP to the real TCP peer — overwrites any
191 # value the client sent. Strip X-Forwarded-For so no forged value reaches
192 # the app (the app ignores XFF anyway; this is hygiene).
193 header_up CF-Connecting-IP {http.request.remote.host}
194 header_up -X-Forwarded-For
195 }
196
197 header {
198 X-Content-Type-Options "nosniff"
199 Strict-Transport-Security "max-age=31536000; includeSubDomains"
200 Referrer-Policy "strict-origin-when-cross-origin"
201 }
202
203 encode gzip zstd
204
205 log {
206 output file /var/log/caddy/custom-domains.log
207 format json
208 }
209 }
210
211 # HTTP catch-all — redirect to HTTPS (also needed for ACME HTTP-01 challenges)
212 :80 {
213 redir https://{host}{uri} permanent
214 }
215