Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to apply WAF globally ? #48

Open
LeVraiRoiDHyrule opened this issue Jan 30, 2025 · 10 comments
Open

How to apply WAF globally ? #48

LeVraiRoiDHyrule opened this issue Jan 30, 2025 · 10 comments

Comments

@LeVraiRoiDHyrule
Copy link

LeVraiRoiDHyrule commented Jan 30, 2025

Hi,

I am a begginer with caddy-WAF. I would like to apply it globally on all my subdomains, while the only setting specific to each subdomains would be rule edits to avoid false positives (I was using Bunkerweb previously, which by default uses owasp CRS too, and there is A LOT of false positives with the services I am using).

I can't find what the Caddyfile structure would be to achieve this.
Could someone help me with some examples ?

Thanks in advance for any answer, have a nice day.

My Dockerfile is the following:

        ARG CADDY_VERSION=2
        
        # # Deps # #
        
        # WAF
        FROM python:slim AS deps-stage
        
        WORKDIR /caddy-waf
        
        ADD https://github.com/fabriziosalmi/caddy-waf.git /caddy-waf
        
        RUN pip install --no-cache-dir requests tqdm
        
        RUN python3 get_caddy_feeds.py
        
        # # Build # #
        FROM caddy:${CADDY_VERSION}-builder AS build-stage
        
        # WAF
        ADD https://github.com/fabriziosalmi/caddy-waf.git /caddy-waf
        
        # Sablier
        ADD https://github.com/sablierapp/sablier.git /sablier
        
        RUN xcaddy build \
            --with github.com/yroc92/postgres-storage \
            --with github.com/caddy-dns/desec \
            --with github.com/lucaslorentz/caddy-docker-proxy/v2 \
            --with github.com/sablierapp/sablier/plugins/caddy=/sablier/plugins/caddy \
        #    --with github.com/hslatman/caddy-crowdsec-bouncer/http \
            --with github.com/fabriziosalmi/caddy-waf=/caddy-waf
            
        # # Release # #
        FROM caddy:${CADDY_VERSION}-alpine AS release-stage
        
        # WAF
        COPY --from=deps-stage /caddy-waf/rules.json owasp_rules.json
        COPY --from=deps-stage /caddy-waf/ip_blacklist.txt ip_blacklist.txt
        COPY --from=deps-stage /caddy-waf/dns_blacklist.txt dns_blacklist.txt
        ADD https://git.io/GeoLite2-Country.mmdb GeoLite2-Country.mmdb
        
        # Binary
        COPY --from=build-stage /usr/bin/caddy /usr/bin/caddy
        
        CMD ["caddy", "docker-proxy"]

I am trying to apply it this way globally, so far:

{
	route {
		waf {
			dns_blacklist_file dns_blacklist.txt
			ip_blacklist_file ip_blacklist.txt
			metrics_endpoint /waf_metrics
			rule_file owasp_rules.json
		}
		@wafmetrics {
			path /waf_metrics
		}
		handle @wafmetrics {
			
		}
	}
}

Thanks in advance for any answer

@fabriziosalmi
Copy link
Owner

fabriziosalmi commented Jan 31, 2025

Helo, here's how you can structure your Caddyfile:

{
    # Global Options (can define metrics)
    waf {
        metrics_endpoint /waf_metrics
    }

  route {
    @wafmetrics {
      path /waf_metrics
    }

    handle @wafmetrics {
        #  This empty handler allows the metrics endpoint to be called.
		respond "" 200
    }
  }
}


# Subdomain 1: Uses default rules and blacklists
subdomain1.mydomain.com {
   handle {
		waf {
                  rule_file owasp_rules.json
                  ip_blacklist_file ip_blacklist.txt
                  dns_blacklist_file dns_blacklist.txt
        }
        reverse_proxy localhost:8080
       }
}

# Subdomain 2: Uses custom rules
subdomain2.mydomain.com {
   handle {
		waf {
			rule_file custom_rules_subdomain2.json
			ip_blacklist_file ip_blacklist.txt
                        dns_blacklist_file dns_blacklist.txt
		}
       reverse_proxy localhost:8081
    }
}

# Subdomain 3: Uses default rules and blacklists (again)
subdomain3.mydomain.com {
   handle {
		waf {
                  rule_file owasp_rules.json
                  ip_blacklist_file ip_blacklist.txt
                 dns_blacklist_file dns_blacklist.txt
        }
		reverse_proxy localhost:8082
	}
}

