corazawaf / coraza-spoa

EXPERIMENTAL: A wrapper around the OWASP Coraza WAF for HAProxy's SPOE filters
Apache License 2.0
85 stars 18 forks source link

Customize WAF per website #111

Open bazalt opened 1 week ago

bazalt commented 1 week ago

Hello,

I have one an only web frontend, dispatching requests to multiple backends through a host maps:

# haproxy.cfg
frontend fe_web
  bind :80
  bind :443 ssl crt /path/to/cert.pem

  # 
  # Apply Coraza WAF
  #
  filter spoe engine coraza config /usr/local/etc/haproxy/coraza.cfg

  # Currently haproxy cannot use variables to set the code or deny_status, so this needs to be manually configured here
  http-request redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }
  http-response redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }

  http-request deny deny_status 403 hdr waf-block "request"  if { var(txn.coraza.action) -m str deny }
  http-response deny deny_status 403 hdr waf-block "response" if { var(txn.coraza.action) -m str deny }

  http-request silent-drop if { var(txn.coraza.action) -m str drop }
  http-response silent-drop if { var(txn.coraza.action) -m str drop }

  # Deny in case of an error, when processing with the Coraza SPOA
  http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }
  http-response deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }

  # Dynamic backend from /etc/haproxy/maps/hosts.map
  use_backend %[req.hdr(host),lower,map_dom(/etc/haproxy/maps/hosts.map,be_default)]

Coraza WAF is setup on this frontend, and it's working well... but I'm struggling on some use cases:

  1. I would like to disable WAF on a subset of websites. Is there a way to achieve this?
  2. Even deeper: would it be possible to disable some specific OWASP rules, just for a specific website?

Thank you.

DavidProdinger commented 1 week ago

Yes you can do that. I have a similar setup with multiple hosts/domains.

See #92

/etc/haproxy/coraza.cfg

See the app arg

spoe-message coraza-req
    args app=req.hdr(host),regsub("^www.",,i) id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body
    event on-frontend-http-request

spoe-message coraza-res
    args app=str(txn.app_name) id=unique-id version=res.ver status=status headers=res.hdrs body=res.body
    event on-http-response

/etc/coraza/config.yaml

Your domain (without www) is the application. Also featuring different configurations/exclusions with a sites.d directory.

You can configure different log files too.

# The SPOA server bind address
bind: 0.0.0.0:9000

# Process request and response with this application if provided app name is not found.
# You can remove or comment out this config param if you don't need "default_application" functionality.
default_application: default_haproxy

applications:
  default_haproxy: &default
    # Get the coraza.conf from https://github.com/corazawaf/coraza
    #
    # Download the OWASP CRS from https://github.com/coreruleset/coreruleset/releases
    # and copy crs-custom.conf & the rules, plugins directories to /etc/coraza-spoa
    directives: |
      Include /etc/coraza-spoa/sites/coraza.conf
      Include /etc/coraza-spoa/crs-setup.conf
      Include /etc/coraza-spoa/sites/crs-custom.conf
      Include /etc/coraza-spoa/sites/plugins/*-config.conf
      Include /etc/coraza-spoa/sites/plugins/*-before.conf
      Include /etc/coraza-spoa/rules/*.conf
      Include /etc/coraza-spoa/sites/plugins/*-after.conf
      Include /etc/coraza-spoa/sites/after.conf

    # HAProxy configured to send requests only, that means no cache required
    # NOTE: there are still some memory & caching issues, so use this with care
    no_response_check: true

    # The transaction cache lifetime in milliseconds (60000ms = 60s)
    transaction_ttl_ms: 600000
    # The maximum number of transactions which can be cached
    transaction_active_limit: 100000

    # The log level configuration, one of: debug/info/warn/error/panic/fatal
    log_level: info
    # The log file path
    log_file: /var/log/coraza-spoa/coraza-agent.log

  # YOUR DOMAINS HERE
  example.com:
    <<: *default
    directives: |
      Include /etc/coraza-spoa/sites.d/example.com/coraza.conf
      Include /etc/coraza-spoa/crs-setup.conf
      Include /etc/coraza-spoa/sites.d/example.com/crs-custom.conf
      Include /etc/coraza-spoa/sites.d/example.com/plugins/*-config.conf
      Include /etc/coraza-spoa/sites.d/example.com/plugins/*-before.conf
      # Next line is for the default CRS Rules to load (all)
      Include /etc/coraza-spoa/rules/*.conf
      # Custom rules for the site
      Include /etc/coraza-spoa/sites.d/example.com/rules/*.conf
      Include /etc/coraza-spoa/sites.d/example.com/plugins/*-after.conf

    # Adjust the log file path
    log_file: /var/log/coraza-spoa/coraza-agent-example.com.log
bazalt commented 1 week ago

Very clean answer, sir. Thank you very much 👍️.

bazalt commented 2 days ago

Sorry to reopen this issue, but I have a complementary answer:

In HAProxy, I dynamically load backends using a txn.backend variable:

# haproxy.cfg
[...]
frontend fe_web
    [...]
    http-request set-var(txn.backend) req.hdr(host),lower,map_dom(/path/to/backends.map,be_default)

    # 
    # Apply Coraza WAF
    #

    # Load and apply WAF
    filter spoe engine coraza config /path/to/coraza.cfg

    # Currently haproxy cannot use variables to set the code or deny_status, so this needs to be manually configured here
    http-request redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }
    http-response redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }

    http-request deny deny_status 403 if { var(txn.coraza.action) -m str deny }
    http-response deny deny_status 403 if { var(txn.coraza.action) -m str deny }

    http-request silent-drop if { var(txn.coraza.action) -m str drop }
    http-response silent-drop if { var(txn.coraza.action) -m str drop }

    # Deny in case of an error, when processing with the Coraza SPOA
    http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }
    http-response deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }

    use_backend %[var(txn.backend)]

I would like to reuse txn.backend variable to load custom WAF directives, like this:

# coraza.cfg
[coraza]
spoe-agent coraza-agent
    messages    coraza-req
    [...]

spoe-message coraza-req
    # Set app name from var(txn.backend)
    args app=var(txn.backend) id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body
    event on-frontend-http-request

I tried multiple syntaxes, but for now, all of them seems to be ignored. I resolved setting app var by cloning from haproxy.cfg, but I'm not satisfied with this not DRY solution:

# coraza.cfg
[...]
spoe-message coraza-req
    args app=req.hdr(host),lower,map_dom(/path/to/backends.map,be_default)

    args id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body
    event on-frontend-http-request

Do you know a cleaner way to achieve this? Thank you.