Skip to content

Caddy

bash
# log location
/var/lib/caddy/.local/share/caddy

Forward Auth

https://docs.goauthentik.io/docs/providers/proxy/server_caddy

Update config via API

bash
caddy fmt --overwrite Caddyfile
curl localhost:2019/load \
	-H "Content-Type: text/caddyfile" \
	--data-binary @Caddyfile

Cookbook

Static files

bash
subdomain.domain.tld {
    root * /opt/files

    route {
        crowdsec
        file_server browse
        # ---- alternate config ----
        # Serve pre-compressed files if present
        file_server /downloads/* {
          precompressed gzip zstd br
        }
        # ---- end alternate config ----

        # ---- optional config ----
        encode zstd gzip
        # ---- end optional config ----
    }
}

Basic auth

bash
# create password hash
caddy hash-password --algorithm bcrypt
bash
subdomain.domain.tld {
    basicauth * {
        $USERNAME $PASSWORD_HASH
    }
    reverse_proxy 127.0.0.1:$PORT
}

IP filter

bash
# https://gist.github.com/morph027/b771fb579c36ae550ebb2764581a1d0e

intranet.example.com {
  @ipfilter {
    not remote_ip 192.168.0.0/16
  }
  route @ipfilter {
    # redirect
    redir https://example.com/
    # or respond
    # respond "Access denied" 403 {
    #   close
    # }
  }
  reverse_proxy / https://intranet.lan
}

SPA

bash
:80 {
 # https://caddy.community/t/how-to-serve-spa-applications-with-caddy-v2/8761/2
 try_files {path} /
 encode gzip
 root * /app/dist/spa
 file_server
}

Shorthand

bash
(base) {
    route {
        crowdsec
        reverse_proxy $IP:{args[0]}
    }
}

subdomain.domain.tld {
    import base $PORT
}

Set header

bash
subdomain.domain.tld {
  reverse_proxy $IP:$PORT {
    header_up Host example.com
  }
}

L4

bash
{
    crowdsec {
        api_url http://127.0.0.1:8080
        api_key xxxxxxxxxxx
        ticker_interval 15s
    }
    servers {
        listener_wrappers {
            layer4 {
                @ssh ssh
                route @ssh {
                    proxy $IP:$PORT
                }
                route
            }
            tls
        }
    }
}

Set transport

bash
subdomain.domain.tld {
    route {
        crowdsec
        reverse_proxy $IP:$PORT {
            transport http {
                dial_timeout 5m
                response_header_timeout 5m
                read_timeout 5m
                write_timeout 5m
                tls_insecure_skip_verify # optional
            }
        }
    }
}

Return 403

bash
subdomain.domain.tld {
    route {
       crowdsec
       respond / "Access Denied" 403
       reverse_proxy $IP:$PORT
    }
}

Set certain routes behind basicauth

bash
subdomain.domain.tld {
  @api_gateway path /api/* /gateway/*
	handle @api_gateway {
		basicauth * {
			username passwordhash
		}

		route {
	    	crowdsec
            reverse_proxy $IP:$PORT
		}
	}
	handle {
		route {
            ...
		}
	}
}

Set API key header

bash
subdomain.domain.tld {
    route {
        crowdsec
        rewrite * /api/foo{uri} # accessing $DOMAIN/api/foo is changed to $DOMAIN/

        @unauthorized {
            not {
                header X-API-Key $API_KEY
            }
        }
        respond @unauthorized "Unauthorized: Missing or invalid API key." 401 {
            close
        }
        reverse_proxy http://$IP:PORT
    }
}

CORs

bash
(cors) {
	@cors_preflight method OPTIONS
	@cors header Origin {args[0]}

	handle @cors_preflight {
		header Access-Control-Allow-Origin "{args[0]}"
		header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, HEAD"
		header Access-Control-Allow-Headers "Range,If-Match"
		header Access-Control-Max-Age "3600"
		respond "" 204
	}

	handle @cors {
		header Access-Control-Allow-Origin "{args[0]}"
		header Access-Control-Expose-Headers "ETag"
	}
}
a.domain.tld {
	root * /opt/foo
	file_server

	import cors https://b.domain.tld
}

Caddy with Plugins

bash
sudo apt install golang-go -y
go install "golang.org/dl/go1.25.4@latest"
go1.25.4 download
go env -w GOTOOLCHAIN=go1.25.4

go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
~/go/bin/xcaddy build \
  --with github.com/caddy-dns/cloudflare \
  --with github.com/mholt/caddy-l4 \
  --with github.com/caddyserver/transform-encoder \
  --with github.com/hslatman/caddy-crowdsec-bouncer/http@main \
  --with github.com/hslatman/caddy-crowdsec-bouncer/appsec@main \
  --with github.com/hslatman/caddy-crowdsec-bouncer/layer4@main
sudo mv caddy /usr/bin/

sudo groupadd --system caddy
sudo useradd --system \
    --gid caddy \
    --create-home \
    --home-dir /var/lib/caddy \
    --shell /usr/sbin/nologin \
    --comment "Caddy web server" \
    caddy

sudo systemctl unmask --now caddy.service # in case you uninstalled apt's caddy

Paste this in sudo vi /etc/systemd/system/caddy.service

toml
# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
User=caddy
Group=caddy
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
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Start service via

bash
sudo systemctl daemon-reload
sudo systemctl enable caddy
sudo systemctl start caddy

Crowdsec

bash
curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec -y
sudo cscli bouncers add caddy

# test
sudo cscli decisions add -i $IP -d 1m
sudo cscli metrics show decisions

Resources