# Add more subdomains as needed, always configure each handle with a reverse proxy

please let me know if this works since docs needs some improvements in such area and this kind of additions will be more than useful for others caddy users :)

@AdamWHY2K
Copy link
Contributor

Doesn't seem to be working for me: Error: adapting config using caddyfile: Caddyfile:3: unrecognized global option: waf

Caddyfile:

{
        # Global Options (can define metrics)
        waf {
                metrics_endpoint /waf_metrics
        }

        route {
                @wafmetrics {
                        path /waf_metrics
                }

                handle @wafmetrics {
                        #  This empty handler allows the metrics endpoint to be called.
                        respond "" 200
                }
        }
}

1.com {
        handle {
                waf {
                        rule_file owasp_rules.json
                        ip_blacklist_file ip_blacklist.txt
                        dns_blacklist_file dns_blacklist.txt
                        block_countries /etc/caddy/GeoLite2-Country.mmdb AF BH BY CN EG HN IL IN IQ IR JO KG KP KW KZ LB LR PK PS QA RS RU SA SD SO SS SV SY TJ TM TR TT US UZ
                        rate_limit {
                                requests 100
                                window 10s
                                cleanup_interval 5m
                                # paths /api/v1/.* /admin/.*   # List of regex patterns
                                match_all_paths true
                        }
                        tor {
                                enabled true
                                tor_ip_blacklist_file tor_ip_blacklist.txt
                                update_interval 24h
                                retry_on_failure true
                                retry_interval 1h
                        }
                }
                reverse_proxy localhost:3000
        }
}

2.com {
        handle {
                waf {
                        rule_file owasp_rules.json
                        ip_blacklist_file ip_blacklist.txt
                        dns_blacklist_file dns_blacklist.txt
                }
                reverse_proxy localhost:8096
        }
}

3.com {
        handle {
                waf {
                        rule_file owasp_rules.json
                        ip_blacklist_file ip_blacklist.txt
                        dns_blacklist_file dns_blacklist.txt
                }
                reverse_proxy localhost:5000
        }
}

@LeVraiRoiDHyrule
Copy link
Author

I am having the same error with this example.

@fabriziosalmi
Copy link
Owner

Hello buddies, can you refer to this updated example and provide feedback? It will be helpful to improve docs 🙏

{
    route {
        @wafmetrics {
            path /waf_metrics
        }
        handle @wafmetrics {
            respond "" 200
        }
    }
}

1.com {
    handle {
        waf {
            metrics_endpoint /waf_metrics # Still needed here for WAF to register the endpoint
            rule_file owasp_rules.json
            ip_blacklist_file ip_blacklist.txt
            dns_blacklist_file dns_blacklist.txt
            block_countries /etc/caddy/GeoLite2-Country.mmdb AF BH BY CN EG HN IL IN IQ IR JO KG KP KW KZ LB LR PK PS QA RS RU SA SD SO SS SV SY TJ TM TR TT US UZ
            rate_limit {
                requests 100
                window 10s
                cleanup_interval 5m
                match_all_paths true
            }
            tor {
                enabled true
                tor_ip_blacklist_file tor_ip_blacklist.txt
                update_interval 24h
                retry_on_failure true
                retry_interval 1h
            }
        }
        reverse_proxy localhost:3000
    }
}

2.com {
    handle {
        waf {
            metrics_endpoint /waf_metrics # Still needed here for WAF to register the endpoint
            rule_file custom_rules_subdomain2.json  # Custom rule file for subdomain 2
            ip_blacklist_file ip_blacklist.txt
            dns_blacklist_file dns_blacklist.txt
        }
        reverse_proxy localhost:8096
    }
}

3.com {
    handle {
        waf {
            metrics_endpoint /waf_metrics # Still needed here for WAF to register the endpoint
            rule_file owasp_rules.json
            ip_blacklist_file ip_blacklist.txt
            dns_blacklist_file dns_blacklist.txt
        }
        reverse_proxy localhost:5000
    }
}

@fabriziosalmi fabriziosalmi reopened this Feb 3, 2025
@AdamWHY2K
Copy link
Contributor

Error: adapting config using caddyfile: Caddyfile:2: unrecognized global option: route

