• Home
  • \
  • Posts
  • Using Caddy as a reverse proxy in 2022

    Using Caddy as a reverse proxy in 2022

    Dusty Candland | December 26, 2022 | post, commandwp, caddy

    I’ve been wanting to move my load balancer system for WP Support HQ to Caddy web server for a while now. I took the slow holiday time to finally get it done!

    The load balancer is important when hosting websites for people, so I can move the server they’re on without having them update their DNS. There are other advantages; SSL termination, Web Application Firewalls, and Rate Limiting.

    Why Caddy?

    Caddy is fast. It has automatic SSL certificates. It’s easy to configure. At least easier than Nginx.

    It is also easy to compile in other modules, I learned.

    SSL Termination

    This is handed by Caddy almost automatically. Because I wasn’t doing this before, I needed to figure out how to proxy to a secure connection also. Termination is good, because it’s one less certificate to manage and faster to connect to the backend.

    Web Application Firewalls

    This helps protect my website from 0 days and potentially poorly written custom code. While it only helps, it is still worth having in place. I used the OWASP Coraza middleware for Caddy.

    Rate Limiting

    This helps protect the servers from being overloaded by crawlers and bots. A lot of crawlers and bots target WordPress sites. However, some clients use Cloudflare, so I needed to exclude those from these limits. I used the HTTP rate limiting module for Caddy 2.

    The Install

    I setup a new server on DigitalOcean with Ubuntu 22.04. Then installed Caddy using the deb packages. Install Caddy then install XCaddy. We’ll use XCaddy to build in the other modules for the WAF and Rate Limiting.

    xcaddy build --with github.com/corazawaf/coraza-caddy --with github.com/mholt/caddy-ratelimit
    systemctl stop caddy
    cp caddy /usr/bin/caddy
    systemctl start caddy

    UDP Receive Buffer Size

    sysctl -w net.core.rmem_max=2500000

    The Configuration

    I used the Caddyfile configuration format. Everything from here on is in the /etc/caddy/ directory.

      - Caddyfile
      - trusted_proxies
      - waf/
      - vhosts/

    Tip: Run Caddy directly to see configuration errors. caddy run --config /etc/caddy/Caddyfile. Stop the service first.

    The Caddyfile

            email dusty@wpsupporthq.com
            order coraza_waf first
            order rate_limit before basicauth
            admin off
            log {
                    output file /var/log/caddy/caddy.log {
                            roll_keep_for 30d
    (waf) {
            coraza_waf {
                    include /etc/caddy/waf/coraza/coraza.conf-recommended
                    include /etc/caddy/waf/coreruleset/crs-setup.conf.example
                    include /etc/caddy/waf/coreruleset/rules/*.conf
    (trusted) {
            import /etc/caddy/trusted_proxies
    (secure_headers) {
            header {
                    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
                    Content-Security-Policy "upgrade-insecure-requests"
                    Referrer-Policy "strict-origin-when-cross-origin"
                    ?Cache-Control "public, max-age=15, must-revalidate"
    # not CloudFlare IPs
    (rate_limits) {
            @not_excepted not remote_ip ...
            rate_limit @not_excepted {
                    zone site {
                            key {http.request.host}
                            window 5m
                            events 1000
                    zone ip {
                            key {http.request.remote.host}
                            window 5m
                            events 250
    :80 {
            root * /usr/share/caddy
            import rate_limits
    wps-lb-2.wpserverhq.com:443 {
            root * /usr/share/caddy
            import rate_limits
    import /etc/caddy/vhosts/*

    This is the main configuration file. There are global options and snippets for use in the vhost files.


    Is a list of trusted proxies for the reverse_proxy directive. We need this to get the correct IP from these proxies, Cloudflare, in my case.

    trusted_proxies ...

    The waf directory holds the default rules for the firewall. I followed the setup instructions for Using OWASP Core Ruleset.

    There is one newer rule that doesn’t work right now. The solution is to just delete it until it’s supported. GitHub Issue

    # Setup
    cd waf/
    git clone git@github.com:coreruleset/coreruleset.git
    rm /etc/caddy/waf/coreruleset/rulesREQUEST-922-MULTIPART-ATTACK.conf
    mkdir coraza
    cd coraza
    wget https://raw.githubusercontent.com/corazawaf/coraza/v3/dev/coraza.conf-recommended

    The vhosts directory holds the configuration of the sites.

    Tip: When saving a vhosts file, I had to also re-save the Caddyfile otherwise I’d get an odd configuration error. Not sure why?

    Error: adapting config using caddyfile: /etc/caddy/vhosts/test.wpsupporthq.com:1: unrecognized directive: test.wpsupporthq.com:443
    Did you mean to define a second site? If so, you must use curly braces around each site to separate their configurations.


    test.wpsupporthq.com:443 {
            encode gzip
            import secure_headers
            # Change to https for secure upstream
            reverse_proxy {
                    # Uncomment to keep a TLS connection to the upstream server
                    # transport http {
                    #       tls
                    #       tls_insecure_skip_verify
                    #       compression off
                    # }
                    header_up Host {http.request.host}
                    header_up X-Real-IP {http.request.remote}
                    header_up X-Forwarded-Port {http.request.port}
                    import trusted
            import waf
            import rate_limits

    Next Steps

    Redirect www to non-www and vis-versa.

    Automate the vhost files with Ansible.