What did I do with Caddy today?
Defence in depth is always important and having a reverse proxy allows some actions to be executed before traffic hits a webserver.
Hosted on my Caddy are the following:
- several YOURLS shortcut instances
- reverse proxy for all of my websites
- reverse proxy with IP filtering for my internal sites
- Fail2Ban operating to ban using Cloudflare API for IPs that try to scrape into the sites
With most domains already protected by Cloudflare (bringing excellent TLS configurations, bot protection and AI bot-blocking), my domains are already well-protected from abuse. There was, however, a need for more.
Hosted at home, for my own consumption, are some Synology servers, some Piholes and a Splunk server. The problem is that, when I Caddy in front of them and collect a certificate based on a subdomain (because I want to not have browser errors when connecting), the bad guys are picking up on these newly-minted certificates and scanning my servers….!
Caddy enabled me to limit what can connect to the resources so that they were better protected by introducing filters based on IP address.
It even RickRolls people who persistently try to connect! Try it yourself.

It’s worth noting the Internet is a dirty place; the image shows the hits on the system in the first 10 minutes since DNS and Caddy were configured. Certificate Transparency is (maybe not) your friend?
Caddy is extremely useful and is formed around snippets; the snippets can then be pulled into the domains hosted by Caddy.
Start with creating security headers – maybe less important behind Cloudflare but a wise starting point all the same. Notice that I have Splunk behind my Caddy and it’s easy to break if you have a Content Security Policy that is too strict.
# === Strong security headers (relaxed CSP for Google Fonts, etc.) ===
(securityheaders) {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' <https://cdnjs.cloudflare.com> <https://fonts.googleapis.com> <https://static.cloudflareinsights.com>; style-src 'self' 'unsafe-inline' <https://fonts.googleapis.com>; font-src 'self' <https://fonts.gstatic.com> data:; img-src 'self' https://*.splunk.com https://*.cartocdn.com data:; connect-src 'self' <https://telemetry-splkmobile.dataeng.splunk.com> wss: https:; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests;"
Permissions-Policy "geolocation=(), microphone=(), camera=()"
Cross-Origin-Embedder-Policy "unsafe-none"
Cross-Origin-Opener-Policy "same-origin"
Cross-Origin-Resource-Policy "same-origin"
}
}
Sitting behind Cloudflare (the free tier, it is too good to not sit behind and brings many benefits), it’s worth handling the Cloudflare forwarded headers.
# === Cloudflare Headers ===
(cloudflare-headers) {
header {
header_up -X-Forwarded-*
header_up -CF-Connecting-IP
header_up -CF-IPCountry
-X-Forwarded-For
-X-Real-IP
X-Client-IP "{http.request.header.CF-Connecting-IP}"
}
}
If you’re not behind Cloudflare, you will want to score highly with Qualys’ SSLLabs.
# === TLS Settings with Cloudflare API for domain renewals ===
(tls-settings) {
tls {
protocols tls1.2 tls1.3
}
}
In the case where you have your own resources and want to protect them from everyone else but benefit from a TLS connection and proper certificate, managed via ACME, you have great options.
# === IP Whitelist direct connections ===
(ip-restricted-direct) {
import tls-settings
header {
X-Debug-IP "{http.request.remote}"
}
@notWhitelisted {
not remote_ip my.ip.address
}
handle @notWhitelisted {
respond "404" 403
}
}
# === IP Whitelist behind Cloudflare ===
(ip-restricted) {
import cloudflare-headers
import tls-settings
header {
X-Debug-IP "{http.request.header.CF-Connecting-IP}"
}
@notWhitelisted {
not header CF-Connecting-IP my.ip.address
}
handle @notWhitelisted {
respond "404" 404
}
}
There’s even room for some resource protection of selected resources per domain; the below example shows anyone trying to scrape into my YOURLS servers being Rickrolled and my reports folder being limited to my IP while the rest of the service is open.
# === Secure /admin with a Rickroll (behind Cloudflare) ===
(admin-rickroll) {
@adminPath {
path /admin* /yourls-api.php /yourls-infos.php
not header CF-Connecting-IP my.ip.address
}
handle @adminPath {
rewrite * /403.html
file_server
}
}
# === Secure /reports (behind Cloudflare) ===
(secure-reports-path) {
@reportsPath {
path /reports*
not header CF-Connecting-IP my.ip.address
}
handle @reportsPath {
respond "404 Error" 403
}
}
YOURLS, you say? It does not work properly without some love on Caddy but with some tweaks, it the shortcuts you issue can work and the other resources can be limited (reCAPTCHA on YOURLS is a little <meh>).
# === YOURLS Shared Config Snippet ===
(yourls-site) {
import cloudflare-headers
import admin-rickroll
import tls-settings
@cf header CF-Connecting-IP *
header @cf {
X-Forwarded-For {http.request.header.CF-Connecting-IP}
Vary "Accept-Encoding, Cookie, CF-Connecting-IP"
}
root * {args[0]}
php_fastcgi unix//run/php/php8.3-fpm.sock {
trusted_proxies 0.0.0.0/0
# trusted_proxies cloudflare interval 12h timeout 15s # Corrected line
env REMOTE_ADDR {http.request.header.CF-Connecting-IP}
}
encode gzip
file_server
@notStatic {
not path /admin* /css* /js* /images* /includes* /user* /sleeky-frontend* /yourls-*.php /favicon.ico /index.html
not file
}
rewrite @notStatic /yourls-loader.php?{query}
}
Behind Caddy, I have some Synology units and some Syncthing instances. Notice the clue that Tailscale provides here – a fabulous service that allows my Caddy to forward traffic to the devices no matter the network they sit on.
# === Synology Units ===
syn.mydomain.com {
import tls-settings
import ip-restricted
reverse_proxy https://tailscale_ip:5001 {
transport http {
tls_insecure_skip_verify
}
}
log {
import logfile_common synology-access.log
}
}
# === SyncThing(s) ===
st.mydomain.com {
import tls-settings
import ip-restricted-direct
reverse_proxy http://tailscale_ip:8384
log {
import logfile_common syncthing-access.log
}
}
The Caddy also proxies connections to my web servers and to my honeypots (which actually cheekily redirect direct connections to the honeypots via Caddy and over TLS).
The web servers benefit from Server Name Identification so I simply forward the inbound connections for the domain to the IP that my host provides; SNI does the rest….
mydomain.com {
import tls-settings
reverse_proxy server.ip.address {
header_up Host mydomain.com
header_up X-Forwarded-Host mydomain.com
header_up X-Forwarded-Proto https
}
log {
import logfile_common mydomain-access.log
}
}
What you get is a site that looks excellent when TLS configuration is reviewed by one of the online services that can do that. An example behind Cloudflare:

Without Cloudflare and hitting the Securityheaders snippet directly, the results are the same (Caddy really does prioritise strong ciphers).

The only pitfalls of this configuration is that you might end up with your Caddy server being blacklisted by your hosting provider; I managed this within 24 hours (my provider is Infomaniak) but a simple email exchange resulted in my Caddy IP being whitelisted.
Congratulations on reading to here! There are clues on how you could do this for yourself and what the benefits can be. If you have internal services that you want to use via a browser and have easy TLS in place, this with IP filtering is a solution. With distributed system, you can route traffic from Caddy onwards via Tailscale (I ❤️ Tailscale!). The limitations are almost all in your imagination!
