diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index 3ca7f2b9..71b46f6c 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -119,6 +119,7 @@ FCr
fcrdns
fediverse
ffprobe
+fhdr
financials
finfos
Firecrawl
@@ -156,6 +157,7 @@ grw
gzw
Hashcash
hashrate
+hdr
headermap
healthcheck
healthz
@@ -165,6 +167,7 @@ Hetzner
hmc
homelab
hostable
+HSTS
htmlc
htmx
httpdebug
@@ -331,6 +334,8 @@ stackoverflow
startprecmd
stoppostcmd
storetest
+srcip
+strcmp
subgrid
subr
subrequest
@@ -355,6 +360,7 @@ Timpibot
TLog
traefik
trunc
+txn
uberspace
Unbreak
unbreakdocker
diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md
index 27644146..fac15263 100644
--- a/docs/docs/CHANGELOG.md
+++ b/docs/docs/CHANGELOG.md
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix honeypot and imprint links missing `BASE_PREFIX` when deployed behind a path prefix ([#1402](https://github.com/TecharoHQ/anubis/issues/1402))
- Add ANEXIA Sponsor logo to docs ([#1409](https://github.com/TecharoHQ/anubis/pull/1409))
- Improve idle performance in memory storage
+- Add HAProxy Configurations to Docs ([#1424](https://github.com/TecharoHQ/anubis/pull/1424))
diff --git a/docs/docs/admin/environments/haproxy.mdx b/docs/docs/admin/environments/haproxy.mdx
new file mode 100644
index 00000000..24b0bb34
--- /dev/null
+++ b/docs/docs/admin/environments/haproxy.mdx
@@ -0,0 +1,99 @@
+# HAProxy
+
+import CodeBlock from "@theme/CodeBlock";
+
+To use Anubis with HAProxy, you have two variants:
+ - simple - stick Anubis between HAProxy and your application backend (simple)
+ - perfect if you only have a single application in general
+ - advanced - force Anubis challenge by default and route to the application backend by HAProxy if the challenge is correct
+ - useful for complex setups
+ - routing can be done in HAProxy
+ - define ACLs in HAProxy for domains, paths etc which are required/excluded regarding Anubis
+ - HAProxy 3.0 recommended
+
+## Simple Variant
+
+```mermaid
+---
+title: HAProxy with simple config
+---
+flowchart LR
+ T(User Traffic)
+ HAProxy(HAProxy Port 80/443)
+ Anubis
+ Application
+
+ T --> HAProxy
+ HAProxy --> Anubis
+ Anubis --> |Happy Traffic| Application
+```
+
+Your Anubis env file configuration may look like this:
+
+import simpleAnubis from "!!raw-loader!./haproxy/simple-config.env";
+
+{simpleAnubis}
+
+The important part is that `TARGET` points to your actual application and if Anubis and HAProxy are on the same machine, a UNIX socket can be used.
+
+Your frontend and backend configuration of HAProxy may look like the following:
+
+import simpleHAProxy from "!!raw-loader!./haproxy/simple-haproxy.cfg";
+
+{simpleHAProxy}
+
+This simply enables SSL offloading, sets some useful and required headers and routes to Anubis directly.
+
+## Advanced Variant
+
+Due to the fact that HAProxy can decode JWT, we are able to verify the Anubis token directly in HAProxy and route the traffic to the specific backends ourselves.
+
+In this example are three applications behind one HAProxy frontend. Only App1 and App2 are secured via Anubis; App3 is open for everyone. The path `/excluded/path` can also be accessed by anyone.
+
+```mermaid
+---
+title: HAProxy with advanced config
+---
+
+flowchart LR
+ T(User Traffic)
+ HAProxy(HAProxy Port 80/443)
+ B1(App1)
+ B2(App2)
+ B3(App3)
+ Anubis
+
+ T --> HAProxy
+ HAProxy --> |Traffic for App1 and App2 without valid challenge| Anubis
+ HAProxy --> |app1.example.com | B1
+ HAProxy --> |app2.example.com| B2
+ HAProxy --> |app3.example.com| B3
+```
+
+:::note
+
+For an improved JWT decoding performance, it's recommended to use HAProxy version 3.0 or above.
+
+:::
+
+Your Anubis env file configuration may look like this:
+
+import advancedAnubis from "!!raw-loader!./haproxy/advanced-config.env";
+
+{advancedAnubis}
+
+It's important to use `HS512_SECRET` which HAProxy understands. Please replace `` with your own secret string (alphanumerical string with 128 characters recommended).
+
+You can set Anubis to force a challenge for every request using the following policy file:
+
+import advancedAnubisPolicy from "!!raw-loader!./haproxy/advanced-config-policy.yml";
+
+{advancedAnubisPolicy}
+
+The HAProxy config file may look like this:
+
+import advancedHAProxy from "!!raw-loader!./haproxy/advanced-haproxy.cfg";
+
+{advancedHAProxy}
+
+Please replace `` with the same secret from the Anubis config.
diff --git a/docs/docs/admin/environments/haproxy/advanced-config-policy.yml b/docs/docs/admin/environments/haproxy/advanced-config-policy.yml
new file mode 100644
index 00000000..93c06dc3
--- /dev/null
+++ b/docs/docs/admin/environments/haproxy/advanced-config-policy.yml
@@ -0,0 +1,15 @@
+# /etc/anubis/challenge-any.yml
+
+bots:
+ - name: any
+ action: CHALLENGE
+ user_agent_regex: .*
+
+status_codes:
+ CHALLENGE: 403
+ DENY: 403
+
+thresholds: []
+
+dnsbl: false
+
diff --git a/docs/docs/admin/environments/haproxy/advanced-config.env b/docs/docs/admin/environments/haproxy/advanced-config.env
new file mode 100644
index 00000000..712fdeeb
--- /dev/null
+++ b/docs/docs/admin/environments/haproxy/advanced-config.env
@@ -0,0 +1,11 @@
+# /etc/anubis/default.env
+
+BIND=/run/anubis/default.sock
+BIND_NETWORK=unix
+DIFFICULTY=4
+METRICS_BIND=:9090
+# target is irrelevant here, backend routing happens in HAProxy
+TARGET=http://0.0.0.0
+HS512_SECRET=
+COOKIE_DYNAMIC_DOMAIN=True
+POLICY_FNAME=/etc/anubis/challenge-any.yml
diff --git a/docs/docs/admin/environments/haproxy/advanced-haproxy.cfg b/docs/docs/admin/environments/haproxy/advanced-haproxy.cfg
new file mode 100644
index 00000000..1540067b
--- /dev/null
+++ b/docs/docs/admin/environments/haproxy/advanced-haproxy.cfg
@@ -0,0 +1,59 @@
+# /etc/haproxy/haproxy.cfg
+
+frontend FE-multiple-applications
+ mode http
+ bind :80
+ # ssl offloading on port 443 using a certificate from /etc/haproxy/ssl/ directory
+ bind :443 ssl crt /etc/haproxy/ssl/ alpn h2,http/1.1 ssl-min-ver TLSv1.2 no-tls-tickets
+
+ # set X-Real-IP header required for Anubis
+ http-request set-header X-Real-IP "%[src]"
+
+ # redirect HTTP to HTTPS
+ http-request redirect scheme https code 301 unless { ssl_fc }
+ # add HSTS header
+ http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+
+ # only force Anubis challenge for app1 and app2
+ acl acl_anubis_required hdr(host) -i "app1.example.com"
+ acl acl_anubis_required hdr(host) -i "app2.example.com"
+
+ # exclude Anubis for a specific path
+ acl acl_anubis_ignore path /excluded/path
+
+ # use Anubis if auth cookie not found
+ use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ req.cook(techaro.lol-anubis-auth) -m found }
+
+ # get payload of the JWT such as algorithm, expire time, restrictions
+ http-request set-var(txn.anubis_jwt_alg) req.cook(techaro.lol-anubis-auth),jwt_header_query('$.alg') if acl_anubis_required !acl_anubis_ignore
+ http-request set-var(txn.anubis_jwt_exp) cook(techaro.lol-anubis-auth),jwt_payload_query('$.exp','int') if acl_anubis_required !acl_anubis_ignore
+ http-request set-var(txn.anubis_jwt_res) cook(techaro.lol-anubis-auth),jwt_payload_query('$.restriction') if acl_anubis_required !acl_anubis_ignore
+ http-request set-var(txn.srcip) req.fhdr(X-Real-IP) if acl_anubis_required !acl_anubis_ignore
+ http-request set-var(txn.now) date() if acl_anubis_required !acl_anubis_ignore
+
+ # use Anubis if JWT has wrong algorithm, is expired, restrictions don't match or isn't signed with the correct key
+ use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ var(txn.anubis_jwt_alg) -m str HS512 }
+ use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore { var(txn.anubis_jwt_exp),sub(txn.now) -m int lt 0 }
+ use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ var(txn.srcip),digest(sha256),hex,lower,strcmp(txn.anubis_jwt_res) eq 0 }
+ use_backend BE-anubis if acl_anubis_required !acl_anubis_ignore !{ cook(techaro.lol-anubis-auth),jwt_verify(txn.anubis_jwt_alg,"") -m int 1 }
+
+ # custom routing in HAProxy
+ use_backend BE-app1 if { hdr(host) -i "app1.example.com" }
+ use_backend BE-app2 if { hdr(host) -i "app2.example.com" }
+ use_backend BE-app3 if { hdr(host) -i "app3.example.com" }
+
+backend BE-app1
+ mode http
+ server app1-server 127.0.0.1:3000
+
+backend BE-app2
+ mode http
+ server app2-server 127.0.0.1:4000
+
+backend BE-app3
+ mode http
+ server app3-server 127.0.0.1:5000
+
+BE-anubis
+ mode http
+ server anubis /run/anubis/default.sock
diff --git a/docs/docs/admin/environments/haproxy/simple-config.env b/docs/docs/admin/environments/haproxy/simple-config.env
new file mode 100644
index 00000000..02a3f667
--- /dev/null
+++ b/docs/docs/admin/environments/haproxy/simple-config.env
@@ -0,0 +1,10 @@
+# /etc/anubis/default.env
+
+BIND=/run/anubis/default.sock
+BIND_NETWORK=unix
+SOCKET_MODE=0666
+DIFFICULTY=4
+METRICS_BIND=:9090
+COOKIE_DYNAMIC_DOMAIN=true
+# address and port of the actual application
+TARGET=http://localhost:3000
diff --git a/docs/docs/admin/environments/haproxy/simple-haproxy.cfg b/docs/docs/admin/environments/haproxy/simple-haproxy.cfg
new file mode 100644
index 00000000..445d0788
--- /dev/null
+++ b/docs/docs/admin/environments/haproxy/simple-haproxy.cfg
@@ -0,0 +1,22 @@
+# /etc/haproxy/haproxy.cfg
+
+frontend FE-application
+ mode http
+ bind :80
+ # ssl offloading on port 443 using a certificate from /etc/haproxy/ssl/ directory
+ bind :443 ssl crt /etc/haproxy/ssl/ alpn h2,http/1.1 ssl-min-ver TLSv1.2 no-tls-tickets
+
+ # set X-Real-IP header required for Anubis
+ http-request set-header X-Real-IP "%[src]"
+
+ # redirect HTTP to HTTPS
+ http-request redirect scheme https code 301 unless { ssl_fc }
+ # add HSTS header
+ http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+
+ # route to Anubis backend by default
+ default_backend BE-anubis-application
+
+BE-anubis-application
+ mode http
+ server anubis /run/anubis/default.sock
diff --git a/docs/docs/admin/installation.mdx b/docs/docs/admin/installation.mdx
index 6ecaa5a1..7000afef 100644
--- a/docs/docs/admin/installation.mdx
+++ b/docs/docs/admin/installation.mdx
@@ -203,6 +203,7 @@ To get Anubis filtering your traffic, you need to make sure it's added to your H
- [Kubernetes](./environments/kubernetes.mdx)
- [Nginx](./environments/nginx.mdx)
- [Traefik](./environments/traefik.mdx)
+- [HAProxy](./environments/haproxy.mdx)
:::note
diff --git a/docs/docs/admin/native-install.mdx b/docs/docs/admin/native-install.mdx
index bdda5954..9abafa91 100644
--- a/docs/docs/admin/native-install.mdx
+++ b/docs/docs/admin/native-install.mdx
@@ -143,3 +143,4 @@ For more details on particular reverse proxies, see here:
- [Apache](./environments/apache.mdx)
- [Nginx](./environments/nginx.mdx)
+- [HAProxy](./environments/haproxy.mdx)