Table of Contents
This image is designed to be used with Docker Compose and includes the following modules:
- Docker proxy module for Caddy configuration via Docker labels
- Cloudflare DNS-01 module for DNS-01 domain control validation and wildcard certs
- Cloudflare IP module for trusting proxy headers from Cloudflare CDN
- Crowdsec bouncer module for layer 7 enforcement of Crowdsec decisions
It is statically-linked and built with a non-root distroless base image that does not include a shell or other OS utilities.
π If you need more details about how to configure Caddy via the Docker proxy module please refer to the documentation.
The main purpose of creating this image is to have DNS challenge for wildcard domains on Cloudflare and optional Crowdsec integration. With the Cloudflare IP module, we can dynamically trust their CDN IP addresses for Cloudflare-proxied domains, enabling Caddy to resolve the real client IP addresses for inbound connections. Crowdsec will then use this for layer 7 enforcement of decisions.
Renovate scans for and submits pull requests for dependency updates as they become available. Whenever the repository is updated, a new image is built and pushed by Github Actions.
π° It will work on any Linux box amd64 or arm64.
You will need to have:
- π³ Docker
- π docker-compose
- Domain name
- Cloudflare DNS Zone
β¬οΈ A docker-compose.yml example with a wildcard domain, external services, trusted proxies, Crowdsec integration, and least-privilege containers:
services:
caddy:
container_name: caddy
image: sholdee/caddy-proxy-cloudflare:latest
user: 65532:65532 # use non-root user
group_add: # add docker group ID from /etc/group for docker socket access
- 123
privileged: false
cap_drop:
- ALL # drop all capabilities
cap_add:
- NET_BIND_SERVICE # add NET_BIND_SERVICE to bind ports <=1024
security_opt:
- no-new-privileges:true # deny privilege escalation
read_only: true # set read-only root filesystem
tmpfs: # autosaved config does not need to be persisted
- /config
networks:
- caddy
dns: # set container DNS to Cloudflare
- 1.1.1.1
- 1.0.0.1
restart: unless-stopped
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" # need socket to read labels and events
- "/opt/docker/caddy/data:/data:rw" # need for certiticate storage, make sure to chown -R 65532:65532
ports:
- "80:80/tcp"
- "443:443/tcp"
labels: # global options
caddy.email: "[email protected]" # need for ACME cert regsitration account
caddy.acme_dns: "cloudflare ${CF_TOKEN}" # replace ${CF_TOKEN} with your Cloudflare API token
caddy.servers.trusted_proxies: "cloudflare" # trust Cloudflare IP proxy headers via caddy-cloudflare-ip module
caddy.servers.trusted_proxies.interval: "1h" # optional Cloudflare IP refresh interval, default is 24h
caddy.servers.trusted_proxies.timeout: "15s" # time to wait for response from Cloudflare, default is no timeout
caddy.servers.client_ip_headers: "Cf-Connecting-Ip" # use Cf-Connecting-Ip header as the client IP
caddy.servers.trusted_proxies_strict: # use strict processing of client_ip_headers
caddy.log.output: "stdout" # set global option to log to stdout
caddy.persist_config: "off" # persist_config not needed with docker proxy module configuration
caddy.crowdsec.api_url: "http://crowdsec:8080" # api url for crowdsec container
caddy.crowdsec.api_key: "${CROWDSEC_API_KEY}" # crowdsec api key that is set for caddy
caddy.crowdsec.ticker_interval: "3s" # crowdsec local api poll interval for stream bouncer
caddy_0: "*.domain.com" # example labels for proxying an external service by IP
caddy_0.log: # enable logging for *.domain.com block
caddy_0.1_@service: "host service.domain.com"
caddy_0.1_handle: "@service"
caddy_0.1_handle.route.crowdsec: # add crowdsec to this upstream via route block
caddy_0.1_handle.route.reverse_proxy: http://10.1.1.10:8080
caddy_0.1_handle.route.reverse_proxy.header_up: "X-Forwarded-For {client_ip}" # set X-Forwarded-For header to client_ip
caddy_1: "*.domain.com"
caddy_1.1_@example: "host example.domain.com"
caddy_1.1_handle: "@example"
caddy_1.1_handle.route.crowdsec:
caddy_1.1_handle.route.reverse_proxy: http://10.1.1.20:8000
caddy_1.1_handle.route.reverse_proxy.header_up: "X-Forwarded-For {client_ip}"
crowdsec:
container_name: crowdsec
image: crowdsecurity/crowdsec:latest
user: 65532:65532
group_add:
- 123
privileged: false
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
networks:
- caddy
environment:
- TZ=America/Chicago
- GID=65532
- USE_WAL=true
- COLLECTIONS=crowdsecurity/linux crowdsecurity/caddy crowdsecurity/whitelist-good-actors # base collections for caddy
- PARSERS=crowdsecurity/whitelists # whitelist internal ip addresses
- BOUNCER_KEY_CADDY=${CROWDSEC_API_KEY} # the api key that will be set for caddy
- ENROLL_KEY=${ENROLL_KEY} # optional enrollment key for crowdsec hub
volumes:
- /opt/docker/crowdsec/data:/var/lib/crowdsec/data:rw # crowdsec data dir
- /opt/docker/crowdsec/config:/etc/crowdsec:rw # crowdsec config dir
- /opt/docker/crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro # our log sources config
- /var/run/docker.sock:/var/run/docker.sock:ro # need to mount docker socket to read stdout logs
restart: unless-stopped
whoami:
container_name: whoami
image: jwilder/whoami:latest
hostname: TheDocker # Expected result using curl
user: 65532:65532
privileged: false
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
read_only: true
networks:
- caddy
restart: unless-stopped
labels:
caddy: "*.domain.com"
caddy.1_@whoami: "host whoami.domain.com"
caddy.1_handle: "@whoami"
caddy.1_handle.route.crowdsec:
caddy.1_handle.route.reverse_proxy: "{{upstreams 8000}}" # set http port that caddy will send traffic
caddy.1_handle.route.reverse_proxy.header_up: "X-Forwarded-For {client_ip}"
If using Crowdsec, you will also need to create and mount acquis.yaml to the container for your log source(s):
source: docker
container_name:
- caddy
labels:
type: caddy
Please get your scoped Cloudflare API token from here.
β¬οΈ Go on TOP βοΈ
β¬οΈ Your can run the following command to see that is working:
$ curl --insecure -vvI https://test.ionut.vip 2>&1 | awk 'BEGIN { cert=0 } /^\* Server certificate:/ { cert=1 } /^\*/ { if (cert) print }'
* Server certificate:
* subject: CN=test.ionut.vip ################################ CA from Let's Enctrypt Staging
* start date: Jan 5 15:15:00 2021 GMT
* expire date: Apr 5 15:15:00 2021 GMT
* issuer: CN=Fake LE Intermediate X1 ######################## This is telling you that acme is working as expected!
* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fc02180ec00)
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
$ curl -k https://test.ionut.vip
I'm TheDocker################################### Expected result from hostname above
healthy
word. This is telling you that docker is running healthcheck itself in order to make sure it is working properly.
β¬οΈ Please test yourself using the following command:
β― docker inspect --format "{{json .State.Health }}" caddy | jq
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2021-01-04T11:10:49.2975799Z",
"End": "2021-01-04T11:10:49.3836437Z",
"ExitCode": 0,
"Output": ""
}
]
}
To verify that Crowdsec is parsing logs, check the metrics:
β― sudo docker exec crowdsec cscli metrics
Acquisition Metrics:
+-----------------------------------------+------------+--------------+----------------+------------------------+-------------------+
| Source | Lines read | Lines parsed | Lines unparsed | Lines poured to bucket | Lines whitelisted |
+-----------------------------------------+------------+--------------+----------------+------------------------+-------------------+
| docker:caddy | 14.90k | 14.90k | - | 1.11k | 3.11k |
+-----------------------------------------+------------+--------------+----------------+------------------------+-------------------+
ποΈ Distributed under the Eclipse Public License 2.0. See LICENSE for more information.
π΄ Please free to open a ticket on Github.
- π @lucaslorentz π
- π β’οΈ @Caddy π₯ and its huge ποΈ community β
- π dns.providers.cloudflare π
- π http.ip_sources.cloudflare π₯
- π crowdsec π
β¬οΈ Go on TOP βοΈ