| 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 |
|