├── test ├── Dockerfile ├── curl.sh ├── spoe-modsecurity.conf ├── haproxy.cfg ├── docker-compose.yml └── run.sh ├── .github ├── workflows │ ├── test.yml │ └── image.yaml └── dependabot.yml ├── rootfs ├── spoa.patch ├── start.sh └── Dockerfile ├── README.md └── LICENSE /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN apk add --no-cache bash tini curl 4 | 5 | COPY ./curl.sh / 6 | 7 | ENTRYPOINT ["tini", "--", "/curl.sh"] 8 | -------------------------------------------------------------------------------- /test/curl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Split by comma 5 | IFS=',' read -r -a lines <<< "$@" 6 | 7 | for x in "${lines[@]}"; do 8 | # Split by space 9 | IFS=' ' read -r -a param <<< "${x[@]}" 10 | echo "<======== ${param[*]} ========>" 11 | curl "${param[@]}" 12 | echo -e '\n' 13 | done 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v6 14 | - name: Run 15 | run: | 16 | cd test 17 | ./run.sh 18 | -------------------------------------------------------------------------------- /test/spoe-modsecurity.conf: -------------------------------------------------------------------------------- 1 | [modsecurity] 2 | spoe-agent modsecurity-agent 3 | messages check-request 4 | option var-prefix modsec 5 | timeout hello 100ms 6 | timeout idle 30s 7 | timeout processing 1s 8 | use-backend spoe-modsecurity 9 | spoe-message check-request 10 | args unique-id method path query req.ver req.hdrs_bin req.body_size req.body 11 | event on-frontend-http-request 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://dependabot.com/docs/config-file-beta/validator/ 2 | --- 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | 11 | # find * -iname dockerfile* -exec dirname {} \; | sort -u | uniq 12 | 13 | - package-ecosystem: "docker" 14 | directory: "rootfs" 15 | schedule: 16 | interval: "monthly" 17 | 18 | - package-ecosystem: "docker" 19 | directory: "test" 20 | schedule: 21 | interval: "monthly" 22 | -------------------------------------------------------------------------------- /test/haproxy.cfg: -------------------------------------------------------------------------------- 1 | defaults 2 | # log global 3 | maxconn 100 4 | option redispatch 5 | option dontlognull 6 | option http-server-close 7 | option http-keep-alive 8 | timeout client 50s 9 | timeout client-fin 50s 10 | timeout connect 5s 11 | timeout http-keep-alive 1m 12 | timeout http-request 5s 13 | timeout queue 5s 14 | timeout server 50s 15 | timeout server-fin 50s 16 | timeout tunnel 1h 17 | 18 | frontend httpfront 19 | mode http 20 | bind *:8080 21 | filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf 22 | http-request deny if { var(txn.modsec.code) -m int gt 0 } 23 | default_backend echoserver 24 | 25 | backend spoe-modsecurity 26 | mode tcp 27 | timeout connect 5s 28 | timeout server 5s 29 | server modsec-spoa1 modsecurity-spoa:12345 30 | 31 | backend echoserver 32 | mode http 33 | server echoserver echoserver:8000 check weight 1 check inter 2s 34 | -------------------------------------------------------------------------------- /rootfs/spoa.patch: -------------------------------------------------------------------------------- 1 | --- Makefile.orig 2 | +++ Makefile 3 | @@ -35,7 +35,7 @@ 4 | 5 | CFLAGS += -g -Wall -pthread 6 | INCS += -Iinclude -I$(MODSEC_INC) -I$(APACHE2_INC) -I$(APR_INC) -I$(LIBXML_INC) -I$(EVENT_INC) 7 | -LIBS += -lpthread $(EVENT_LIB) -levent_pthreads -lcurl -lapr-1 -laprutil-1 -lxml2 -lpcre -lyajl 8 | +LIBS += -lpthread $(EVENT_LIB) -levent_pthreads -lcurl -lapr-1 -laprutil-1 -lxml2 -lpcre -lpcre2-8 -lyajl 9 | 10 | OBJS = spoa.o modsec_wrapper.o 11 | 12 | --- spoa.c.orig 13 | +++ spoa.c 14 | @@ -1244,7 +1244,7 @@ 15 | { 16 | struct worker *worker = arg; 17 | 18 | - LOG(worker, "%u clients connected", worker->nbclients); 19 | + DEBUG(worker, "%u clients connected", worker->nbclients); 20 | } 21 | 22 | static void 23 | @@ -1478,7 +1478,7 @@ 24 | goto disconnect; 25 | } 26 | if (client->status_code != SPOE_FRM_ERR_NONE) 27 | - LOG(client->worker, "<%lu> Peer closed connection: %s", 28 | + DEBUG(client->worker, "<%lu> Peer closed connection: %s", 29 | client->id, spoe_frm_err_reasons[client->status_code]); 30 | goto disconnect; 31 | } 32 | -------------------------------------------------------------------------------- /rootfs/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ $# -gt 0 ] && [ "$1" = "${1#-}" ]; then 5 | # First char isn't `-`, probably a `docker run -ti ` 6 | # Just exec and exit 7 | exec "$@" 8 | exit 9 | fi 10 | 11 | unset options configFiles 12 | while [ $# -gt 0 ]; do 13 | case "$1" in 14 | -f) 15 | shift 16 | configFiles="$configFiles $1" 17 | ;; 18 | --) 19 | shift 20 | configFiles="$configFiles $@" 21 | break 22 | ;; 23 | *) 24 | options="$options $1" 25 | ;; 26 | esac 27 | shift 28 | done 29 | 30 | configFiles="${configFiles:-/etc/modsecurity/modsecurity.conf /etc/modsecurity/owasp-modsecurity-crs.conf}" 31 | 32 | conf=$(mktemp) 33 | for f in $configFiles; do 34 | if [ ! -f "$f" ]; then 35 | echo "File not found: $f" >&2 36 | exit 1 37 | fi 38 | echo "Include $f" 39 | done > $conf 40 | 41 | echo "Using options:${options:- }" 42 | echo "Using config files:" 43 | sed -n 's/Include / - /p' $conf 44 | 45 | exec modsecurity $options -f $conf 46 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.6' 3 | services: 4 | 5 | modsecurity-spoa: 6 | hostname: modsec 7 | build: 8 | context: ../rootfs 9 | dockerfile: Dockerfile 10 | image: modsecurity-spoa-test 11 | networks: 12 | - modsec 13 | 14 | haproxy: 15 | hostname: haproxy 16 | image: haproxy:3.0-alpine 17 | volumes: 18 | - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro 19 | - ./spoe-modsecurity.conf:/etc/haproxy/spoe-modsecurity.conf:ro 20 | networks: 21 | - modsec 22 | - echoserver 23 | - client 24 | depends_on: 25 | - modsecurity-spoa 26 | - echoserver 27 | 28 | echoserver: 29 | hostname: echoserver 30 | image: jcmoraisjr/dory 31 | networks: 32 | - echoserver 33 | 34 | client: 35 | hostname: echoserver 36 | build: 37 | context: . 38 | dockerfile: Dockerfile 39 | image: curl-test 40 | # Split parameters with ',' 41 | command: > 42 | haproxy:8080, 43 | haproxy:8080?p=/etc/passwd, 44 | -X POST haproxy:8080 45 | networks: 46 | - client 47 | depends_on: 48 | - haproxy 49 | 50 | networks: 51 | modsec: 52 | echoserver: 53 | client: 54 | -------------------------------------------------------------------------------- /.github/workflows/image.yaml: -------------------------------------------------------------------------------- 1 | name: image 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | build-image: 8 | runs-on: ubuntu-latest 9 | env: 10 | EXTRA_TAGS: 11 | steps: 12 | - name: Configure envvars 13 | run: | 14 | GIT_TAG="${GITHUB_REF#refs/tags/}" 15 | TAGS=$( 16 | for repository in quay.io/jcmoraisjr; do 17 | for project in modsecurity-spoa; do 18 | for tag in "$GIT_TAG" ${{ env.EXTRA_TAGS }}; do 19 | echo -n "${repository}/${project}:${tag}," 20 | done 21 | done 22 | done 23 | ) 24 | echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV 25 | echo "TAGS=$TAGS" >> $GITHUB_ENV 26 | - uses: actions/checkout@v6 27 | - uses: docker/login-action@v3 28 | with: 29 | registry: docker.io 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | - uses: docker/login-action@v3 33 | with: 34 | registry: quay.io 35 | username: ${{ secrets.QUAY_USERNAME }} 36 | password: ${{ secrets.QUAY_TOKEN }} 37 | - uses: docker/setup-qemu-action@v3 38 | - uses: docker/setup-buildx-action@v3 39 | - uses: docker/build-push-action@v6 40 | with: 41 | context: rootfs 42 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 43 | push: true 44 | tags: ${{ env.TAGS }} 45 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | PROJECT_NAME="modsec" 5 | COMPOSE="docker-compose" 6 | if ! which $COMPOSE; then 7 | COMPOSE="docker compose" 8 | fi 9 | 10 | # Functions 11 | 12 | function ci_log_entry() { 13 | 14 | local LOG_TYPE="${1:?Needs log type}" 15 | local LOG_MSG="${2:?Needs log message}" 16 | local COLOR='\033[93m' 17 | local ENDCOLOR='\033[0m' 18 | 19 | echo -e "${COLOR}$(date "+%Y-%m-%d %H:%M:%S") [$LOG_TYPE] ${LOG_MSG}${ENDCOLOR}" 20 | 21 | } 22 | 23 | # ci_log_group_* functions are used to make Github Actions output cleaner 24 | function ci_log_group_start(){ 25 | local LOG_TYPE="${1:?Needs log type}" 26 | local LOG_MSG="${2:?Needs log message}" 27 | 28 | if [ -n "${CI:-}" ]; then 29 | echo "::group::${LOG_MSG}" 30 | else 31 | ci_log_entry "${LOG_TYPE}" "${LOG_MSG}" 32 | fi 33 | } 34 | 35 | function ci_log_group_end { 36 | if [ -n "${CI:-}" ]; then 37 | echo "::endgroup::" 38 | fi 39 | } 40 | 41 | function containers_down { 42 | 43 | ci_log_entry "INFO" "Terminating containers" 44 | $COMPOSE -p "$PROJECT_NAME" down 45 | 46 | } 47 | 48 | # Always terminate containers 49 | 50 | trap containers_down EXIT 51 | 52 | # Run 53 | 54 | ci_log_group_start "INFO" "Build containers" 55 | $COMPOSE -p "$PROJECT_NAME" build 56 | ci_log_group_end 57 | 58 | ci_log_group_start "INFO" "Starting containers" 59 | $COMPOSE -p "$PROJECT_NAME" up --force-recreate --detach haproxy 60 | ci_log_group_end 61 | 62 | ci_log_group_start "INFO" "Running tests" 63 | $COMPOSE -p "$PROJECT_NAME" run client 64 | ci_log_group_end 65 | 66 | ci_log_group_start "INFO" "Showing logs" 67 | $COMPOSE -p "$PROJECT_NAME" logs modsecurity-spoa 68 | ci_log_group_end 69 | -------------------------------------------------------------------------------- /rootfs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22.2 2 | 3 | ARG HAPROXY_MODSEC_REF=3c895f3e7dd291dba19d57ba054b277e6fb80ca4 4 | ARG HAPROXY_MODSEC_SHA256=645ff6fd6fe1394462bd6b82ee5430606c631b83021f8b5cd7eca2b62b427970 5 | ARG MODSEC_VERSION=2.9.11 6 | ARG MODSEC_SHA256=1fe16eb96b6093f062cef73ec8b7ae481a59813766d49a7f5e4d1b85900e239e 7 | 8 | ADD spoa.patch / 9 | 10 | RUN apk upgrade --no-cache \ 11 | && apk add --no-cache --virtual .build-modsecurity \ 12 | curl \ 13 | openssl \ 14 | patch \ 15 | tar \ 16 | make \ 17 | gcc \ 18 | libc-dev \ 19 | linux-headers \ 20 | apache2-dev \ 21 | pcre-dev \ 22 | pcre2-dev \ 23 | libxml2-dev \ 24 | libevent-dev \ 25 | curl-dev \ 26 | yajl-dev \ 27 | && curl -fsSLo /tmp/modsecurity.tar.gz https://github.com/owasp-modsecurity/ModSecurity/releases/download/v${MODSEC_VERSION}/modsecurity-v${MODSEC_VERSION}.tar.gz \ 28 | && curl -fsSLo /tmp/haproxy-modsecurity.zip https://github.com/haproxy/spoa-modsecurity/archive/${HAPROXY_MODSEC_REF}.zip \ 29 | && echo "$MODSEC_SHA256 /tmp/modsecurity.tar.gz" | sha256sum -c \ 30 | && mkdir -p /usr/src/modsecurity \ 31 | && tar xzf /tmp/modsecurity.tar.gz --strip-components=1 -C /usr/src/modsecurity \ 32 | && rm /tmp/modsecurity.tar.gz \ 33 | && echo "$HAPROXY_MODSEC_SHA256 /tmp/haproxy-modsecurity.zip" | sha256sum -c \ 34 | && unzip -d /usr/src /tmp/haproxy-modsecurity.zip \ 35 | && mv /usr/src/spoa-modsecurity-${HAPROXY_MODSEC_REF} /usr/src/haproxy-modsecurity \ 36 | && cd /usr/src/haproxy-modsecurity \ 37 | && patch -p0 /etc/modsecurity/owasp-modsecurity-crs.conf \ 89 | && find \ 90 | /etc/modsecurity/owasp-modsecurity-crs/rules \ 91 | -type f -maxdepth 1 -name '*.conf' \ 92 | | sort | sed 's/^/Include /' >> /etc/modsecurity/owasp-modsecurity-crs.conf 93 | 94 | COPY . / 95 | 96 | ENTRYPOINT ["tini", "--", "/start.sh"] 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAProxy agent for ModSecurity 2 | 3 | HAProxy [agent](http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#9.3) (SPOA) 4 | for [ModSecurity](http://www.modsecurity.org) web application firewall 5 | ([WAF](https://en.wikipedia.org/wiki/Web_application_firewall)). 6 | 7 | [![Docker Repository on Quay](https://quay.io/repository/jcmoraisjr/modsecurity-spoa/status "Docker Repository on Quay")](https://quay.io/repository/jcmoraisjr/modsecurity-spoa) 8 | 9 | ## SPOP and HAProxy Version 10 | 11 | The current [SPOP](https://www.haproxy.org/download/2.2/doc/SPOE.txt) version is v2, used since modsecurity-spoa v0.4. This agent version works on HAProxy 1.8.10 and newer. 12 | 13 | SPOP v1 is used on modsecurity-spoa v0.1 to v0.3. This agent version works on HAProxy up to 1.8.9. 14 | 15 | ## Agent Configuration 16 | 17 | Command line syntax: 18 | 19 | ``` 20 | $ docker run -p 12345:12345 quay.io/jcmoraisjr/modsecurity-spoa [options] [-- [ ...] ] 21 | ``` 22 | 23 | `config-files` can be used either after `--` (see above) or from `-f` option (see below). 24 | The only difference is that the later supports only one filename. All config-files found 25 | will be used, included in the same order as they have been declared. 26 | 27 | ### Customize the Configuration Files 28 | 29 | In order to use the default configuration in your customization, you should copy the following files from the image: 30 | ``` 31 | docker create --name modsec quay.io/jcmoraisjr/modsecurity-spoa 32 | docker cp modsec:/etc/modsecurity . 33 | docker rm modsec 34 | ``` 35 | 36 | Download and customize the configuration files for either the [ModSecurity repository](https://github.com/SpiderLabs/ModSecurity/blob/v2/master/modsecurity.conf-recommended) or from [OWASP repository](https://github.com/SpiderLabs/owasp-modsecurity-crs/blob/v3.3/dev/crs-setup.conf.example). 37 | Use the copied files from the previous code section in your run command: 38 | ``` 39 | docker run -p 12345:12345 -v $PWD/modsecurity:/etc/modsecurity quay.io/jcmoraisjr/modsecurity-spoa -n 1 40 | ``` 41 | 42 | If you do not want to include the default configuration files and only use the configuration files (ex./ custom-config.conf) that you design, leave out the copied default configuration files from before in your run command: 43 | ``` 44 | docker run -p 12345:12345 -v $PWD/modsecurity:/etc/modsecurity quay.io/jcmoraisjr/modsecurity-spoa -n 1 -- /etc/modsecurity/custom-config.conf 45 | ``` 46 | 47 | ### Running without Config Files 48 | 49 | If no config-file is declared, the following will be used: 50 | 51 | * `/etc/modsecurity/modsecurity.conf`: ModSecurity recommended config, from ModSecurity [repository](https://github.com/SpiderLabs/ModSecurity/tree/v2/master) 52 | * Changes: `SecRuleEngine`, changed from `DetectionOnly` to `On` 53 | * `/etc/modsecurity/owasp-modsecurity-crs.conf`: Generic attack detection rules for ModSecurity, from OWASP ModSecurity CRS [repository](https://github.com/SpiderLabs/owasp-modsecurity-crs) 54 | * Changes: `SecDefaultAction`, `phase:1` and `phase:2`, changed from `log,auditlog,pass` to `log,noauditlog,deny,status:403` 55 | 56 | Options are: (from modsecurity agent -h) 57 | 58 | ``` 59 | -h Print this message 60 | -d Enable the debug mode 61 | -f ModSecurity configuration file 62 | -m Specify the maximum frame size (default : 16384) 63 | -p Specify the port to listen on (default : 12345) 64 | -n Specify the number of workers (default : 10) 65 | -c Enable the support of the specified capability 66 | -t