Caddyfile:

{
        route {
                @wafmetrics {
                        path /waf_metrics
                }
                handle @wafmetrics {
                        respond "" 200
                }
        }
}

1.com {
        handle {
                waf {
                        metrics_endpoint /waf_metrics
                        rule_file rules.json
                        ip_blacklist_file ip_blacklist.txt
                        dns_blacklist_file dns_blacklist.txt
                        block_countries /etc/caddy/GeoLite2-Country.mmdb AF BH BY CN EG HN IL IN IQ IR JO KG KP KW KZ LB LR PK PS QA RS RU SA SD SO SS SV SY TJ TM TR TT US UZ
                        rate_limit {
                                requests 100
                                window 10s
                                cleanup_interval 5m
                                match_all_paths true
                        }
                        tor {
                                enabled true
                                tor_ip_blacklist_file tor_ip_blacklist.txt
                                update_interval 24h
                                retry_on_failure true
                                retry_interval 1h
                        }
                }
                reverse_proxy localhost:3000
        }
}

2.com {
        handle {
                waf {
                        metrics_endpoint /waf_metrics
                        rule_file rules.json
                        ip_blacklist_file ip_blacklist.txt
                        dns_blacklist_file dns_blacklist.txt
                }
                reverse_proxy localhost:8096
        }
}

3.com {
        handle {
                waf {
                        metrics_endpoint /waf_metrics
                        rule_file rules.json
                        ip_blacklist_file ip_blacklist.txt
                        dns_blacklist_file dns_blacklist.txt
                }
                reverse_proxy localhost:5000
        }
}

@LeVraiRoiDHyrule
Copy link
Author

LeVraiRoiDHyrule commented Feb 9, 2025

Hi, same error here. Route is apparently not a global option.
Could you define a basic config with as little settings as possible with a clear distinction between what is global config and what is per-subdomain config ?
Thanks in advance for your help.

My current caddyfile is the following:

{
	acme_dns cloudflare REDACTED
	grace_period 10s
	order waf first
	storage postgres {
		connection_string postgres://caddy:REDACTED@caddy-db:5432/caddy?sslmode=disable
	}
}
(wafsnippet) {
	waf {
		anomaly_threshold 5
		dns_blacklist_file dns_blacklist.txt
		ip_blacklist_file ip_blacklist.txt
		log_json
		log_severity error
		rate_limit {
			cleanup_interval 5m
			match_all_paths true
			requests 1000
			window 1m
		}
		redact_sensitive_data
		rule_file owasp_rules.json
		whitelist_countries GeoLite2-Country.mmdb FR CH
	}
}
openmediavault.mydomain.com {
	import wafsnippet
	reverse_proxy http://192.168.1.27:10997
}

I don't quite understand what needs to be global and what needs to be per website. I would like to use snippets as much as possible to avoid redundancy.

I have added order waf first as I get this error message without it: Removing invalid block: directive 'waf' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option. I am not quite sure what are the implications between using a route block or the order global option.

With this setup, when trying to access openmediavault.mydomain.com, I get the following error:

