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

Closed
bazalt opened this issue Nov 7, 2024 · 13 comments · May be fixed by #157
Closed

Customize WAF per website #111

bazalt opened this issue Nov 7, 2024 · 13 comments · May be fixed by #157

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.

@superstes
Copy link

superstes commented Dec 27, 2024

I've also encountered a similar issue:

HAProxy:

frontend test
    ...
    http-request set-var(txn.waf_app) str(app1) if { req.hdr(host) -m str -i www.oxl.at oxl.at }
    http-request set-var(txn.waf_app) str(default) if !{ var(txn.waf_app) -m found }

    # testing - to make sure the var is really set
    http-request deny status 418 if !{ var(txn.waf_app) -m found }

    filter spoe engine coraza config /etc/haproxy/coraza-spoe.cfg

SPOE:

...

spoe-message coraza-req
    args app=var(txn.waf_app) 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

Error:
{"level":"panic","app":"","time":"2024-12-27T20:04:10+01:00","message":"app not found"}

Version: haproxy 3.1.1-1
The Coraza-SPOA uses the current main-branch codebase.

@superstes
Copy link

superstes commented Dec 27, 2024

Ok. The HAProxy SPOE Docs mention this:

on-frontend-http-request  is triggered just before the evaluation of "http-request" rules on the frontend side

So this event seems to happen before the ACLs/Vars in the frontend-section are processed. Thus the variables are not set yet..

If changed to on-backend-http-request the error messages are gone. Not sure if this would have drawbacks or even work correctly..

@fionera
Copy link
Contributor

fionera commented Dec 27, 2024

We should replace this hook by an explicit call to the spop handler like I do in https://github.com/DropMorePackets/berghain/blob/master/examples/haproxy/haproxy.cfg#L33

@superstes
Copy link

Nice to know that we can do so. Had read about the spoe-group but did not find a good example before..
Would make sense in that case - yeah 👍

@superstes
Copy link

superstes commented Dec 28, 2024

As mentioned by @fionera - by using a spoe-group the variable works:

SPOE:

[coraza]
spoe-agent coraza-agent
    messages    coraza-req
    groups      coraza-req
    option      var-prefix      coraza
    option      set-on-error    error
    timeout     hello           2s
    timeout     idle            2m
    timeout     processing      500ms
    use-backend coraza-waf-spoa
    log         global

spoe-message coraza-req
    args app=var(txn.waf_app) 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

spoe-group coraza-req
    messages coraza-req

Frontend:

frontend test
    ...
    http-request set-var(txn.waf_app) str(app1) if { req.hdr(host) -m str -i www.oxl.at oxl.at }
    http-request set-var(txn.waf_app) str(default) if !{ var(txn.waf_app) -m found }

    filter spoe engine coraza config /etc/haproxy/waf-coraza-spoe.cfg
    http-request send-spoe-group coraza coraza-req

Note: This will perform all http-request actions that are above the send-spoe-group.
For example - redirects above it are performed without the traffic being sent to the WAF.

fionera added a commit that referenced this issue Jan 5, 2025
To allow for dynamic app names, the use of an explicit spoe call is required. To reduce confusion we update the example to use this config.

Closes #111
@fionera
Copy link
Contributor

fionera commented Jan 5, 2025

I have updated the example to use this config. This should reduce the confusion :) Thanks y'all

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

Successfully merging a pull request may close this issue.

4 participants