FrankenPHP & Caddy WAF
We've been working with FrankenPHP for a long time and wanted to add an extra layer of security to our projects. Since FrankenPHP is built with Caddy, it can easily extend it with additional Caddy modules, such as the Caddy WAF module.
The Caddy WAF module provides an HTTP handler middleware that inspects requests and responses, acting as a web application firewall. This is particularly useful for protecting my applications from common web attacks.
To add the Caddy WAF module to FrankenPHP, you need to build FrankenPHP from scratch. The FrankenPHP docs provide a good starting point for the Dockerfile needed:
FROM dunglas/frankenphp:1.12-builder-php8.5-trixie AS builder
# Copy xcaddy in the builder image
COPY /usr/bin/xcaddy /usr/bin/xcaddy
# CGO must be enabled to build FrankenPHP
RUN CGO_ENABLED=1 \
XCADDY_SETCAP=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,with_ui" \
CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
--with github.com/dunglas/caddy-cbrotli \
# Mercure and Vulcain are included in the official build, but feel free to remove them
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy \
# Add Caddy WAF module
--with github.com/fabriziosalmi/caddy-waf
FROM dunglas/frankenphp:1.12-php8.5-trixie AS runner
# Replace the official binary by the one contained your custom modules
COPY /usr/local/bin/frankenphp /usr/local/bin/frankenphp
Once the build is ready, you can use a custom Caddyfile to enable and configure the WAF module:
{
{$CADDY_GLOBAL_OPTIONS}
frankenphp {
{$FRANKENPHP_CONFIG}
}
}
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
log {
output stdout
}
root * /app/public
encode zstd br gzip
{$CADDY_SERVER_EXTRA_DIRECTIVES}
@waf path /waf /waf_metrics*
route {
waf {
metrics_endpoint /waf_metrics
dashboard /waf
dns_blacklist_file /etc/caddy/waf/blacklist/dns_blacklist.txt
ip_blacklist_file /etc/caddy/waf/blacklist/ip_blacklist.txt
}
php_server
}
}
Besides DNS and IP backlist files, the WAF module also supports custom rule files in the following format:
{
"id": "unique-rule-id",
"phase": 1,
"pattern": "(?i)example",
"targets": ["URI", "ARGS"],
"severity": "HIGH",
"score": 8,
"action": "block",
"description": "Human-readable description",
"priority": 10
}
In the GitHub repo, there are scripts to convert OWASP ModSecurity Core Rule Sets, the Spiderlabs rules and others into the Caddy WAF JSON format. Add the converted JSON files to the Docker image and reference these files in the Caddyfile:
route {
waf {
metrics_endpoint /waf_metrics
dashboard /waf
dns_blacklist_file /etc/caddy/waf/blacklist/dns_blacklist.txt
ip_blacklist_file /etc/caddy/waf/blacklist/ip_blacklist.txt
rule_file /etc/caddy/waf/rules/rules.json
rule_file /etc/caddy/waf/rules/graphql.json
rule_file /etc/caddy/waf/rules/rce.json
rule_file /etc/caddy/waf/rules/sql-injection.json
}
}
Caddy WAF also supports hot reloading of rule files, allowing for seamless updates without requiring a restart of the Caddy server. Whenever a file change is detected, the rule files will be automatically reloaded.
In today's day and age, using a web application firewall is a sensible measure to protect your application. If budget constraints prevent you from using services like Cloudflare, and you're already leveraging Caddy in your setup, installing the Caddy WAF module can be a viable solution.
Alternatively, if you're not currently using Caddy or FrankenPHP, you could consider installing it as a reverse proxy in front of your application, depending on your specific needs.