msg=ERROR http: panic serving arg="runtime error: invalid memory address or nil pointer dereference" trace="goroutine 198 [running]:
github.com/quic-go/quic-go/http3.(*Server).handleRequest.func2.1()
\tgithub.com/quic-go/[email protected]/http3/server.go:682 +0xb9
panic({0x17df6a0?, 0x2bce670?})
\truntime/panic.go:785 +0x132
github.com/fabriziosalmi/caddy-waf.(*RequestValueExtractor).extractSingleValue(0xc00090d510, {0xc000a4d500?, 0x1ef96b8?}, 0xc00134a3c0, {0x0, 0x0})
\tgithub.com/fabriziosalmi/[email protected]/request.go:135 +0x12c1
github.com/fabriziosalmi/caddy-waf.(*RequestValueExtractor).ExtractValue(0xc00090d510, {0xc000a4d500?, 0x172cdc0?}, 0xc00134a3c0, {0x0, 0x0})
\tgithub.com/fabriziosalmi/[email protected]/request.go:76 +0xe5
github.com/fabriziosalmi/caddy-waf.(*Middleware).extractValue(...)
\tgithub.com/fabriziosalmi/[email protected]/caddywaf.go:530
github.com/fabriziosalmi/caddy-waf.(*Middleware).handlePhase(0xc000636908, {0x1f16510, 0xc000a25200}, 0xc000b4ab40, 0x2, 0xc0006b5270)
\tgithub.com/fabriziosalmi/[email protected]/handler.go:323 +0x2179
github.com/fabriziosalmi/caddy-waf.(*Middleware).isPhaseBlocked(0xc000636908, {0x1f16510, 0xc000a25200}, 0x1eff4b0?, 0x172d140?, 0xc0006b5270)
\tgithub.com/fabriziosalmi/[email protected]/handler.go:80 +0x27
github.com/fabriziosalmi/caddy-waf.(*Middleware).ServeHTTP(0xc000636908, {0x1f16510, 0xc000a25200}, 0xc000b4a780, {0x1f07ac0, 0xc000b16e10})
\tgithub.com/fabriziosalmi/[email protected]/handler.go:39 +0x2c7
github.com/caddyserver/caddy/v2/modules/caddyhttp.wrapMiddleware.func1.1({0x1f16510, 0xc000a25200}, 0xc000b4a780)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:331 +0xd2
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0x1f07ac0?, {0x1f16510?, 0xc000a25200?}, 0xc000b4a780?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.RouteList.Compile.wrapRoute.func1.1({0x1f16510, 0xc000a25200}, 0xc000b4a780)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:298 +0x26d
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0xc000986a20?, {0x1f16510?, 0xc000a25200?}, 0x1f07ac0?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*Subroute).ServeHTTP(0xc0002f6340, {0x1f16510, 0xc000a25200}, 0xc000b4a780, {0x1f07ac0, 0x1d17a08})
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/subroute.go:74 +0x67
github.com/caddyserver/caddy/v2/modules/caddyhttp.wrapMiddleware.func1.1({0x1f16510, 0xc000a25200}, 0xc000b4a780)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:331 +0xd2
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0x1f07ac0?, {0x1f16510?, 0xc000a25200?}, 0xc000b4a780?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.RouteList.Compile.wrapRoute.func1.1({0x1f16510, 0xc000a25200}, 0xc000b4a780)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:298 +0x26d
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0x0?, {0x1f16510?, 0xc000a25200?}, 0x410c05?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*Server).enforcementHandler(0xc000a25280?, {0x1f16510?, 0xc000a25200?}, 0x87548a?, {0x1f07ac0?, 0xc0003782a0?})
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/server.go:482 +0x24b
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*App).Provision.(*Server).wrapPrimaryRoute.func2({0x1f16510?, 0xc000a25200?}, 0x4eb24f?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/server.go:458 +0x35
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0xc000a26300?, {0x1f16510?, 0xc000a25200?}, 0xc000b4a780?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*Server).ServeHTTP(0xc000a2f188, {0x1f16510, 0xc000a25200}, 0xc000b4a500)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/server.go:370 +0xc88
github.com/quic-go/quic-go/http3.(*Server).handleRequest.func2(0x1f194f0?, 0xc00072b4a0?, {0x1f081e0?, 0xc000a2f188?}, 0xc000822680?, 0x1c?)
\tgithub.com/quic-go/[email protected]/http3/server.go:690 +0x5f
github.com/quic-go/quic-go/http3.(*Server).handleRequest(0xc0006480f0, 0xc001c7f6c0, {0x1f27a78, 0xc000b34e40}, 0xc00072b590, 0xc000d871a0)
\tgithub.com/quic-go/[email protected]/http3/server.go:691 +0xac5
github.com/quic-go/quic-go/http3.(*Server).handleConn.func1()
\tgithub.com/quic-go/[email protected]/http3/server.go:578 +0x5b
created by github.com/quic-go/quic-go/http3.(*Server).handleConn in goroutine 191
\tgithub.com/quic-go/[email protected]/http3/server.go:574 +0x32d
"

@fabriziosalmi
Copy link
Owner

fabriziosalmi commented Feb 9, 2025

Maybe we need to investigate into that a bit more :)

This approach uses snippets to define the WAF configuration once and then import it into each site, minimizing repetition.

{
  acme_dns cloudflare REDACTED # Replace REDACTED
  grace_period 10s
  storage postgres {
    connection_string postgres://caddy:REDACTED@caddy-db:5432/caddy?sslmode=disable # Replace REDACTED
  }
}

