From b479c0ccb382fbe091dd9b20ec58755edd74be4c Mon Sep 17 00:00:00 2001 From: mivirl <> Date: Mon, 20 Jan 2025 04:47:13 +0000 Subject: [PATCH] Initial commit --- README.md | 121 ++++++++++++ build.sh | 308 +++++++++++++++++++++++++++++ haproxy_conf/haproxy.cfg | 138 +++++++++++++ haproxy_conf/spoe-modsecurity.conf | 14 ++ services/waf.service | 10 + services/waf_darkhttpd.service | 98 +++++++++ services/waf_haproxy.service | 114 +++++++++++ services/waf_modsecurity.service | 104 ++++++++++ video_scripts/add_video.sh | 41 ++++ video_scripts/get_videos.sh | 100 ++++++++++ 10 files changed, 1048 insertions(+) create mode 100644 README.md create mode 100644 build.sh create mode 100644 haproxy_conf/haproxy.cfg create mode 100644 haproxy_conf/spoe-modsecurity.conf create mode 100644 services/waf.service create mode 100644 services/waf_darkhttpd.service create mode 100644 services/waf_haproxy.service create mode 100644 services/waf_modsecurity.service create mode 100644 video_scripts/add_video.sh create mode 100644 video_scripts/get_videos.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..a212761 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Web Application Firewall + +Firewall implemented with [HaProxy](https://haproxy.org) and +[ModSecurity](https://modsecurity.org/). + +Proxies incoming requests and allows or blocks connections to the protected +service using the following information: +- ModSecurity rules + - Default rules are based on [OWASP CRS](https://coreruleset.org) +- JA4 hash (TLS fingerprint) + - More information: + [JA4 Network Fingerprinting](https://blog.foxio.io/ja4%2B-network-fingerprinting) + - Allow or block by hash +- IP address + - Allow or block by IP +- Request rate + - Rate limits requests from the same IP +- Block rate + - Blocks IP if sufficient recent requests from the IP were blocked + +When a request is blocked, a custom page will be displayed. The default will +return a page with a random GIF from a small set downloaded during the build +process (see `video_scripts/get_videos.sh`). + +All services used are sandboxed using systemd to limit access to just the files, +binaries, and libraries needed to run them. + + +## Building and installing + +Should be run on a linux machine, currently only works with debian-based +distributions: + +```sh +sh build.sh +``` + +Copy the produced `dist.tar` to the machine you want to run it on, extract, +make any config changes, then run `dist/install.sh`. + +**The default configuration assumes the protected service is listening on port +2222**. This can be changed by editing `dist/haproxy/haproxy.cfg` and changing +the IP in the section `backend backend_normal` + +```sh +tar -xf dist +cd dist +sudo sh ./install.sh +``` + + +## Usage + +After install, the firewall can be started and stopped using `waf.service`: + +```sh +systemctl start waf.service +systemctl status waf*.service +systemctl stop waf.service +``` + +Enabling or disabling requires specifying all services: + +```sh +systemctl enable waf.service waf_haproxy.service waf_modsecurity.service waf_darkhttpd.service +systemctl disable waf.service waf_haproxy.service waf_modsecurity.service waf_darkhttpd.service +``` + + +## Configuration + +Relevant files for configuration: +- `dist/haproxy/haproxy.cfg` - Configuration for haproxy, including what ports + it binds to. +- `dist/waf_haproxy.service` - Systemd service for haproxy, must be edited to + add any new files to the sandbox or change what ports it's allowed to bind to. +- `dist/darkhttpd/srv` - Directory of static files that will be served when the + client is blocked. Default is a page with a random GIF (see + `video_scripts/get_videos.sh`). +- `dist/modsecurity/modsecurity.conf`, `dist/modsecurity/rules` - Rules used + for ModSecurity detections. Can be configured to reduce false positives. + +Map files are key-value pairs separated by a space. These are used to match +against for allow and block rules. Currently **allow rules override block +rules**, but this can be configured in `dist/haproxy/haproxy.cfg`. Documentation +for map files can be found at the +[Haproxy documentation](https://www.haproxy.com/documentation/haproxy-configuration-tutorials/core-concepts/map-files/) +- `dist/haproxy/ja4_accept.map` - JA4 hashes to always allow access for +- `dist/haproxy/ja4_block.map` - JA4 hashes to always deny access for +- `dist/haproxy/ip_block.map` - IP addresses to always allow access for +- `dist/haproxy/ip_accept.map` - IP addresses to always deny access for + + +### Configuring after installation + +Once installed to `/opt/waf_configs`, it's still possible to edit files, +including while the services are running. **If configuration files or +allow/blocklists are edited while the service is running, the inode for the +files must not change**. This can be handled by setting `:set backupcopy=yes` +in vim, for example, or by overwriting with `cat tempfile > config`. Once +the files have been changed, they need to be added to the systemd sandbox with +`systemctl bind` and the service should be reloaded. + +Adding a JA4 hash to the blocklist without stopping services: + +```sh +echo 't13d1516h2_8daaf6152771_02713d6af862 block_rule_name' \ + | sudo tee -a /opt/waf_configs/haproxy/ja4_block.map +sudo systemctl bind waf_haproxy.service /opt/waf_configs/haproxy/ja4_block.map /etc/haproxy/ja4_block.map +sudo systemctl reload waf_haproxy.service +``` + + +## Uninstalling + +```sh +systemctl stop waf.service +systemctl disable waf.service waf_haproxy.service waf_modsecurity.service waf_darkhttpd.service +rm -r /opt/waf_configs +userdel waf_user +``` diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..d12baae --- /dev/null +++ b/build.sh @@ -0,0 +1,308 @@ +#!/bin/sh +set -e + +set -x +sudo apt-get update +sudo apt-get install -y curl gcc make g++ libevent-dev perl asciinema mpv +set +x + +DOWNLOAD="curl -L" + +CWD=$(pwd) + +HAPROXY_MAJOR_VERSION="3.1" +HAPROXY_MINOR_VERSION="2" +PCRE2_VERSION="10.44" +OPENSSL_VERSION="3.4.0" +ZLIB_VERSION="1.3.1" +LUA_VERSION="5.4.7" +DARKHTTPD_VERSION="1.16" +MODSECURITY_VERSION="3.0.13" +CORERULESET_VERSION="4.10.0" + + +HAPROXY_VERSION="${HAPROXY_MAJOR_VERSION}.${HAPROXY_MINOR_VERSION}" +HAPROXY_FILE="haproxy-${HAPROXY_VERSION}.tar.gz" +HAPROXY_LINK="https://www.haproxy.org/download/${HAPROXY_MAJOR_VERSION}/src/${HAPROXY_FILE}" +HAPROXY_OUT="$CWD/build/haproxy" + +PCRE2_FILE="pcre2-${PCRE2_VERSION}.tar.gz" +PCRE2_LINK="https://github.com/PCRE2Project/pcre2/releases/download/pcre2-${PCRE2_VERSION}/${PCRE2_FILE}" +PCRE2_OUT="$CWD/build/pcre" + +OPENSSL_FILE="openssl-${OPENSSL_VERSION}.tar.gz" +OPENSSL_LINK="https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/${OPENSSL_FILE}" +OPENSSL_OUT="$CWD/build/openssl" + +ZLIB_FILE="zlib-${ZLIB_VERSION}.tar.gz" +ZLIB_LINK="https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}/${ZLIB_FILE}" +ZLIB_OUT="$CWD/build/zlib" + +LUA_FILE="lua-${LUA_VERSION}.tar.gz" +LUA_LINK="https://www.lua.org/ftp/${LUA_FILE}" +LUA_OUT="$CWD/build/lua" + +JA3N_FILE="ja3n.lua" +JA3N_LINK="https://raw.githubusercontent.com/O-X-L/haproxy-ja3n/refs/heads/latest/ja3n.lua" + +JA4_FILE="ja4.zip" +JA4_LINK="https://github.com/O-X-L/haproxy-ja4/archive/refs/heads/latest.zip" +JA4DB_FILE="ja4db.json" +JA4DB_LINK="https://ja4db.com/api/read/" +JA4_OUT="$CWD/build/ja4" + +DARKHTTPD_FILE="darkhttpd-v${VERSION}.tar.gz" +DARKHTTPD_LINK="https://github.com/emikulic/darkhttpd/archive/refs/tags/v1.16.tar.gz" +DARKHTTPD_OUT="$CWD/build/darkhttpd" + +MODSECURITY_FILE="modsecurity-v${MODSECURITY_VERSION}.tar.gz" +MODSECURITY_LINK="https://github.com/owasp-modsecurity/ModSecurity/releases/download/v${MODSECURITY_VERSION}/${MODSECURITY_FILE}" +MODSECURITY_OUT="$CWD/build/modsecurity" + +CORERULESET_FILE="coreruleset-${CORERULESET_VERSION}-minimal.tar.gz" +CORERULESET_LINK="https://github.com/coreruleset/coreruleset/releases/download/v${CORERULESET_VERSION}/${CORERULESET_FILE}" +CORERULESET_OUT="$CWD/build/coreruleset" + +SPOA_FILE="spoa-modsecurity.zip" +SPOA_LINK="https://github.com/FireBurn/spoa-modsecurity/archive/refs/heads/master.zip" +SPOA_OUT="$CWD/build/spoa-modsecurity" + +mkdir -p tarballs sources build + +cd tarballs + +printf "Downloading openssl\n" +if ! [ -f "$OPENSSL_FILE" ]; then + $DOWNLOAD "$OPENSSL_LINK" -o "$OPENSSL_FILE" +fi +printf "Downloading pcre2\n" +if ! [ -f "$PCRE2_FILE" ]; then + $DOWNLOAD "$PCRE2_LINK" -o "$PCRE2_FILE" +fi +printf "Downloading zlib\n" +if ! [ -f "$ZLIB_FILE" ]; then + $DOWNLOAD "$ZLIB_LINK" -o "$ZLIB_FILE" +fi +printf "Downloading lua\n" +if ! [ -f "$LUA_FILE" ]; then + $DOWNLOAD "$LUA_LINK" -o "$LUA_FILE" +fi +printf "Downloading haproxy\n" +if ! [ -f "$HAPROXY_FILE" ]; then + $DOWNLOAD "$HAPROXY_LINK" -o "$HAPROXY_FILE" +fi +printf "Downloading haproxy-ja4\n" +if ! [ -f "$JA4_FILE" ]; then + $DOWNLOAD "$JA4_LINK" -o "$JA4_FILE" +fi +printf "Downloading darkhttpd\n" +if ! [ -f "$DARKHTTPD_FILE" ]; then + $DOWNLOAD "$DARKHTTPD_LINK" -o "$DARKHTTPD_FILE" +fi +printf "Downloading modsecurity\n" +if ! [ -f "$MODSECURITY_FILE" ]; then + $DOWNLOAD "$MODSECURITY_LINK" -o "$MODSECURITY_FILE" +fi +printf "Downloading modsecurity coreruleset\n" +if ! [ -f "$CORERULESET_FILE" ]; then + $DOWNLOAD "$CORERULESET_LINK" -o "$CORERULESET_FILE" +fi +printf "Downloading spoa-modsecurity\n" +if ! [ -f "$SPOA_FILE" ]; then + $DOWNLOAD "$SPOA_LINK" -o "$SPOA_FILE" +fi + +cd .. + +cd sources + +printf "Extracting openssl\n" +tar -xf "../tarballs/${OPENSSL_FILE}" +printf "Extracting pcre2\n" +tar -xf "../tarballs/${PCRE2_FILE}" +printf "Extracting zlib\n" +tar -xf "../tarballs/${ZLIB_FILE}" +printf "Extracting lua\n" +tar -xf "../tarballs/${LUA_FILE}" +printf "Extracting haproxy\n" +tar -xf "../tarballs/${HAPROXY_FILE}" +printf "Extracting darkhttpd\n" +tar -xf "../tarballs/${DARKHTTPD_FILE}" +printf "Extracting modsecurity\n" +tar -xf "../tarballs/${MODSECURITY_FILE}" +printf "Extracting modsecurity coreruleset\n" +tar -xf "../tarballs/${CORERULESET_FILE}" +printf "Extracting spoa-modsecurity\n" +unzip "../tarballs/${SPOA_FILE}" +printf "Extracting haproxy-ja4\n" +unzip "../tarballs/${JA4_FILE}" + + +NPROC=$(nproc) + +# Build openssl +cd "openssl-${OPENSSL_VERSION}" +mkdir -p "$OPENSSL_OUT" +./config --prefix="$OPENSSL_OUT" no-shared no-tests +make -j "$NPROC" +make install_sw +cd .. + +# Build pcre2 +cd "pcre2-${PCRE2_VERSION}" +mkdir -p "$PCRE2_OUT" +CFLAGS='-O2' ./configure --prefix="$PCRE2_OUT" --disable-shared --enable-jit +make -j "$NPROC" +make install +cd .. + +# Build zlib +cd "zlib-${ZLIB_VERSION}" +mkdir -p "$ZLIB_OUT" +./configure --static --prefix="$ZLIB_OUT" +make -j "$NPROC" +make install +cd .. + +# Build lua +cd "lua-${LUA_VERSION}" +mkdir -p "$LUA_OUT" +make -j "$NPROC" +make all install INSTALL_TOP="$LUA_OUT" +cd .. + +# Build darkhttpd +cd "darkhttpd-${DARKHTTPD_VERSION}" +mkdir -p "$DARKHTTPD_OUT" +make -j "$NPROC" darkhttpd-static +cp darkhttpd-static "${DARKHTTPD_OUT}/darkhttpd" +cd .. + +# Build modsecurity +cd "modsecurity-v${MODSECURITY_VERSION}" +mkdir -p "$MODSECURITY_OUT" +./configure --prefix="$MODSECURITY_OUT" --without-lua --without-geoip --without-maxmind --with-pcre2="$PCRE2_OUT" +make -j "$NPROC" +make install +cd .. + +# Combine coreruleset to single file +cd "coreruleset-${CORERULESET_VERSION}" +mkdir -p "$CORERULESET_OUT" +cp "../modsecurity-v${MODSECURITY_VERSION}/modsecurity.conf-recommended" modsecurity.conf +cp "../modsecurity-v${MODSECURITY_VERSION}/unicode.mapping" unicode.mapping +cat crs-setup.conf.example >> modsecurity.conf +sed -i 's/SecRuleEngine .*/SecRuleEngine On/' modsecurity.conf +sed -i 's/\(SecDefaultAction "phase:[12]\),log,auditlog,pass"/\1,log,auditlog,deny,status:403/' modsecurity.conf +sed -i 's/^\(http:\/\/127.0.0.1\|http:\/\/localhost\)$/#\1/' rules/ssrf.data +find . -type f -name "*.conf" | grep -v -e "./modsecurity.conf" -e "./plugins" | sort | sed 's/^/Include /' >> modsecurity.conf +cp -r modsecurity.conf unicode.mapping rules "${CORERULESET_OUT}" +cd .. + + +# Build spoa-modsecurity +cd "spoa-modsecurity-master" +mkdir -p "${SPOA_OUT}/bin" +make -j "$NPROC" MODSEC_INC="${MODSECURITY_OUT}/include" MODSEC_LIB="${MODSECURITY_OUT}/lib" +make install PREFIX="$SPOA_OUT" +cd .. + +# Build haproxy +cd "haproxy-${HAPROXY_VERSION}" +mkdir -p "$HAPROXY_OUT" +make -j "$NPROC" TARGET=linux-glibc-legacy \ + USE_TFO=1 USE_LINUX_TPROXY=1 USE_GETADDRINFO=1 \ + USE_OPENSSL=1 SSL_INC="${OPENSSL_OUT}/include" SSL_LIB="${OPENSSL_OUT}/lib64" \ + USE_STATIC_PCRE2=1 PCRE2_INC="${PCRE2_OUT}/include" PCRE2_LIB="${PCRE2_OUT}/lib" \ + USE_ZLIB=1 ZLIB_INC="${ZLIB_OUT}/include" ZLIB_LIB="${ZLIB_OUT}/lib" \ + USE_LUA=1 LUA_INC="${LUA_OUT}/include" LUA_LIB="${LUA_OUT}/lib" +make install DESTDIR="$HAPROXY_OUT" PREFIX="" +cd .. + +# Update JA4 fingerprint map file +printf "Downloading updated JA4 fingerprints\n" +cd "haproxy-ja4-latest" +mkdir -p "$JA4_OUT" +if ! [ -f "$JA4DB_FILE" ]; then + $DOWNLOAD "$JA4DB_LINK" -o "$JA4DB_FILE" +fi +python3 ja4db-dedupe.py +python3 ja4db-to-map.py +cp ja4.map "${JA4_OUT}/ja4_names.map" +cp ja4.lua "$JA4_OUT" +cd .. + +cd .. + +# Default page for a blocked request will show a random GIF or video +printf "Downloading videos\n" +mkdir -p build/videos +cp video_scripts/add_video.sh video_scripts/get_videos.sh build/videos +chmod u+x build/videos/* +cd build/videos +./get_videos.sh +cd ../.. + +printf "Creating distribution in ./dist\n" +mkdir -p dist/haproxy dist/darkhttpd dist/modsecurity +cp services/*.service dist/ + +cp "${HAPROXY_OUT}/sbin/haproxy" dist/haproxy +chmod 655 dist/haproxy/haproxy +cp "${JA4_OUT}/ja4.lua" "${JA4_OUT}/ja4_names.map" dist/haproxy +printf 'd00i000000_74c887e210ea_8e2f6cc4d42a not_encrypted\n' >> dist/haproxy/ja4_names.map +cp haproxy_conf/* dist/haproxy/ +touch dist/haproxy/ja4_block.map dist/haproxy/ja4_allow.map dist/haproxy/ip_block.map dist/haproxy/ip_allow.map + +cp "${DARKHTTPD_OUT}/darkhttpd" dist/darkhttpd/ +chmod 655 dist/darkhttpd/darkhttpd +cp -r build/videos/srv dist/darkhttpd/ + +cp "${MODSECURITY_OUT}/lib/libmodsecurity.so" dist/modsecurity +cp "${SPOA_OUT}/bin/modsecurity" dist/modsecurity/modsecurity +chmod 655 dist/modsecurity/modsecurity +cp -r "${CORERULESET_OUT}/modsecurity.conf" "${CORERULESET_OUT}/unicode.mapping" "${CORERULESET_OUT}/rules" dist/modsecurity +touch dist/modsecurity/audit.log + +cp build/videos/videos.bin dist/ + +chmod -R +r dist/ + +printf "Creating test certificate\n" +openssl req -x509 -newkey rsa:4096 -sha256 -nodes -subj "/CN=Some Name" -addext "subjectAltName = DNS:localhost,IP:127.0.0.1" -keyout server.key -out server.crt -days 30 +cat server.key server.crt > dist/haproxy/server.pem +chmod 600 dist/haproxy/server.pem + +cat <<-"EOF" > dist/install.sh + #!/bin/sh + if ! [ "$UID" = "0" ]; then + printf "Script must be run as root\n" + exit + fi + set -x + useradd waf_user || true + chsh -s /bin/false waf_user + mkdir -p /opt/waf_configs/ + cp -r * /opt/waf_configs/ + chown waf_user:root /opt/waf_configs/modsecurity/audit.log + chown waf_user:root /opt/waf_configs/haproxy/server.pem + ln -s /opt/waf_configs/*.service /etc/systemd/system/ + systemctl daemon-reload + systemctl enable waf.service waf_haproxy.service waf_modsecurity.service waf_darkhttpd.service + systemctl start waf.service + set +x + + printf "\n" + printf "Start the WAF with 'systemctl start waf.service'\n" + printf "Get status with 'systemctl status waf*'\n" + printf "Edit '/opt/waf_configs/haproxy.cfg' to change configuration settings for the proxy\n" + printf " and edit the sandbox service file at '/opt/waf_configs/waf_haproxy.service'\n" + printf "Block JA4 hashes by appending to '/opt/waf_configs/haproxy/ja4_block.map', then\n" + printf " run 'systemctl reload waf_haproxy'.\n" + printf " The inode of the file **must not change**, so append with '>>' or configure\n" + printf " your editor to overwrite the file directly (e.g. ':set backupcopy=yes' in vim)\n" + EOF +chmod +x dist/install.sh +tar -cf dist.tar dist + +printf "Copy 'dist.tar' to machine, extract, then install with 'dist/install.sh'\n" diff --git a/haproxy_conf/haproxy.cfg b/haproxy_conf/haproxy.cfg new file mode 100644 index 0000000..5d8707e --- /dev/null +++ b/haproxy_conf/haproxy.cfg @@ -0,0 +1,138 @@ +global + log /dev/log local0 + log /dev/log local1 notice + daemon + + # See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate + 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 + + tune.lua.bool-sample-conversion normal + tune.ssl.capture-buffer-size 96 + lua-load /etc/haproxy/ja4.lua + +defaults + log global + mode http + option dontlognull + log-format "%{+json}o %(client_ip)ci %(client_port)cp %(request_date)tr %(fe_name_transport)ft %(be_name)b %(status_code)ST %(bytes_read)B %(captured_request_headers)hr %(captured_response_headers)hs %(http_request){+Q}r" + timeout connect 5000 + timeout client 50000 + timeout server 50000 + +# Used to keep data from stick tables when reloading the service +peers persist_st + peer host 127.0.0.1:10000 + +# Stick table - Record number of http requests in the last 1m +backend st_rate_limit + stick-table type ipv6 size 10k expire 10m store http_req_rate(1m) peers persist_st + +# Stick table - Record number of waf blocks in the last 10s. Used to make +# quickly repeating requests get blocked immediately until the client slows down +backend st_waf_debounce + stick-table type ipv6 size 10k expire 1m store gpc0_rate(10s) peers persist_st + +# Stick table - Record number of waf blocks in the last 1h. Used to keep a +# client blocked for a while if it triggers too many blocks +backend st_waf_trigger + stick-table type ipv6 size 10k expire 2h store gpc0_rate(1h) peers persist_st + + + +frontend incoming_http + # Ports that haproxy will listen on + bind :80 + bind :443 ssl crt /etc/haproxy/server.pem + + log-format "%{+json}o %(client_ip)ci %(client_port)cp %(request_date)tr %(fe_name_transport)ft %(unique_id)ID %(be_name)b %(status_code)ST %(bytes_read)B %(JA4_hash)[capture.req.hdr(1)] %(JA4_app_match)[capture.req.hdr(2)] %(JA4_block_rule)[capture.req.hdr(3)] %(JA4_allow_rule)[capture.req.hdr(4)] %(IP_block_rule)[capture.req.hdr(5)] %(IP_allow_rule)[capture.req.hdr(6)] %(modsecurity_code)[capture.req.hdr(7)] %(block_reason)[capture.req.hdr(8)] %(captured_response_headers)hs %(http_request){+Q}r %(user_agent)[capture.req.hdr(0)]" + + http-request capture req.hdr(User-Agent) len 512 + + # Calculate ja4 hash and log + http-request lua.fingerprint_ja4 + http-request set-var(txn.fingerprint_app) var(txn.fingerprint_ja4),map(/etc/haproxy/ja4_names.map) + http-request capture var(txn.fingerprint_ja4) len 36 + http-request capture var(txn.fingerprint_app) len 200 + + # Check JA4 allow/blocklists + acl ja4_allow var(txn.fingerprint_ja4) -m beg -M -f /etc/haproxy/ja4_allow.map + acl ja4_block var(txn.fingerprint_ja4) -m beg -M -f /etc/haproxy/ja4_block.map + http-request set-var(txn.ja4_block_match) var(txn.fingerprint_ja4),map(/etc/haproxy/ja4_block.map) + http-request set-var(txn.ja4_allow_match) var(txn.fingerprint_ja4),map(/etc/haproxy/ja4_allow.map) + http-request capture var(txn.ja4_block_match) len 200 + http-request capture var(txn.ja4_allow_match) len 200 + + # Check IP allow/blocklists + acl ip_allow src -m ip -M -f /etc/haproxy/ip_allow.map + acl ip_block src -m ip -M -f /etc/haproxy/ip_block.map + http-request set-var(txn.ip_block_match) src,map_ip(/etc/haproxy/ip_block.map) + http-request set-var(txn.ip_allow_match) src,map_ip(/etc/haproxy/ip_allow.map) + http-request capture var(txn.ip_block_match) len 200 + http-request capture var(txn.ip_allow_match) len 200 + + # Use stick tables to record & check rate limits + http-request track-sc0 src table st_rate_limit + http-request track-sc1 src table st_waf_debounce + http-request track-sc2 src table st_waf_trigger + + # Configuration for rate limit speed. Active if this many requests have + # been made in the last 1m. See backend st_rate_limit's http_req_rate(1m) + # setting + acl rate_limit_high sc0_http_req_rate(st_rate_limit) gt 100 + acl rate_limit_low sc0_http_req_rate(st_rate_limit) gt 20 + + # Run the request through modsecurity to check for malicious input + filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf + http-request capture var(txn.modsec.code) len 8 + + acl modsec_block var(txn.modsec.code) -m int gt 0 # Will return 403 if block is recommended + + # Configurations for blocking based on previous blocks for this IP + acl trigger sc2_gpc0_rate(st_waf_trigger) gt 40 + acl trigger_debounce sc1_gpc0_rate(st_waf_debounce) gt 0 + + # Boolean for if the request should be blocked + acl waf_triggered acl(ip_block) -m bool true + acl waf_triggered acl(ja4_block) -m bool true + acl waf_triggered acl(rate_limit_high) -m bool true + acl waf_triggered acl(rate_limit_low) -m bool true + acl waf_triggered acl(modsec_block) -m bool true + acl waf_triggered acl(trigger_debounce) -m bool true + acl waf_triggered acl(trigger) -m bool true + + # Set block reason in log + http-request set-var(txn.block_reason) str("n/a") if !waf_triggered + http-request set-var(txn.block_reason) str("Recent block - debounce") if trigger_debounce + http-request set-var(txn.block_reason) str("ModSecurity triggered") if modsec_block + http-request set-var(txn.block_reason) str("Rate limit - low") if rate_limit_low + http-request set-var(txn.block_reason) str("Rate limit - high") if rate_limit_high + http-request set-var(txn.block_reason) str("High block rate") if trigger + http-request set-var(txn.block_reason) str("JA4 block") if ja4_block + http-request set-var(txn.block_reason) str("IP block") if ip_block + http-request capture var(txn.block_reason) len 24 + + # Increment stick table counters for st_waf_debounce and st_waf_trigger + http-request sc-inc-gpc0(1) if waf_triggered + http-request sc-inc-gpc0(2) if waf_triggered + + # Send a 429 response if the client is making way too many requests + http-request deny deny_status 429 if rate_limit_high !ip_allow !ja4_allow + + # Otherwise choose backend based on whether the request was blocked. Sends + # a 200 status code regardless of whether it was blocked. + use_backend backend_normal if ip_allow || ja4_allow + use_backend backend_ban if waf_triggered + default_backend backend_normal + +backend spoe-modsecurity + mode tcp + server modsec-spoa 127.0.0.1:19824 + +backend backend_ban + server banserver 127.0.0.1:23610 + +# Edit this to point the the actual server this firewall is protecting! +backend backend_normal + server normalserver 127.0.0.1:2222 diff --git a/haproxy_conf/spoe-modsecurity.conf b/haproxy_conf/spoe-modsecurity.conf new file mode 100644 index 0000000..45612fc --- /dev/null +++ b/haproxy_conf/spoe-modsecurity.conf @@ -0,0 +1,14 @@ +[modsecurity] + +spoe-agent modsecurity-agent + messages check-request + option var-prefix modsec + timeout hello 100ms + timeout idle 30s + timeout processing 50ms + use-backend spoe-modsecurity + +spoe-message check-request + args unique-id src src_port dst dst_port method path query req.ver req.hdrs_bin req.body_size req.body + event on-frontend-http-request + diff --git a/services/waf.service b/services/waf.service new file mode 100644 index 0000000..14ebb55 --- /dev/null +++ b/services/waf.service @@ -0,0 +1,10 @@ +[Unit] +Description=Web Application Firewall Group + +[Service] +Type=oneshot +ExecStart=/bin/true +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/services/waf_darkhttpd.service b/services/waf_darkhttpd.service new file mode 100644 index 0000000..62abb8c --- /dev/null +++ b/services/waf_darkhttpd.service @@ -0,0 +1,98 @@ +[Unit] +Description=Darkhttpd server +PartOf=waf.service +After=waf.service + +[Service] +Restart=on-failure +ExecStart=/bin/darkhttpd /srv --port 23610 --addr 127.0.0.1 + +# Sandboxing ------------------------------------------------------------------- + +## Files & mounts -------------------------------------------------------------- + +PrivateMounts=true + +# Hide all files that aren't explicitly bound +TemporaryFileSystem=/:ro +TemporaryFileSystem=/tmp +PrivateTmp=true + +# Adds /dev/null, /dev/zero, and /dev/random +PrivateDevices=true + +# Dynamically linked libraries +BindReadOnlyPaths=/lib /lib64 /usr/lib /usr/lib64 + +# Needed for reloading the service +BindReadOnlyPaths=/bin/kill + +## Needed to notify systemd of service status +BindPaths=/run/systemd/notify + +# DNS +BindReadOnlyPaths=/etc/resolv.conf + +# Configuration files & binary +BindReadOnlyPaths=/opt/waf_configs/darkhttpd/darkhttpd:/bin/darkhttpd +BindReadOnlyPaths=/opt/waf_configs/darkhttpd/srv:/srv + +# Only allow executing binaries in /bin +NoExecPaths=/ +ExecPaths=/bin + +UMask=0077 + +## User ------------------------------------------------------------------------ + +User=waf_user + +## Restrictions ---------------------------------------------------------------- + +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictFileSystems=tmpfs +RestrictNamespaces=true +MemoryDenyWriteExecute=true + +ProtectProc=invisible +ProcSubset=pid + +## Capabilities & system calls ------------------------------------------------- +# Some of these are redundant with the readonly filesystem, but can be useful +# if more access is needed to the filesystem + +# Prevent privilege escalation +NoNewPrivileges=true + +# Allow binding ports +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +# (Needs v249 or later) Restrict ports the service can listen on +SocketBindAllow=tcp:23610 +SocketBindDeny=any + +# Disable risky system calls +SystemCallFilter=@system-service +SystemCallFilter=~@cpu-emulation @keyring @module @obsolete @raw-io +SystemCallFilter=~@reboot @swap @sync + +# Prevent changes to the system time +ProtectClock=true + +# Prevent changes to the system hostname +ProtectHostname=true + +# Prevent changes to the kernel +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true + +# Prevent changes to control groups (used for containers) +ProtectControlGroups=true + +# Prevent possible bugs in code for other architectures +SystemCallArchitectures=native +LockPersonality=true + +[Install] +WantedBy=waf.service diff --git a/services/waf_haproxy.service b/services/waf_haproxy.service new file mode 100644 index 0000000..f59ae9b --- /dev/null +++ b/services/waf_haproxy.service @@ -0,0 +1,114 @@ +[Unit] +Description=HAProxy Load Balancer +PartOf=waf.service +After=waf.service + +[Service] +KillMode=mixed +Restart=always +SuccessExitStatus=143 +Type=notify + +Environment="CONFIG=/etc/haproxy/haproxy.cfg" "PIDFILE=/tmp/haproxy.pid" +ExecStart=/bin/haproxy -Ws -f $CONFIG -p $PIDFILE +ExecReload=/bin/haproxy -Ws -f $CONFIG -c -q +ExecReload=/bin/kill -USR2 $MAINPID + +# Sandboxing ------------------------------------------------------------------- + +## Files & mounts -------------------------------------------------------------- + +PrivateMounts=true + +# Hide all files that aren't explicitly bound +TemporaryFileSystem=/:ro +TemporaryFileSystem=/tmp +PrivateTmp=true + +# Adds /dev/null, /dev/zero, and /dev/random +PrivateDevices=true + +# Dynamically linked libraries +BindReadOnlyPaths=/lib /lib64 /usr/lib /usr/lib64 + +# Needed for reloading the service +BindReadOnlyPaths=/bin/kill + +## Needed to notify systemd of service status +BindPaths=/run/systemd/notify + +# DNS +BindReadOnlyPaths=/etc/resolv.conf + +# Configuration files & binary +BindReadOnlyPaths=/opt/waf_configs/haproxy/haproxy:/bin/haproxy +BindReadOnlyPaths=/opt/waf_configs/haproxy/haproxy.cfg:/etc/haproxy/haproxy.cfg +BindReadOnlyPaths=/opt/waf_configs/haproxy/ja4.lua:/etc/haproxy/ja4.lua +BindReadOnlyPaths=/opt/waf_configs/haproxy/ja4_names.map:/etc/haproxy/ja4_names.map +BindReadOnlyPaths=/opt/waf_configs/haproxy/ja4_block.map:/etc/haproxy/ja4_block.map +BindReadOnlyPaths=/opt/waf_configs/haproxy/ja4_allow.map:/etc/haproxy/ja4_allow.map +BindReadOnlyPaths=/opt/waf_configs/haproxy/ip_block.map:/etc/haproxy/ip_block.map +BindReadOnlyPaths=/opt/waf_configs/haproxy/ip_allow.map:/etc/haproxy/ip_allow.map +BindReadOnlyPaths=-/opt/waf_configs/haproxy/server.pem:/etc/haproxy/server.pem +BindReadOnlyPaths=/opt/waf_configs/haproxy/spoe-modsecurity.conf:/etc/haproxy/spoe-modsecurity.conf + +# Only allow executing binaries in /bin +NoExecPaths=/ +ExecPaths=/bin + +UMask=0077 + +## User ------------------------------------------------------------------------ + +User=waf_user + +## Restrictions ---------------------------------------------------------------- + +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictFileSystems=tmpfs +RestrictNamespaces=true +MemoryDenyWriteExecute=true + +ProtectProc=invisible +ProcSubset=pid + +## Capabilities & system calls ------------------------------------------------- +# Some of these are redundant with the readonly filesystem, but can be useful +# if more access is needed to the filesystem + +# Prevent privilege escalation +NoNewPrivileges=true + +# Allow binding ports +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +# (Needs v249 or later) Restrict ports the service can listen on +SocketBindAllow=tcp:80 +SocketBindAllow=tcp:443 +SocketBindDeny=any + +# Disable risky system calls +SystemCallFilter=@system-service +SystemCallFilter=~@cpu-emulation @keyring @module @obsolete @raw-io +SystemCallFilter=~@reboot @swap @sync + +# Prevent changes to the system time +ProtectClock=true + +# Prevent changes to the system hostname +ProtectHostname=true + +# Prevent changes to the kernel +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true + +# Prevent changes to control groups (used for containers) +ProtectControlGroups=true + +# Prevent possible bugs in code for other architectures +SystemCallArchitectures=native +LockPersonality=true + +[Install] +WantedBy=waf.service diff --git a/services/waf_modsecurity.service b/services/waf_modsecurity.service new file mode 100644 index 0000000..88771f4 --- /dev/null +++ b/services/waf_modsecurity.service @@ -0,0 +1,104 @@ +[Unit] +Description=Modsecurity SPOA server +PartOf=waf.service +After=waf.service + +[Service] +Restart=always +ExecStart=/bin/modsecurity -p 19824 -f /etc/modsecurity/modsecurity.conf + +# Sandboxing ------------------------------------------------------------------- + +## Files & mounts -------------------------------------------------------------- + +PrivateMounts=true + +# Hide all files that aren't explicitly bound +TemporaryFileSystem=/:ro +TemporaryFileSystem=/tmp +PrivateTmp=true + +# Adds /dev/null, /dev/zero, and /dev/random +PrivateDevices=true + +# Dynamically linked libraries +BindReadOnlyPaths=/lib /lib64 /usr/lib /usr/lib64 + +# Needed for reloading the service +BindReadOnlyPaths=/bin/kill + +## Needed to notify systemd of service status +BindPaths=/run/systemd/notify + +# DNS +BindReadOnlyPaths=/etc/resolv.conf + +# Configuration files & binary +BindReadOnlyPaths=/opt/waf_configs/modsecurity/modsecurity:/bin/modsecurity +BindReadOnlyPaths=/opt/waf_configs/modsecurity/libmodsecurity.so:/lib/libmodsecurity.so.3 +BindReadOnlyPaths=/opt/waf_configs/modsecurity/modsecurity.conf:/etc/modsecurity/modsecurity.conf +BindReadOnlyPaths=/opt/waf_configs/modsecurity/rules:/etc/modsecurity/rules +BindReadOnlyPaths=/opt/waf_configs/modsecurity/unicode.mapping:/etc/modsecurity/unicode.mapping + +# Log files +BindPaths=/opt/waf_configs/modsecurity/audit.log:/var/log/modsec_audit.log + +# Only allow executing binaries in /bin +NoExecPaths=/ +ExecPaths=/bin + +UMask=0077 + +## User ------------------------------------------------------------------------ + +User=waf_user + +## Restrictions ---------------------------------------------------------------- + +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictFileSystems=tmpfs +RestrictNamespaces=true +MemoryDenyWriteExecute=true + +ProtectProc=invisible +ProcSubset=pid + +## Capabilities & system calls ------------------------------------------------- +# Some of these are redundant with the readonly filesystem, but can be useful +# if more access is needed to the filesystem + +# Prevent privilege escalation +NoNewPrivileges=true + +# Allow binding ports +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +# (Needs v249 or later) Restrict ports the service can listen on +SocketBindAllow=tcp:19824 +SocketBindDeny=any + +# Disable risky system calls +SystemCallFilter=@system-service +SystemCallFilter=~@cpu-emulation @keyring @module @obsolete @raw-io +SystemCallFilter=~@reboot @swap @sync + +# Prevent changes to the system time +ProtectClock=true + +# Prevent changes to the system hostname +ProtectHostname=true + +# Prevent changes to the kernel +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true + +# Prevent changes to control groups (used for containers) +ProtectControlGroups=true + +# Prevent possible bugs in code for other architectures +SystemCallArchitectures=native +LockPersonality=true + +[Install] +WantedBy=waf.service diff --git a/video_scripts/add_video.sh b/video_scripts/add_video.sh new file mode 100644 index 0000000..18ea157 --- /dev/null +++ b/video_scripts/add_video.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# This script converts a GIF or video to an asciinema recording and creates +# a self-contained C header file that plays the video on repeat + +set -e + +help_text() { + echo "Usage: $0