There are still some basic WAF features to be implemented.
NOTE: The feature-set this role provides does not come lose to the one available in HAProxy Enterprise by default.
Fingerprinting Docs for detailed information on how you might want to track clients.
haproxy:
waf:
script_kiddy:
disable: ['.zip'] # disable specific entries from the script-kiddy filters
exclude: # exclude by path sub-string (at runtime)
- '.well_known/'
frontends:
fe_web:
bind: ['[::]:80 v4v6']
security:
headers: true
fingerprint_ssl: true
restrict_methods: true
allow_only_methods: ['HEAD', 'GET', 'POST']
# deny_dangerous_methods: true
block_script_bots: true
block_bad_crawler_bots: true
flag_bots: true
block_script_kiddies: true
routes:
be_test:
domains: ['app.test.ansibleguy.net']
default_backend: 'be_fallback'
backends:
be_test:
security:
# NOTE: it could make sense for your environment to apply some filters on an app-basis
# but these are optional
restrict_methods: true
allow_only_methods: ['HEAD', 'GET', 'POST']
# deny_dangerous_methods: true
block_script_bots: true
block_bad_crawler_bots: true
servers:
- 'srv-1 192.168.10.11:80'
- 'srv-2 192.168.10.12:80'
be_fallback:
lines: 'http-request redirect code 302 location https://github.com/ansibleguy'
root@test-ag-haproxy-waf:/# cat /etc/haproxy/haproxy.cfg
> # Ansible managed: Do NOT edit this file manually!
> # ansibleguy.infra_haproxy
>
> global
> daemon
> user haproxy
> group haproxy
>
> tune.ssl.capture-buffer-size 96
>
> log /dev/log local0
> log /dev/log local1 notice
> chroot /var/lib/haproxy
> stats socket /run/haproxy/admin.sock mode 660 level admin
> stats timeout 30s
> ca-base /etc/ssl/certs
> crt-base /etc/ssl/private
> ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
> ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
> ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
>
> defaults
> log global
> mode http
> option httplog
> option dontlognull
> timeout connect 5000
> timeout client 50000
> timeout server 50000
> errorfile 400 /etc/haproxy/errors/400.http
> errorfile 403 /etc/haproxy/errors/403.http
> errorfile 408 /etc/haproxy/errors/408.http
> errorfile 500 /etc/haproxy/errors/500.http
> errorfile 502 /etc/haproxy/errors/502.http
> errorfile 503 /etc/haproxy/errors/503.http
> errorfile 504 /etc/haproxy/errors/504.http
root@test-ag-haproxy-waf:/# cat /etc/haproxy/conf.d/frontend.cfg
> # Ansible managed: Do NOT edit this file manually!
> # ansibleguy.infra_haproxy
>
> frontend fe_web
> mode http
> bind [::]:80 v4v6
>
> frontend fe_web
> mode http
> bind [::]:80 v4v6
>
> http-request deny status 405 default-errorfiles if !{ method HEAD GET POST }
>
> # block well-known script-bots
> http-request deny status 425 default-errorfiles if { req.fhdr(User-Agent) -m sub -i -f /etc/haproxy/lst/waf-badbot-ua-sub.lst }
> # block well-known bad-crawler-bots
> http-request deny status 425 default-errorfiles if { req.fhdr(User-Agent) -m str -i -f /etc/haproxy/lst/waf-crawler-ua-full.lst }
> http-request deny status 425 default-errorfiles if { req.fhdr(User-Agent) -m sub -i -f /etc/haproxy/lst/waf-crawler-ua-sub.lst }
> # block script-kiddy requests
> http-request deny status 425 default-errorfiles if { path -m beg -i -f /etc/haproxy/lst/waf-script-kiddy-path-beg.lst }
> http-request deny status 425 default-errorfiles if { path -m end -i -f /etc/haproxy/lst/waf-script-kiddy-path-end.lst }
> http-request deny status 425 default-errorfiles if { path -m sub -i -f /etc/haproxy/lst/waf-script-kiddy-path-sub.lst }
> # FLAG BOTS
> ## flag bots by common user-agent substrings
> http-request set-var(txn.bot) int(1) if !{ var(txn.bot) -m found } { req.fhdr(User-Agent) -m sub -i -f /etc/haproxy/lst/waf-bot-ua-sub.lst }
>
> ## unusual if action has no referrer; could produce false-positives in some special cases
> http-request set-var(txn.bot) int(1) if !{ var(txn.bot) -m found } !{ method GET HEAD } !{ req.hdr(Referer) -m found }
> ## browsers set these ones usually
> http-request set-var(txn.bot) int(1) if !{ var(txn.bot) -m found } !{ req.hdr(Accept-Language) -m found }
> http-request set-var(txn.bot) int(1) if !{ var(txn.bot) -m found } !{ req.fhdr(User-Agent) -m found }
>
> http-request set-var(txn.bot) int(0) if !{ var(txn.bot) -m found }
> http-request capture var(txn.bot) len 1
>
> # Security headers
> http-response add-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;" if !{ res.hdr(Strict-Transport-Security) -m found }
> http-response add-header X-Frame-Options "SAMEORIGIN" if !{ res.hdr(X-Frame-Options) -m found }
> http-response add-header X-Content-Type-Options "nosniff" if !{ res.hdr(X-Content-Type-Options) -m found }
> http-response add-header X-Permitted-Cross-Domain-Policies "none" if !{ res.hdr(X-Permitted-Cross-Domain-Policies) -m found }
> http-response add-header X-XSS-Protection "1; mode=block" if !{ res.hdr(X-XSS-Protection) -m found }
> # SSL fingerprint
> http-request lua.fingerprint_ja3n
> http-request capture var(txn.fingerprint_ssl) len 32
>
> http-request capture req.fhdr(User-Agent) len 200
>
> # BACKEND be_test
> acl be_test_filter_domains req.hdr(host) -m str -i app.test.ansibleguy.net
> use_backend be_test if be_test_filter_domains
>
> default_backend be_fallback
root@test-ag-haproxy-waf:/# cat /etc/haproxy/conf.d/backend.cfg
> # Ansible managed: Do NOT edit this file manually!
> # ansibleguy.infra_haproxy
>
> backend be_test
> mode http
>
> http-request deny status 405 default-errorfiles if !{ method HEAD GET POST }
>
> # block well-known script-bots
> http-request deny status 425 default-errorfiles if { req.fhdr(User-Agent) -m sub -i curl wget Apache-HttpClient nmap Metasploit headless cypress go-http-client zgrab python httpx httpcore aiohttp httputil urllib GuzzleHttp phpcrawl Zend_Http_Client Wordpress Symfony-HttpClient cpp-httplib java perl axios ruby }
> # block well-known bad-crawler-bots
> http-request deny status 425 default-errorfiles if { req.fhdr(User-Agent) -m sub -i spider test-bot tiny-bot fidget-spinner-bot download scrapy }
>
> server srv-1 192.168.10.11:80 check
> server srv-2 192.168.10.12:80 check
>
>
> backend be_fallback
> mode http
>
> http-request redirect code 302 location https://github.com/ansibleguy
>
root@test-ag-haproxy-waf:/# systemctl status haproxy.service
> * haproxy.service - HAProxy Load Balancer
> Loaded: loaded (/lib/systemd/system/haproxy.service; enabled; preset: enabled)
> Drop-In: /etc/systemd/system/haproxy.service.d
> `-override.conf
> Active: active (running) since Sat 2024-05-04 16:24:54 UTC; 4min 11s ago
> Docs: man:haproxy(1)
> file:/usr/share/doc/haproxy/configuration.txt.gz
> https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/
> https://github.com/ansibleguy/infra_haproxy
> Process: 4574 ExecStartPre=/usr/sbin/haproxy -c -f $CONFIG -f /etc/haproxy/conf.d/ (code=exited, status=0/SUCCESS)
> Process: 4635 ExecReload=/usr/sbin/haproxy -c -f $CONFIG -f /etc/haproxy/conf.d/ (code=exited, status=0/SUCCESS)
> Process: 4637 ExecReload=/bin/kill -USR2 $MAINPID (code=exited, status=0/SUCCESS)
> Main PID: 4576 (haproxy)
> Status: "Ready."
> Tasks: 7 (limit: 1783)
> Memory: 132.2M
> CPU: 297ms
> CGroup: /system.slice/haproxy.service
> |-4576 /usr/sbin/haproxy -Ws -f /etc/haproxy/haproxy.cfg -f /etc/haproxy/conf.d/ -p /run/haproxy.pid -S /run/haproxy-master.sock
> `-4639 /usr/sbin/haproxy -sf 4578 -x sockpair@4 -Ws -f /etc/haproxy/haproxy.cfg -f /etc/haproxy/conf.d/ -p /run/haproxy.pid -S /run/haproxy-master.sock