(waf_settings) {
  waf {
    metrics_endpoint /waf_metrics # Important: must be defined in each site, but we define it in the snippet
    rule_file owasp_rules.json
    ip_blacklist_file ip_blacklist.txt
    dns_blacklist_file dns_blacklist.txt

    # Optional settings
    anomaly_threshold 5
    log_json
    log_severity error
    redact_sensitive_data
    # whitelist_countries GeoLite2-Country.mmdb FR CH # Commented out for broader usability


    rate_limit {
      cleanup_interval 5m
      match_all_paths true
      requests 1000
      window 1m
    }
  }
}

openmediavault.mydomain.com {
  import waf_settings
  reverse_proxy http://192.168.1.27:10997
  log {
      output stdout
  }
}

othersubdomain.mydomain.com {
    import waf_settings
    reverse_proxy localhost:8080
    log {
        output stdout
    }
}

# Add more subdomains as needed

Explanation:

  • Global Options (Curly Braces): These options affect the entire Caddy instance (ACME DNS, storage).
  • (waf_settings) Snippet: This defines a reusable block of WAF configuration.
  • Crucially, metrics_endpoint /waf_metrics is included in the snippet. This is necessary because the WAF plugin needs to register the endpoint within each site.
  • The other WAF settings (rule file, blacklists, etc.) are also in the snippet.
  • Optional settings like anomaly_threshold, log_json, redact_sensitive_data and the rate_limit are included in the snippet for convenience. You can customize these per-site if needed by overriding them in the site block after the import.
  • whitelist_countries is commented out because it requires GeoLite2-Country.mmdb. I have omitted this line to avoid the need for file creation for testing.
  • Site Blocks (openmediavault.mydomain.com, etc.): import waf_settings includes the WAF configuration from the snippet.
  • reverse_proxy directs traffic to your backend server.
  • log {output stdout} configures logging.
  • No Global route Block: The route block is completely removed from the global scope.
  • No order waf first: Remove this line. The placement of your waf import takes care of this directive.
  • File Paths: Ensure that owasp_rules.json, ip_blacklist.txt, and dns_blacklist.txt exist in the correct location (relative to where Caddy is running). Create empty files if necessary.
  • metrics_endpoint: The metrics endpoint /waf_metrics will be exposed for each site where you import the waf_settings snippet. Ensure this doesn't create conflicts.

Customization: If you need to customize the WAF configuration for a specific site, you can override settings after the import:

mysubdomain.example.com {
  import waf_settings
  reverse_proxy localhost:8081

  waf {
    anomaly_threshold 10  # Override the anomaly threshold for this site
    rule_file custom_rules.json #Override the file path as well
  }
}

