Fix blank page: add Cloudflare-fronted HTTP-only Caddyfile #4
@@ -0,0 +1,74 @@
|
|||||||
|
# Caddyfile for the blog behind Cloudflare.
|
||||||
|
#
|
||||||
|
# Topology: Cloudflare (terminates TLS) -> Caddy (HTTP :80) -> SWS (blog:80)
|
||||||
|
#
|
||||||
|
# Cloudflare handles the public HTTPS cert, so Caddy serves plain HTTP on :80
|
||||||
|
# and does NOT need the Cloudflare DNS-01 / automatic-HTTPS setup. Set the
|
||||||
|
# Cloudflare SSL/TLS mode to "Full" so CF->origin is encrypted at the network
|
||||||
|
# layer (e.g. over your tunnel/Netbird), even though this listener is HTTP.
|
||||||
|
#
|
||||||
|
# Copy this file to /etc/caddy/Caddyfile on the caddy_cf host and reload:
|
||||||
|
# docker exec caddy_cf caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
# or restart the container.
|
||||||
|
{
|
||||||
|
# Trust only Cloudflare's edge IPs so real visitor IPs (Cf-Connecting-Ip /
|
||||||
|
# X-Forwarded-For) are honoured only when the request actually comes from
|
||||||
|
# Cloudflare. This prevents header spoofing from any other source.
|
||||||
|
#
|
||||||
|
# Ranges from https://www.cloudflare.com/ips-v4/ and /ips-v6/ .
|
||||||
|
# Keep in sync when Cloudflare updates them (rarely). If you rebuild
|
||||||
|
# caddy-cf with the caddy-dynamic-cloudflare / caddy-cloudflare-ip plugin,
|
||||||
|
# this static list can be replaced with `trusted_proxies cloudflare`.
|
||||||
|
servers {
|
||||||
|
trusted_proxies static \
|
||||||
|
173.245.48.0/20 \
|
||||||
|
103.21.244.0/22 \
|
||||||
|
103.22.200.0/22 \
|
||||||
|
103.31.4.0/22 \
|
||||||
|
141.101.64.0/18 \
|
||||||
|
108.162.192.0/18 \
|
||||||
|
190.93.240.0/20 \
|
||||||
|
188.114.96.0/20 \
|
||||||
|
197.234.240.0/22 \
|
||||||
|
198.41.128.0/17 \
|
||||||
|
162.158.0.0/15 \
|
||||||
|
104.16.0.0/13 \
|
||||||
|
104.24.0.0/14 \
|
||||||
|
172.64.0.0/13 \
|
||||||
|
131.0.72.0/22 \
|
||||||
|
2400:cb00::/32 \
|
||||||
|
2606:4700::/32 \
|
||||||
|
2803:f800::/32 \
|
||||||
|
2405:b500::/32 \
|
||||||
|
2405:8100::/32 \
|
||||||
|
2a06:98c0::/29 \
|
||||||
|
2c0f:f248::/32
|
||||||
|
client_ip_headers Cf-Connecting-Ip X-Forwarded-For
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bind to the hostname on the HTTP port only. Because the address has an
|
||||||
|
# explicit ":80" scheme/port, Caddy will NOT try to provision a TLS cert.
|
||||||
|
http://blog.ahmza.com {
|
||||||
|
# THE responder: proxy every request to the blog (SWS) container.
|
||||||
|
# This is what was missing before (requests fell through to a NOP -> blank page).
|
||||||
|
reverse_proxy blog:80
|
||||||
|
|
||||||
|
# Compression happens here (SWS has it disabled on purpose).
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Security headers (single source of truth; SWS has security-headers off).
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
# Drop Caddy's default Server header.
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,44 @@
|
|||||||
# Caddy as a Reverse Proxy
|
# Caddy as a Reverse Proxy
|
||||||
|
|
||||||
This document describes how to run [Caddy](https://caddyserver.com/) as a
|
This document describes how to run [Caddy](https://caddyserver.com/) as a
|
||||||
reverse proxy in front of the blog. It covers both deployment targets used by
|
reverse proxy in front of the blog.
|
||||||
this project: the Kubernetes (Raspberry Pi) cluster and the Docker Compose host
|
|
||||||
(Gimli).
|
|
||||||
|
|
||||||
It uses the custom [`caddy-cf`](https://github.com/ahmzaa/caddy-cf) image
|
It uses the custom [`caddy-cf`](https://github.com/ahmzaa/caddy-cf) image
|
||||||
(`ghcr.io/ahmzaa/caddy-cf:latest`) — a Caddy build with the **Cloudflare DNS
|
(`ghcr.io/ahmzaa/caddy-cf:latest`) — a Caddy build with the **Cloudflare DNS
|
||||||
module** baked in. The stock Caddy image cannot solve the ACME **DNS-01**
|
module** baked in.
|
||||||
challenge for Cloudflare; this custom build can, which is what lets us obtain
|
|
||||||
certificates without exposing ports `80`/`443` to the public internet.
|
## Deployment topologies
|
||||||
|
|
||||||
|
There are two supported ways to run Caddy in front of the blog. **Pick one.**
|
||||||
|
|
||||||
|
### 1. Cloudflare in front (current production) — HTTP only
|
||||||
|
|
||||||
|
```
|
||||||
|
Cloudflare (terminates TLS) -> Caddy (HTTP :80) -> SWS (blog:80)
|
||||||
|
```
|
||||||
|
|
||||||
|
Cloudflare proxies the domain and provides the public HTTPS certificate, so
|
||||||
|
Caddy only needs to serve plain **HTTP on :80** and reverse-proxy to the blog.
|
||||||
|
Caddy does **not** provision a TLS cert in this mode. This is the live setup and
|
||||||
|
its config is the committed [`Caddyfile`](../Caddyfile) at the repo root —
|
||||||
|
see [Cloudflare-fronted setup](#cloudflare-fronted-setup-http-only) below.
|
||||||
|
|
||||||
|
Set the Cloudflare **SSL/TLS mode to "Full"** so the Cloudflare→origin hop is
|
||||||
|
still encrypted at the network layer (e.g. over Netbird), even though Caddy's
|
||||||
|
listener is HTTP.
|
||||||
|
|
||||||
|
### 2. Caddy terminates TLS via Cloudflare DNS-01
|
||||||
|
|
||||||
|
```
|
||||||
|
Client -> Caddy (HTTPS :443, ACME DNS-01) -> SWS (blog:80)
|
||||||
|
```
|
||||||
|
|
||||||
|
Caddy obtains/renews the certificate itself using the ACME **DNS-01** challenge
|
||||||
|
through the Cloudflare API — no inbound `80`/`443` from the public internet is
|
||||||
|
required, which suits a Netbird-only setup with no Cloudflare proxy in front.
|
||||||
|
The stock Caddy image cannot do this; the `caddy-cf` build bundles the
|
||||||
|
Cloudflare DNS provider plugin. See the Docker Compose / Kubernetes examples
|
||||||
|
later in this document.
|
||||||
|
|
||||||
## Why a reverse proxy?
|
## Why a reverse proxy?
|
||||||
|
|
||||||
@@ -21,7 +50,7 @@ the reverse proxy so that each layer owns exactly one job:
|
|||||||
|
|
||||||
| Concern | Owned by | Notes |
|
| Concern | Owned by | Notes |
|
||||||
| -------------------- | -------- | ------------------------------------------------- |
|
| -------------------- | -------- | ------------------------------------------------- |
|
||||||
| TLS / HTTPS certs | Caddy | Automatic certs via Cloudflare **DNS-01** challenge |
|
| TLS / HTTPS certs | Cloudflare *or* Caddy | Cloudflare in topology 1; Caddy (DNS-01) in topology 2 |
|
||||||
| Compression (gzip/zstd) | Caddy | `encode` directive; SWS compression is off |
|
| Compression (gzip/zstd) | Caddy | `encode` directive; SWS compression is off |
|
||||||
| Security headers | Caddy | Single source of truth, avoids duplicate headers |
|
| Security headers | Caddy | Single source of truth, avoids duplicate headers |
|
||||||
| Serving static files | SWS | Reads from `/public`, sets `Cache-Control`/`ETag` |
|
| Serving static files | SWS | Reads from `/public`, sets `Cache-Control`/`ETag` |
|
||||||
@@ -50,11 +79,55 @@ Whatever the deployment, the SWS origin exposes:
|
|||||||
- **Health endpoint:** `GET /health` → `200 OK`
|
- **Health endpoint:** `GET /health` → `200 OK`
|
||||||
- **Cache headers:** already set by SWS (Caddy should pass them through, not override)
|
- **Cache headers:** already set by SWS (Caddy should pass them through, not override)
|
||||||
|
|
||||||
Caddy only needs to terminate TLS (via Cloudflare DNS-01), add compression +
|
Caddy only needs to handle TLS (or let Cloudflare do it), add compression +
|
||||||
security headers, and forward requests to that origin.
|
security headers, and forward requests to that origin.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Cloudflare-fronted setup (HTTP only)
|
||||||
|
|
||||||
|
**This is the current production topology.** The canonical config is the
|
||||||
|
committed [`Caddyfile`](../Caddyfile) at the repo root; copy it to the
|
||||||
|
`caddy_cf` host and reload. Highlights:
|
||||||
|
|
||||||
|
- **`http://blog.ahmza.com`** — the explicit `http://` scheme binds Caddy to
|
||||||
|
`:80` only and stops it from trying to provision a TLS certificate (Cloudflare
|
||||||
|
already serves HTTPS to visitors).
|
||||||
|
- **`reverse_proxy blog:80`** — proxies to the blog (SWS) container by its
|
||||||
|
compose service name. Caddy and the blog container must share a Docker
|
||||||
|
network. Omitting this responder makes Caddy return an empty `NOP` response —
|
||||||
|
i.e. a **blank page**.
|
||||||
|
- **`encode zstd gzip`** — compression happens here (SWS has it disabled).
|
||||||
|
- **`header { ... }`** — security headers (SWS has them disabled).
|
||||||
|
- **`trusted_proxies static <Cloudflare ranges>`** + **`client_ip_headers
|
||||||
|
Cf-Connecting-Ip X-Forwarded-For`** — so the real visitor IP is used for
|
||||||
|
logging/matchers, but only when the request genuinely comes from Cloudflare
|
||||||
|
(prevents header spoofing from any other source). The ranges come from
|
||||||
|
<https://www.cloudflare.com/ips-v4/> and <https://www.cloudflare.com/ips-v6/>;
|
||||||
|
keep them in sync when Cloudflare updates them. If `caddy-cf` is rebuilt with
|
||||||
|
the `caddy-cloudflare-ip` plugin, the static list can be replaced with
|
||||||
|
`trusted_proxies cloudflare`.
|
||||||
|
|
||||||
|
Deploy and reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Put the Caddyfile in place on the caddy_cf host (e.g. into the ./conf mount),
|
||||||
|
# then reload without downtime:
|
||||||
|
docker exec caddy_cf caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Verify from the host (expect HTTP 200, Content-Encoding: gzip, X-Frame-Options):
|
||||||
|
curl -sI -H "Accept-Encoding: gzip" http://localhost/ | grep -iE 'HTTP|content-encoding|x-frame'
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Cloudflare SSL/TLS mode:** set it to **Full** so the Cloudflare→origin hop
|
||||||
|
> is encrypted at the network layer (e.g. over Netbird), even though Caddy's
|
||||||
|
> listener is plain HTTP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The remaining sections describe **topology 2** (Caddy terminates TLS itself via
|
||||||
|
the Cloudflare DNS-01 challenge) for setups without a Cloudflare proxy in front.
|
||||||
|
|
||||||
## Docker Compose (Gimli)
|
## Docker Compose (Gimli)
|
||||||
|
|
||||||
Run Caddy and the blog container on the same Docker network. Caddy reaches the
|
Run Caddy and the blog container on the same Docker network. Caddy reaches the
|
||||||
|
|||||||
Reference in New Issue
Block a user