Coraza WAF Caddy Module

repository·main·Indexed 20 days ago

https://github.com/corazawaf/coraza-caddy

A Caddy web server module providing Web Application Firewall (WAF) capabilities. It is 100% compatible with ModSecurity syntax and the OWASP Core Rule Set (CRS). The module is registered as `http.handlers.waf` and can be configured via the `coraza_waf` directive in the Caddyfile.

Tokens
4.6K
Snippets
13
Records
21
Agent score
71%

What's inside coraza-caddy

  1. Load OWASP Core Ruleset (CRS)

    main

    To use the OWASP Core Ruleset, use the load_owasp_crs option within the coraza_waf block. You must then manually Include the necessary CRS configuration files (like crs-setup.conf.example and the rule files) within the directives block, following the coraza-coreruleset documentation. Ensure SecRuleEngine On is set to enable enforcement.

    :8080 {
     coraza_waf {
      load_owasp_crs
      directives `
       Include @coraza.conf-recommended
       Include @crs-setup.conf.example
       Include @owasp_crs/*.conf
       SecRuleEngine On
      `
     }
    
     reverse_proxy httpbin:8081
    }
  2. Run the Coraza Caddy example deployment

    main

    The project provides a mage.go script to manage example deployments.

    Using Docker

    go run mage.go buildExample runExample
    curl -i localhost:8080/

    Using Local Setup

    To run locally, you need to run three separate processes:

    1. Mock Backend: Run go-httpbin on port 8081.
    2. Caddy with Coraza: Build the plugin and run Caddy using the provided example Caddyfile.
    3. Client: Use curl to test the setup.
    # Terminal 1: Mock Backend
    go run github.com/mccutchen/go-httpbin/v2/cmd/go-httpbin@v2.9.0 -port 8081
    
    # Terminal 2: Caddy
    go run mage.go buildCaddy
    ./build/caddy run --config example/Caddyfile --adapter caddyfile
    
    # Terminal 3: Test
    curl -i localhost:8080/
  3. How Coraza processes HTTP requests

    main

    The processRequest function handles the transformation of a standard Go *http.Request into a Coraza types.Transaction.

    Key behaviors include:

    • Connection & URI Mapping: It extracts the client address and port to populate the transaction's connection context and maps the request URI, method, and protocol.
    • Header Handling: It iterates through request headers, including manual restoration of the Host header (which Go's http package promotes to the Request.Host field) and Transfer-Encoding (to ensure rules detecting HTTP request smuggling, like CRS rule 920171, function correctly).
    • Body Inspection: If the transaction requires request body inspection (tx.IsRequestBodyAccessible()), the function reads the body from req.Body.
    • Body Re-initialization: To ensure the downstream HTTP handler can still read the request body after Coraza has inspected it, the function transparently re-initializes req.Body using an io.MultiReader that combines the Coraza buffer with the remaining bytes of the original body.
  4. How Coraza WAF handles requests in Caddy

    main

    The Coraza WAF module acts as a caddyhttp.MiddlewareHandler. For every incoming request, it performs the following lifecycle:

    1. Transaction Creation: A new Coraza transaction is created with a unique ID.
    2. Context Enrichment: The transaction ID is added to the Caddy replacer context as http.transaction_id.
    3. Request Processing: The module processes the connection, URI, headers, and body. If a rule violation triggers an interruption, the module returns a caddyhttp.HandlerError with a 500 Internal Server Error (or the status code specified by the interruption) and the transaction ID.
    4. Response Wrapping: If the request passes, the response writer is wrapped to allow the WAF to inspect the outgoing response.
    5. Cleanup: The transaction is closed and logging is processed via tx.ProcessLogging() and tx.Close().

    If SecRuleEngine is set to Off, the WAF will skip processing and pass the request directly to the next handler.

  5. Handle WAF interruptions in response status codes

    main

    When a Coraza transaction is interrupted (e.g., by a deny rule), the status code sent to the client may need to change. The internal logic uses obtainStatusCodeFromInterruptionOrDefault to determine the correct code:

    • If the interruption action is deny and a specific status is provided in the interruption object, that status is used.
    • If the action is deny but no status is provided, it defaults to 403.
    • Otherwise, it falls back to the original status code recorded during the WriteHeader call.
  6. Configure the coraza_waf directive

    main

    The coraza_waf directive allows you to define WAF rules within your Caddyfile.

    Critical Requirement: You must include order coraza_waf first in the global options block of your Caddyfile for the module to function correctly.

    Rules are defined within a directives block using backticks. You can use standard ModSecurity syntax, such as SecRule, SecAction, and Include to load external configuration files.

    {
        order coraza_waf first
    }
    
    http://127.0.0.1:8080 {
     coraza_waf {
      directives `
       SecAction "id:1,pass,log"
       SecRule REQUEST_URI "/test5" "id:2, deny, log, phase:1"
       SecRule REQUEST_URI "/test6" "id:4, deny, log, phase:3"
       Include file1.conf 
       Include file2.conf
       Include /some/path/*.conf
      `
     }
     reverse_proxy http://192.168.1.15:8080
    }
  7. How the Coraza Response Interceptor works

    main

    The rwInterceptor is a specialized http.ResponseWriter implementation used to facilitate WAF inspection of outgoing responses. It works by intercepting the standard response lifecycle:

    1. Header Interception: When WriteHeader is called, the interceptor captures the headers and status code, then passes them to the Coraza transaction (tx.ProcessResponseHeaders) to check for WAF rules that might interrupt the response.
    2. Body Buffering: If the response body is marked as accessible and processable by the WAF, the interceptor buffers the body instead of writing it directly to the downstream client. This allows the WAF to inspect the content in the response phase.
    3. Interruption Handling: If a WAF rule triggers an interruption (e.g., a deny action), the interceptor can cleanHeaders (remove existing headers) and overrideWriteHeader to change the status code (e.g., to 403 Forbidden) before the data reaches the client.
    4. Interface Preservation: The wrap function uses type assertions to detect if the original ResponseWriter implements http.Hijacker or http.Pusher, wrapping them accordingly to avoid the "observer effect" and maintain compatibility with advanced HTTP features.
  8. Run the FTW test suite via Docker Compose

    main

    The ftw/docker-compose.yml file defines a multi-container environment used to run the 'Framework for Testing WAFs' (FTW) against a Caddy instance equipped with the Coraza WAF module.

    To use this setup, you need a Caddy build context located in the parent directory (..) relative to the ftw directory. The environment includes:

    • caddy: The main service running the Coraza WAF module.
    • backend: A mock backend service (ghcr.io/coreruleset/albedo:0.3.0) listening on port 8081.
    • ftw: The testing engine that executes tests against the Caddy service.
    • Log services: coraza-logs and caddy-logs which process and redirect Caddy logs for easier inspection during testing.
    # Navigate to the ftw directory and run the compose setup
    cd ftw
    docker-compose up
  9. Run Coraza Caddy with Docker Compose

    main

    You can use the provided docker-compose.yml file to set up an end-to-end testing environment. This configuration spins up two services:

    1. httpbin: A backend service (mccutchen/go-httpbin:2.21.0) running on port 8081 used to simulate web traffic.
    2. caddy: The Coraza WAF Caddy module, built from the local context using the ./e2e/Dockerfile. It depends on httpbin and exposes port 8080.

    To use this setup, ensure you are in the directory containing the docker-compose.yml file and run docker compose up.

    services:
      httpbin:
        image: mccutchen/go-httpbin:2.21.0
        command: [ "/bin/go-httpbin", "-port", "8081" ]
        ports:
          - 8081:8081
    
    caddy:
        depends_on:
          - httpbin
        build:
          context: ..
          dockerfile: ./e2e/Dockerfile
        environment:
          - HTTPBIN_HOST=httpbin
        ports:
          - 8080:8080
  10. Use the Coraza debuglog API for structured logging

    main

    The Coraza Caddy module uses a structured logging system based on the debuglog interface. It wraps Uber's zap logger to provide a fluent API for emitting logs at different severity levels.

    Logging follows a two-step pattern:

    1. Initialize a level: Call Trace(), Debug(), Info(), Warn(), or Error() on the logger to obtain a debuglog.Event.
    2. Build the event: Use type-specific methods (like Str, Int, Err) to attach structured fields to the event.
    3. Emit the message: Call .Msg("message") to finalize and write the log entry.

    If the current logger level is lower than the requested level, the methods return a noopEvent, which performs no operations and has IsEnabled() == false, making it efficient for high-frequency logging.

    // Example of structured logging with Coraza
    logger.Info().
        Str("user_id", "12345").
        Int("request_id", 9876).
        Err(err).
        Msg("Processed request successfully")
  11. Integrate Coraza WAF into Caddy

    main

    Coraza WAF is available as a Caddy HTTP handler module with the ID http.handlers.waf. You can use it by adding the coraza_waf directive to your Caddyfile.

    It supports loading the OWASP Core Rule Set (CRS) and defining custom WAF directives or including external rule files.

    Note: The include configuration field is deprecated. You should instead use the include directive inside the directives field for future compatibility.

    example.com {
        coraza_waf {
            load_owasp_crs
            directives "SecRuleEngine On"
            include /path/to/custom/rules.conf
        }
        reverse_proxy localhost:8080
    }