Why Caddy
The usual proxy options (Squid, Nginx) are fiddly to configure, and TLS support usually takes extra work. Caddy supports HTTPS forward proxying (CONNECT tunnels) natively through the forwardproxy plugin, and with Basic Auth on top, the whole setup stays short and cheap to maintain.
I use acme.sh instead of Caddy's built-in ACME because some situations call for more control over how the certificate gets issued: DNS-01 validation, picking a specific CA (ZeroSSL / Let's Encrypt / Buypass), or getting a certificate on a machine whose firewall does not open 80/443.
Prerequisites
- A server with a public IP (this post assumes Debian/Ubuntu)
- A domain with an A record already pointing at the server's IP
- A DNS provider with an API (needed for DNS-01 validation)
One-Shot Script
If you would rather not do this by hand, there is an automated install script:
sudo DOMAIN=proxy.example.com \
[email protected] \
PROXY_USER=myuser \
PROXY_PASS=mypassword \
CF_Token=xxx \
CF_Zone_ID=xxx \
bash <(curl -fsSL https://gist.githubusercontent.com/madeye/9a578ad8c9b8166f999719aa7784aa6f/raw/setup-caddy-proxy.sh)
The manual steps are spelled out below.
1. Install acme.sh and Issue a Certificate
# Install acme.sh
curl https://get.acme.sh | sh -s [email protected]
# Using Cloudflare DNS as the example, set the API token
export CF_Token="your_cloudflare_api_token"
export CF_Zone_ID="your_zone_id"
# Issue the certificate (DNS-01 validation)
~/.acme.sh/acme.sh --issue --dns dns_cf -d proxy.example.com
# Install the certificate into the target directory
mkdir -p /etc/caddy/certs
~/.acme.sh/acme.sh --install-cert -d proxy.example.com \
--cert-file /etc/caddy/certs/cert.pem \
--key-file /etc/caddy/certs/key.pem \
--fullchain-file /etc/caddy/certs/fullchain.pem \
--reloadcmd "systemctl restart caddy"
If you use a different DNS provider, acme.sh supports dozens of DNS APIs. Check its wiki and swap out dns_cf and the matching environment variables.
2. Build Caddy with forwardproxy
The official Caddy binaries do not include the forwardproxy plugin, so you need to build your own with xcaddy.
# Install Go (if you don't have it)
sudo apt install -y golang
# Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
# Build Caddy with forwardproxy
~/go/bin/xcaddy build --with github.com/caddyserver/forwardproxy=github.com/klzgrad/forwardproxy@naive
# Move it into the system path
sudo mv caddy /usr/bin/caddy
sudo chmod +x /usr/bin/caddy
This uses the forwardproxy fork maintained by klzgrad, which supports naive-protocol traffic obfuscation and holds up better against probing. If you don't need that, the official
github.com/caddyserver/forwardproxyworks fine.
3. Configure Caddy
Create the Caddyfile:
sudo mkdir -p /etc/caddy
Edit /etc/caddy/Caddyfile:
{
order forward_proxy before file_server
admin off
}
:443, proxy.example.com {
tls /etc/caddy/certs/fullchain.pem /etc/caddy/certs/key.pem
forward_proxy {
basic_auth user password123 # replace with your own username and password
hide_ip
hide_via
probe_resistance secret.localhost # resist active probing
}
file_server {
root * /var/www/html # pose as an ordinary website
}
}
A few of these settings are worth explaining:
basic_auth: sets the proxy's username and password, blocking unauthorized usehide_ip/hide_via: hides the client's real IP and the proxy signatureprobe_resistance: when a non-proxy request comes in, serve ordinary web content instead, so active probes learn nothingfile_server: goes withprobe_resistance, serving a plain web page as the cover
4. Set Up the systemd Service
Create /etc/systemd/system/caddy.service:
[Unit]
Description=Caddy
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=root
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
[Install]
WantedBy=multi-user.target
Start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now caddy
5. Using It from a Client
Once everything is set up, point your client at the HTTPS proxy:
https://user:[email protected]:443
Browsers
Most browsers can pick up an HTTPS proxy from the system proxy settings. You can also use an extension like SwitchyOmega: pick HTTPS as the protocol and fill in the domain, port, username and password.
Command Line
export https_proxy=https://user:[email protected]:443
export http_proxy=https://user:[email protected]:443
curl -I https://www.google.com
NaiveProxy Client
If you built the server against klzgrad's forwardproxy fork, the NaiveProxy client is the better choice for traffic obfuscation:
{
"listen": "socks://127.0.0.1:1080",
"proxy": "https://user:[email protected]"
}
6. Checks and Troubleshooting
# Check that Caddy is running
sudo systemctl status caddy
# Follow the logs
sudo journalctl -u caddy -f
# Test the certificate
openssl s_client -connect proxy.example.com:443 -servername proxy.example.com
# Test the proxy
curl -x https://user:[email protected]:443 https://httpbin.org/ip
Automatic Renewal
acme.sh installs a cron job for you. Before the certificate expires it renews automatically and restarts Caddy through --reloadcmd to pick up the new one, with nothing left to do by hand.
You can check that the cron job is there:
crontab -l | grep acme
Wrap-Up
What this setup gets you:
- Simple config: a dozen or so lines of Caddyfile, far less hassle than an Nginx + Squid setup
- TLS encryption: all proxy traffic goes over HTTPS, so nobody in the middle can look inside
- Authentication: Basic Auth keeps the proxy from being abused
- Probe resistance:
probe_resistanceplus a cover site means unauthorized visitors only see an ordinary web page - Auto-renewal: acme.sh's cron job keeps the certificate valid
