cs-caddy: Project Documentation
This document provides a comprehensive guide to building, deploying, and configuring a custom Caddy web server image integrated with the CrowdSec Security Engine. It covers the automated build pipeline using GitHub Actions and the local deployment using Docker Compose.
1. Project Overview
The goal of this project is to create a Caddy Docker image with a built-in CrowdSec bouncer. This provides two primary layers of security directly at the web server level, before traffic reaches your applications:
-
IP Address Blocking: Utilizes CrowdSec’s community blocklist and local security engine decisions to block known malicious IP addresses.
-
Web Application Firewall (WAF): The AppSec component inspects web requests for malicious patterns like SQL injection, XSS, and attempts to exploit known vulnerabilities (CVEs), providing an essential layer of application security.
The project uses an automated CI/CD pipeline with GitHub Actions to build and publish the image, ensuring it stays up-to-date with the latest versions of Caddy and the CrowdSec bouncer.
2. The Automated Build Pipeline (GitHub)
This setup uses three key files to create a fully automated build and release pipeline.
The caddy.Dockerfile
This file defines the steps to build the custom Caddy binary. It uses a multi-stage build to keep the final image small and secure.
Key Features:
-
Stage 1 (Builder): Uses a
golang
base image to compile Caddy with the necessary CrowdSec bouncer modules (http
,layer4
,appsec
). -
Stage 2 (Final Image): Uses the official
caddy:latest
image as a base and copies the custom-built binary from the builder stage. This ensures the final image has all the correct underlying dependencies.
# caddy.Dockerfile
# Stage 1: Build custom Caddy with CrowdSec bouncer
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
# Create a main.go file that imports Caddy and all the desired plugins
RUN tee main.go <<EOF
package main
import (
caddycmd "[github.com/caddyserver/caddy/v2/cmd](https://github.com/caddyserver/caddy/v2/cmd)"
_ "[github.com/caddyserver/caddy/v2/modules/standard](https://github.com/caddyserver/caddy/v2/modules/standard)"
_ "[github.com/hslatman/caddy-crowdsec-bouncer/appsec](https://github.com/hslatman/caddy-crowdsec-bouncer/appsec)"
_ "[github.com/hslatman/caddy-crowdsec-bouncer/http](https://github.com/hslatman/caddy-crowdsec-bouncer/http)"
_ "[github.com/hslatman/caddy-crowdsec-bouncer/layer4](https://github.com/hslatman/caddy-crowdsec-bouncer/layer4)"
)
func main() {
caddycmd.Main()
}
EOF
# Initialize a Go module and download all the necessary dependencies
RUN go mod init custom-caddy && go mod tidy
# Build CS-Caddy binary
RUN CGO_ENABLED=0 GOOS=linux go build \
-o /usr/bin/caddy \
-ldflags "-w -s" .
# Final stage: Use upstream Caddy base image
FROM caddy:latest
# Copy CS-Caddy binary from the builder stage
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
GitHub Actions Workflows
This project uses an intelligent, event-driven approach with three workflow files located in the .github/workflows/
directory.
a) check-caddy-release.yml
& check-bouncer-release.yml
(The Checkers)
These two files are nearly identical. Their only job is to periodically check the official GitHub repositories for Caddy and the caddy-crowdsec-bouncer
for new releases.
- How they work: They run on a schedule (e.g., daily). They fetch the latest release tag and use it as a key for
actions/cache
. If the key is new (meaning a new release has been published), the cache action misses. This triggers the final step, which usespeter-evans/repository-dispatch
to send an event (e.g.,caddy-release
) back to our own repository.
b) build-and-push.yml
(The Builder)
This is the main workflow that does all the heavy lifting. It is designed to be triggered by events, not its own schedule, making it highly efficient.
-
Triggers: It runs when:
-
A
repository_dispatch
event is received from one of the “checker” workflows. -
Code is pushed to the
main
branch. -
It is triggered manually via the GitHub UI.
-
-
Process:
-
Fetches the latest version numbers for Caddy and the bouncer.
-
Sets up QEMU and Docker Buildx for multi-arch builds (
linux/amd64
,linux/arm64
). -
Logs into both GitHub Container Registry (GHCR) and Docker Hub using secrets.
-
Builds the
caddy.Dockerfile
. -
Pushes the image to both registries with a comprehensive set of tags (
latest
, version-specific, etc.). -
Creates a formal GitHub Release on the repository to document the new build.
-
Setting Up Secrets
For the workflow to push to Docker Hub, you must add your credentials as encrypted secrets in your GitHub repository.
-
Go to your repository Settings > Secrets and variables > Actions.
-
Create a secret named
DOCKERHUB_USERNAME
with your Docker Hub username. -
Create a secret named
DOCKERHUB_TOKEN
with a Docker Hub Access Token.
The secrets.GITHUB_TOKEN
is provided automatically by GitHub Actions and does not need to be created manually.
3. Local Deployment Guide
This section explains how to use the custom Caddy image in a local Docker Compose setup.
Step 1: docker-compose.yml
Structure
Your docker-compose.yml
should define at least two services: caddy
and crowdsec
. They must be on the same Docker network to communicate.
# docker-compose.yml
version: '3.8'
services:
caddy:
# Use the custom image you built
image: ghcr.io/buildplan/cs-caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- your-network-name
volumes:
# Mount your Caddyfile and a directory for Caddy's logs
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/logs:/var/log/caddy
# Persist Caddy's internal data (like certificates)
- caddy_data:/data
- caddy_config:/config
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
networks:
- your-network-name
volumes:
# Mount the Caddy log file for acquisition
- ./caddy/logs/access.log:/var/log/caddy/access.log:ro
# Mount the entire CrowdSec config directory
- ./crowdsec/config:/etc/crowdsec
# Persist the CrowdSec database
- crowdsec_db:/var/lib/crowdsec/data
volumes:
caddy_data:
caddy_config:
crowdsec_db:
networks:
your-network-name:
Step 2: Configure CrowdSec for AppSec
To enable the Web Application Firewall (WAF), you need to tell your CrowdSec agent to activate its AppSec component.
-
Install the necessary collections:
docker compose exec crowdsec cscli collections install crowdsecurity/appsec-virtual-patching docker compose exec crowdsec cscli collections install crowdsecurity/appsec-generic-rules
-
Create the AppSec acquisition file: Inside the local directory you mounted to
/etc/crowdsec
(e.g.,./crowdsec/config/
), create a new directoryacquis.d
and place a file namedappsec.yaml
inside it.File path:
./crowdsec/config/acquis.d/appsec.yaml
Content:listen_addr: 0.0.0.0:7422 appsec_config: crowdsecurity/appsec-default name: caddy-appsec-listener source: appsec labels: type: appsec
Step 3: Configure the Caddyfile
This is the final piece. Your Caddyfile
tells Caddy how to handle requests, where to send them, and how to apply security.
-
Generate a Bouncer API Key:
docker compose exec crowdsec cscli bouncers add caddy-bouncer
Copy the generated key.
-
Create your
Caddyfile
:# ./caddy/Caddyfile # --- Global Options Block --- { # Define logging once, globally, to avoid parsing errors. log { output file /var/log/caddy/access.log { roll_size 10mb roll_keep 5 } format json level INFO } # --- CrowdSec Configuration --- crowdsec { # Point to the CrowdSec container using its service name api_url http://crowdsec:8080 api_key <your_crowdsec_api_key_goes_here> # Point to the AppSec component running inside CrowdSec appsec_url http://crowdsec:7422 } } # --- Example Site Block --- your-domain.com { # Use a route block to ensure security runs first route { # Apply IP blocking and WAF protection crowdsec appsec # Your application logic comes after security reverse_proxy your-app-container:8000 } }
Step 4: Launch and Verify
After setting up all the files, launch the stack:
docker compose up -d
To verify that everything is working, run:
docker compose exec crowdsec cscli metrics
Look for the “Acquisition Metrics” table to ensure Caddy logs are being parsed, and the “Appsec Metrics” table to confirm the WAF is processing requests.