Skip to content

Add opt-in Coraza WAF (OWASP CRS) support to the FrankenPHP image #41

Description

@hussainweb

Summary

Add opt-in Coraza WAF (OWASP CRS) support to the FrankenPHP variant of this image. The image ships everything needed — a Coraza-enabled binary, the rule sets, and a ready-made Caddy snippet — but the WAF is disabled by default and existing consumers see zero behavior change. A project that wants it (e.g. a Drupal site tired of scanner noise) enables it with minimal work: flip one setting and drop in its own exclusions/firewall config.

Motivation

Public Drupal sites are hammered by scanners probing non-Drupal paths (/wp-login.php, /.env, /.git/config, /xmlrpc.php, /vendor/phpunit/.../eval-stdin.php, ...). These fall through to index.php, causing a full Drupal bootstrap just to render a 404, and pollute watchdog/dblog. The existing Caddyfile hardening matchers catch some of this, but there is no query-string/body inspection and no blocking of well-known scanner paths. Doing the heavy lifting (compiling the module, fetching CRS) in the base image means each downstream project doesn't have to.

Opt-in design

  • Default: off. The shipped /etc/frankenphp/Caddyfile gains one line inside the :80 site, before php_server:

    import {$WAF_SNIPPET:/etc/frankenphp/waf/disabled.caddy}
    

    disabled.caddy is an empty (comment-only) file, so out of the box nothing changes. The global block also gets order coraza_waf first, which is harmless when the directive is unused.

  • Enabling is a one-line env var in the project's Dockerfile:

    ENV WAF_SNIPPET=/etc/frankenphp/waf/enabled.caddy

    enabled.caddy ships in the image and contains the full coraza_waf { ... } block (below).

  • Project-specific work is limited to config: the enabled.caddy block includes /etc/frankenphp/waf/rules/*.conf, so a project COPYs in its own rule files — Drupal-specific CRS exclusions, crs-setup tuning, extra blocked paths — without touching the Caddyfile or rebuilding FrankenPHP.

Current image facts (php8.5-frankenphp-trixie)

  • FrankenPHP v1.12.4 / Caddy v2.11.4 / PHP 8.5.7
  • Entry: docker-php-entrypointfrankenphp run --config /etc/frankenphp/Caddyfile --adapter caddyfile; no entrypoint change is needed.

What the base image ships

1. FrankenPHP binary built with the Coraza module

xcaddy build \
    --with github.com/dunglas/frankenphp=./ \
    --with github.com/dunglas/frankenphp/caddy=./caddy/ \
    --with github.com/dunglas/caddy-cbrotli \
    --with github.com/corazawaf/coraza-caddy/v2
  • Keep github.com/dunglas/caddy-cbrotli: the Caddyfile uses encode zstd br gzip, and the br encoder comes from that module. Dropping it breaks the config.
  • Cost when disabled: slightly larger binary; no runtime or behavior impact.

2. WAF files under /etc/frankenphp/waf/

  • enabled.caddy:

    coraza_waf {
        load_owasp_crs
        directives `
            Include @coraza.conf-recommended
            Include @crs-setup.conf.example
            Include @owasp_crs/*.conf
            Include /etc/frankenphp/waf/rules/*.conf
            SecRuleEngine On
        `
    }
    

    (load_owasp_crs embeds CRS via the coraza-coreruleset Go package; alternatively ship a pinned CRS release and Include its files directly for controlled upgrades.)

  • rules/10-scanner-paths.conf — sensible defaults, deterministic phase-1 hard blocks:

    • Deny *.php requests except Drupal front controllers (index.php, update.php, cron.php, authorize.php, install.php, rebuild.php) — kills wp-login.php, eval-stdin.php, and most scanner 404 noise.
    • Deny well-known probe paths: /.env, /.git, /.aws, /wp-admin, /wp-content, /wp-includes, /xmlrpc.php, /phpmyadmin, /_ignition, /cgi-bin, etc.

3. What an opting-in project provides

  • Its own rules/90-project.conf with CRS exclusions/tuning, e.g.:
    • CRS paranoia level 1 false-positives on admin node forms with HTML bodies are likely — exclusions for /admin/* paths or a raised tx.inbound_anomaly_score_threshold.
    • SecRequestBodyLimit / SecRequestBodyNoFilesLimit adjusted to expected upload sizes.
    • Optionally SecRuleEngine DetectionOnly during rollout, then On.

Test plan

  • Default build: behavior identical to today (regression check).
  • With WAF_SNIPPET enabled:
    • curl /wp-login.php, /.env, /.git/config403 from the WAF (not a Drupal-rendered 404)
    • SQLi probe in a query string (e.g. ?id=1 UNION SELECT ...) → 403
    • Normal pages, node add/edit with HTML body, file upload → pass through (with project exclusions applied)
    • encode zstd br gzip still negotiates Brotli (confirms cbrotli survived the custom build)
    • Optionally add a custom 403 page via Caddy's handle_errors

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions