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

Customize WAF per website #111

Open
bazalt opened this issue Nov 7, 2024 · 7 comments
Open

Customize WAF per website #111

bazalt opened this issue Nov 7, 2024 · 7 comments

Comments

@bazalt
Copy link

bazalt commented Nov 7, 2024

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
Copy link

DavidProdinger commented Nov 7, 2024

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.

  • Turn WAF On or DetectionOnly per application (Website)
  • Different rule exclusions

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
Copy link
Author

bazalt commented Nov 7, 2024

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

@bazalt bazalt closed this as completed Nov 7, 2024
@bazalt
Copy link
Author

bazalt commented Nov 15, 2024

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.

@bazalt bazalt reopened this Nov 15, 2024
@fionera
Copy link
Contributor

fionera commented Nov 26, 2024

Is this still an issue? your solution should normally work.

@bazalt
Copy link
Author

bazalt commented Nov 26, 2024

Yes, it's working. Just wondered if a cleaner solution exists to avoid recalculating app from the map file req.hdr(host),lower,map_dom(/path/to/backends.map,be_default), as the result is already stored in my HAProxy var txn.backend.

@fionera
Copy link
Contributor

fionera commented Nov 26, 2024

That should normally just work. Which haproxy version are you running? I will try to reproduce it :)

@bazalt
Copy link
Author

bazalt commented Nov 26, 2024

Thank you.
I'm working on the 3.0.5-alpine Docker image.

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