Order of tests:

  • Make sure that the waf import/directive comes first to correctly intercept HTTP requests.
  • Test the Metrics Endpoint: Access /waf_metrics for each of your subdomains (e.g., http://openmediavault.mydomain.com/waf_metrics). You should not get your reverse proxy's default response. Instead, the WAF should respond.

Additional examples

Here a series of Caddyfile examples that gradually increase in complexity, building from the absolute simplest to a more practical configuration incorporating the caddy-waf plugin.

Level 1: The Bare Minimum (Illustrating Global vs. Site)

This example shows the most basic configuration and highlights the difference between global and site-specific directives.

{
    auto_https off  # Global option: Disables automatic HTTPS
}

:8080 {  # Site block: Listens on port 8080
    respond "Hello, world!"  # Simple response
}
  • The {} block contains global options that affect Caddy's overall behavior.
  • The :8080 {} block defines the configuration for a specific site (in this case, serving a simple "Hello, world!" response).

Level 2: Adding Basic Logging (Site-Specific)

This example adds basic logging to the site configuration.

{
    auto_https off
}

:8080 {
    log {  # Site-specific logging configuration
        output stdout  # Output logs to the standard output (terminal)
        format console # Use a human-readable format
        level INFO    # Log informational messages and above
    }
    respond "Hello, world!"
}
  • The log {} block is placed inside the :8080 {} site block. This means the logging configuration only applies to requests handled by this site.

Level 3: Introducing handle Blocks (Route Management)

This example introduces handle blocks for more precise route management, while keeping things relatively simple.

{
    auto_https off
}

:8080 {
    log {
        output stdout
        format console
        level INFO
    }

    handle /greet {  # Only match requests to the /greet path
        respond "Greetings!" 200
    }

    handle { # Catch-all: Match all other requests
        respond "Hello, world!" 200
    }
}
  • The handle /greet block matches only requests to the /greet path.
  • The bare handle block acts as a catch-all, handling all other requests that don't match the /greet path. The order of handle blocks is important: Caddy processes them in the order they appear in the Caddyfile.

Level 4: Integrating caddy-waf (Minimal Implementation)

This example shows the minimal integration of caddy-waf within a Caddyfile. Crucially, we place it before the other handle blocks.

{
    auto_https off
}

:8080 {
    log {
        output stdout
        format console
        level INFO
    }

    # caddy-waf:  Place this FIRST to process ALL requests
    waf {
        rule_file rules.json # Ensure this file exists!
    }

    handle /waf_metrics {  # Important: handle the metrics endpoint
        respond "" 200 # Respond with empty 200 to prevent further processing
    }


    handle /greet {
        respond "Greetings!" 200
    }

    handle {
        respond "Hello, world!" 200
    }
}
  • The waf {} block must come before any other handle blocks that you want it to protect. This ensures that the WAF processes all incoming requests.
  • Important: The rule_file rules.json configuration requires a valid rules.json file in the same directory as your Caddyfile. Create a minimal JSON file (even an empty one {}) to avoid errors during startup.
  • The handle /waf_metrics endpoint is essential. The WAF needs to expose its metrics. We respond with an empty 200 to prevent any other handlers from processing the request for metrics. (Ideally, the plugin would handle this itself, but this works as a workaround).

Level 5: A More Usable caddy-waf Configuration (Practical)

This example expands on the previous example, adding more common caddy-waf settings and showing how to exempt certain paths from WAF processing.

{
    auto_https off
}

:8080 {
    log {
        output stdout
        format console
        level INFO
    }

    # caddy-waf: Place this FIRST to process ALL requests
    waf {
        rule_file rules.json
        ip_blacklist_file ip_blacklist.txt
        dns_blacklist_file dns_blacklist.txt
        metrics_endpoint /waf_metrics # Explicitly defined metrics endpoint
    }

    handle /waf_metrics {
        respond "" 200 # Respond with empty 200 to prevent further processing
    }

    # Exempt /static from WAF processing
    @static path /static/*
    handle @static {
        file_server
    }

    handle /greet {
        respond "Greetings!" 200
    }


    handle {
        respond "Hello, world!" 200
    }
}
  • ip_blacklist_file and dns_blacklist_file are added. Create empty files with these names to start.
  • The @static named matcher uses the path matcher to select requests where the path starts with /static/.
  • The handle @static block uses the file_server directive to serve static files from the /static directory (you'd need to create a static directory with some files in it). Crucially, requests to /static/* are not processed by the caddy-waf plugin.

Additional notes:

  • Order Matters: The order of handle blocks is critical. Place the waf {} block before any other handle blocks that should be protected by the WAF.
  • The Metrics Endpoint: The /waf_metrics endpoint must be handled to allow the caddy-waf plugin to function correctly.
  • rules.json: Make sure the rules.json file exists, even if it's empty to start. Start adding rules incrementally.
  • handle Blocks: Use handle blocks to control how requests are routed and processed by Caddy.
  • Named Matchers: Use named matchers (@static, @wafmetrics) to make your Caddyfile more readable and easier to manage.

Please let me know if such progressive how-to can be useful to be integrated in the repo docs, TIA!

@LeVraiRoiDHyrule
Copy link
Author

LeVraiRoiDHyrule commented Feb 9, 2025

Thanks a lot for your very complete answer.

It appears directive waf can't be used directly in the site configuration. I am having this error:
Removing invalid block: directive 'waf' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option
I don't understand why because all of your examples do put waf directive directly in the site configuration.

My full caddyfile is the following:

# GLOBAL
(wafsnippet) {
	waf {
		dns_blacklist_file dns_blacklist.txt
		ip_blacklist_file ip_blacklist.txt
		log_json
		log_severity error
		metrics_endpoint /waf_metrics
		redact_sensitive_data
		rule_file owasp_rules.json
		whitelist_countries GeoLite2-Country.mmdb FR CH
	}
	handle /waf_metrics {
		respond 200 ""
	}
	log {
		format console
		level INFO
		output stdout
	}
}

# PER-SITE
openmediavault.mydomain.com {
	import wafsnippet
	handle {
		reverse_proxy http://192.168.1.27:10997
	}
}

After adding order waf first to the global options, the caddyfile is accepted (I can see that it is also used by this user : #8 (comment)).

Now, when accessing openmediavault.mydomain.com, I get the following error:

2025/02/09 16:34:58.015	INFO	http.log.access.log0	handled request	{"request": {"remote_ip": "IPV6_REDACTED", "remote_port": "64589", "client_ip": "IPV6_REDACTED", "proto": "HTTP/3.0", "method": "GET", "host": "openmediavault.mydomain.com", "uri": "/", "headers": {"Sec-Fetch-Mode": ["navigate"], "Sec-Fetch-User": ["?1"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Dest": ["document"], "Accept-Language": ["fr-FR,fr;q=0.8,en-US;q=0.5,en;q=0.3"], "Accept-Encoding": ["gzip, deflate, br, zstd"], "Dnt": ["1"], "Sec-Gpc": ["1"], "Alt-Used": ["openmediavault.mydomain.com"], "Priority": ["u=0, i"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h3", "server_name": "openmediavault.mydomain.com"}}, "bytes_read": 0, "user_id": "", "duration": 0, "size": 0, "status": 0, "resp_headers": {"Server": ["Caddy"]}}
INF INF INF INF INF INF INF INF ts=1739118898.015487 INF INF INF INF INF INF INF INF ts=1739118898.015487 msg=ERROR http: panic serving arg="runtime error: invalid memory address or nil pointer dereference" trace="goroutine 294 [running]:
github.com/quic-go/quic-go/http3.(*Server).handleRequest.func2.1()
\tgithub.com/quic-go/[email protected]/http3/server.go:682 +0xb9
panic({0x17df6a0?, 0x2bce670?})
\truntime/panic.go:785 +0x132
github.com/fabriziosalmi/caddy-waf.(*RequestValueExtractor).extractSingleValue(0xc0007cb830, {0xc000ff8030?, 0x1ef96b8?}, 0xc001bf4000, {0x0, 0x0})
\tgithub.com/fabriziosalmi/[email protected]/request.go:135 +0x12c1
github.com/fabriziosalmi/caddy-waf.(*RequestValueExtractor).ExtractValue(0xc0007cb830, {0xc000ff8030?, 0x172cdc0?}, 0xc001bf4000, {0x0, 0x0})
\tgithub.com/fabriziosalmi/[email protected]/request.go:76 +0xe5
github.com/fabriziosalmi/caddy-waf.(*Middleware).extractValue(...)
\tgithub.com/fabriziosalmi/[email protected]/caddywaf.go:530
github.com/fabriziosalmi/caddy-waf.(*Middleware).handlePhase(0xc000223208, {0x7f33ee93e370, 0xc000288140}, 0xc0015b8780, 0x2, 0xc0015c3270)
\tgithub.com/fabriziosalmi/[email protected]/handler.go:323 +0x2179
github.com/fabriziosalmi/caddy-waf.(*Middleware).isPhaseBlocked(0xc000223208, {0x7f33ee93e370, 0xc000288140}, 0x1eff4b0?, 0x172d140?, 0xc0015c3270)
\tgithub.com/fabriziosalmi/[email protected]/handler.go:80 +0x27
github.com/fabriziosalmi/caddy-waf.(*Middleware).ServeHTTP(0xc000223208, {0x7f33ee93e370, 0xc000288140}, 0xc0015b83c0, {0x1f07ac0, 0xc000295740})
\tgithub.com/fabriziosalmi/[email protected]/handler.go:39 +0x2c7
github.com/caddyserver/caddy/v2/modules/caddyhttp.wrapMiddleware.func1.1({0x7f33ee93e370, 0xc000288140}, 0xc0015b83c0)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:331 +0xd2
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0x1f07ac0?, {0x7f33ee93e370?, 0xc000288140?}, 0xc0015b83c0?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.RouteList.Compile.wrapRoute.func1.1({0x7f33ee93e370, 0xc000288140}, 0xc0015b83c0)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:298 +0x26d
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0xc00053b908?, {0x7f33ee93e370?, 0xc000288140?}, 0x1f07ac0?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*Subroute).ServeHTTP(0xc0003d39a0, {0x7f33ee93e370, 0xc000288140}, 0xc0015b83c0, {0x1f07ac0, 0x1d17a08})
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/subroute.go:74 +0x67
github.com/caddyserver/caddy/v2/modules/caddyhttp.wrapMiddleware.func1.1({0x7f33ee93e370, 0xc000288140}, 0xc0015b83c0)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:331 +0xd2
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0x1f07ac0?, {0x7f33ee93e370?, 0xc000288140?}, 0xc0015b83c0?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.RouteList.Compile.wrapRoute.func1.1({0x7f33ee93e370, 0xc000288140}, 0xc0015b83c0)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/routes.go:298 +0x26d
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0x7f3435f70108?, {0x7f33ee93e370?, 0xc000288140?}, 0x18?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*Server).enforcementHandler(0x410c05?, {0x7f33ee93e370?, 0xc000288140?}, 0x1?, {0x1f07ac0?, 0xc0002b21e0?})
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/server.go:482 +0x24b
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*App).Provision.(*Server).wrapPrimaryRoute.func2({0x7f33ee93e370?, 0xc000288140?}, 0x4eb24f?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/server.go:458 +0x35
github.com/caddyserver/caddy/v2/modules/caddyhttp.HandlerFunc.ServeHTTP(0xc000888000?, {0x7f33ee93e370?, 0xc000288140?}, 0xc0015b83c0?)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/caddyhttp.go:74 +0x29
github.com/caddyserver/caddy/v2/modules/caddyhttp.(*Server).ServeHTTP(0xc000675888, {0x1f16510, 0xc0013e0400}, 0xc0015b8140)
\tgithub.com/caddyserver/caddy/[email protected]/modules/caddyhttp/server.go:370 +0xc88
github.com/quic-go/quic-go/http3.(*Server).handleRequest.func2(0x1f194f0?, 0xc0017bd450?, {0x1f081e0?, 0xc000675888?}, 0xc001b3a6a0?, 0x1c?)
\tgithub.com/quic-go/[email protected]/http3/server.go:690 +0x5f
github.com/quic-go/quic-go/http3.(*Server).handleRequest(0xc0003dc0f0, 0xc000a97340, {0x1f27a78, 0xc0013cc5a0}, 0xc0006679a0, 0xc0014ce0c0)
\tgithub.com/quic-go/[email protected]/http3/server.go:691 +0xac5
github.com/quic-go/quic-go/http3.(*Server).handleConn.func1()
\tgithub.com/quic-go/[email protected]/http3/server.go:578 +0x5b
created by github.com/quic-go/quic-go/http3.(*Server).handleConn in goroutine 188
\tgithub.com/quic-go/[email protected]/http3/server.go:574 +0x32d
"

@fabriziosalmi
Copy link
Owner

{
  acme_dns cloudflare REDACTED # Replace REDACTED
  grace_period 10s
  storage postgres {
    connection_string postgres://caddy:REDACTED@caddy-db:5432/caddy?sslmode=disable # Replace REDACTED
  }
}

(wafsnippet) {
    route {
        # WAF Plugin runs on all requests first
        waf {
            metrics_endpoint /waf_metrics
            rule_file owasp_rules.json
            ip_blacklist_file ip_blacklist.txt
            dns_blacklist_file dns_blacklist.txt
        }

        # Match the waf metrics endpoint specifically and stop processing
        @wafmetrics path /waf_metrics
        handle @wafmetrics {
            # Do not respond here so it goes to the WAF plugin
        }

        # All other requests, reverse proxy to backend
        handle {
            reverse_proxy {$reverse_proxy_address}  # Use a variable
        }
    }
}

openmediavault.mydomain.com {
  import wafsnippet {
    $reverse_proxy_address http://192.168.1.27:10997
  }
}

othersubdomain.mydomain.com {
  import wafsnippet {
    $reverse_proxy_address localhost:8080
  }
}

I checked and, if I am not completely dumb... this should work :) let me know ;)

@fabriziosalmi
Copy link
Owner

At the same time i am releasein a fix for the issue you encountered (Add nil checks for the http.ResponseWriter in the extractResponseBody and extractDynamicResponseHeader functions.), thank you !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants