-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCaddyfile
More file actions
132 lines (119 loc) · 5.57 KB
/
Copy pathCaddyfile
File metadata and controls
132 lines (119 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# Caddyfile — plain HTTP origin behind Fastly, hardened.
#
# Fastly terminates TLS at the edge and proxies to this VM on port 80.
# Backend and frontend bind to 127.0.0.1 only (host network mode) — Caddy
# reaches them on loopback, nothing else can.
#
# Trust topology (security #013/#029/#032/#034 and extra E1):
# The reverse_proxy directives use header_up to rewrite X-Forwarded-For
# from the Fastly-Client-IP header. That trust is only valid for
# requests that actually came through Fastly — anyone connecting
# directly to port 80 can set Fastly-Client-IP to whatever they want.
# The @from_fastly remote_ip matcher gates the header rewrite on the
# TCP peer being inside Fastly's published edge ranges. Direct callers
# skip the header_up clause, so request.client.host in uvicorn comes
# from their real (untrusted) peer IP and the IP-based gates kick in.
#
# Routing:
# /api/* → backend directly (preserves Host header so the backend's
# DNS-rebinding gate matches the registered public_endpoint;
# peer = 127.0.0.1 from Caddy in host net mode).
# else → Next.js frontend.
#
# Note on edge IP list maintenance:
# The Fastly CIDRs below are the published v4 ranges as of 2026-06-03
# (https://api.fastly.com/public-ip-list). When Fastly adds a new edge
# range, refresh this list. A stale list means legitimate traffic from a
# new POP is treated as direct (untrusted) until Caddy reloads.
{
# No auto-HTTPS — Fastly handles TLS termination at the edge.
auto_https off
# Rate limit module (provided by custom Caddy image, see caddy/Dockerfile).
# 5 share-login attempts / minute per Fastly client IP.
order rate_limit before reverse_proxy
}
(security_headers) {
header {
-Server
# HSTS: tells browsers to always use HTTPS for this domain for 1 year.
Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Clickjacking protection.
X-Frame-Options "DENY"
# MIME-sniffing protection.
X-Content-Type-Options "nosniff"
# Don't leak full URL in Referer to other origins.
Referrer-Policy "strict-origin-when-cross-origin"
# Disable browser features we don't use.
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
# Conservative CSP. Next.js needs inline styles for hydration; loosen if
# you see CSP violations in DevTools after deploy.
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
}
}
:80 {
encode zstd gzip
request_body {
max_size 25MB
}
import security_headers
# Rate-limit share-login attempts. Tunables:
# key: per-client IP (the Fastly-Client-IP header set by Fastly's edge)
# window: 1 minute sliding window
# max_events: 5 per window — generous for typos, harsh for brute force
@share_login path /api/share/login
rate_limit @share_login {
zone share_login {
key {http.request.header.Fastly-Client-IP}
window 1m
events 5
}
}
# Defense in depth (extra E1): replace any client-supplied X-Forwarded-For
# with Caddy's authoritative view of the TCP peer. Then, only when the
# TCP peer is a Fastly edge IP, override with Fastly-Client-IP.
#
# Non-Fastly direct caller: XFF = their real peer IP. uvicorn (with
# --proxy-headers --forwarded-allow-ips=127.0.0.1) sees Caddy at the
# loopback peer and trusts XFF, so request.client.host = the real
# attacker IP. Backend's DNS-rebinding and remote-host checks then
# fire correctly instead of misclassifying as admin.
# Fastly-edge caller: the second directive overrides XFF with the
# client IP that Fastly's edge signed and attached.
request_header X-Forwarded-For {http.request.remote.host}
# Caddy-injected internal proxy marker (security #032): the frontend
# middleware blocks /admin requests when this header is present, while
# direct SSH-tunnel admin connections (which bypass Caddy) have no
# such header and reach the admin surface. Set unconditionally — there
# is no legitimate reason for an upstream to send this themselves.
request_header X-Proxied-By-Caddy "true"
# Named matcher: TCP peer is an actual Fastly edge IP.
@from_fastly_v4 {
remote_ip 23.235.32.0/20 43.249.72.0/22 103.244.50.0/24 103.245.222.0/23 103.245.224.0/24 104.156.80.0/20 140.248.64.0/18 140.248.128.0/17 146.75.0.0/17 151.101.0.0/16 157.52.64.0/18 167.82.0.0/17 167.82.128.0/20 167.82.160.0/20 167.82.224.0/20 172.111.64.0/18 185.31.16.0/22 199.27.72.0/21 199.232.0.0/16
}
# When AND ONLY WHEN the request came from a Fastly edge, propagate the
# authoritative Fastly-Client-IP as X-Forwarded-For. Requests bypassing
# Fastly retain the {client_ip} XFF set above (their real TCP peer),
# so a direct port-80 attacker cannot spoof their source IP regardless
# of what Fastly-Client-IP value they send.
request_header @from_fastly_v4 X-Forwarded-For {http.request.header.Fastly-Client-IP}
# API → backend (preserve Host so backend's DNS-rebinding gate matches the
# registered public_endpoint).
@api path /api/*
reverse_proxy @api 127.0.0.1:8000 {
flush_interval -1
}
# Everything else → Next.js frontend.
reverse_proxy 127.0.0.1:3000 {
flush_interval -1
}
# Detailed access log: JSON format with every request's client IP, host,
# URL, status, latency, and user-agent. Tail with:
# docker compose -f ... logs -f caddy
# Filter to errors:
# docker compose -f ... logs caddy | jq 'select(.status >= 400)'
log {
output stdout
format json
level INFO
}
}