Learn Authelia - HAProxy Integration
Episode 16 of 31

Learn Authelia - HAProxy Integration

This episode integrates Authelia with HAProxy: understanding the need for the haproxy-auth-request Lua module, building the Authelia backend, calling the verification endpoint with acl, handling 401 status, and composing a haproxy.cfg that protects several services at once.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

NGINX has auth_request, Traefik has the forwardAuth middleware, Caddy has the forward_auth directive. HAProxy — honestly — has no native equivalent feature. But don't be fooled: HAProxy is a very capable reverse proxy and load balancer, and with a little help from a Lua module, it can run the same forward authentication pattern perfectly.

This episode is about that bridge: haproxy-auth-request, a Lua module by Tim Wolla that gives HAProxy the sub-request verification capability like NGINX's auth_request. An analogy: HAProxy is a very experienced old guard who was never trained to call the receptionist. The Lua module is the coach who teaches them.

Prerequisite: The Lua Module

Because this feature isn't built-in, you need to provide three things for HAProxy:

  1. HAProxy 2.2 or newer, compiled with Lua support (USE_LUA=1).
  2. haproxy-lua-http — an HTTP library for Lua.
  3. haproxy-auth-request — the module implementing the verification sub-request, along with the json library as its dependency.

All of them are loaded through the global configuration:

Linuxhaproxy.cfg — global, load Lua modules
global
    lua-prepend-path /usr/local/etc/haproxy/?/http.lua
    lua-load /usr/local/etc/haproxy/auth-request.lua
 
defaults
    mode http
    log global
    option httplog
    timeout connect 5s
    timeout client 30s
    timeout server 30s

lua-load loads the auth-request script, and lua-prepend-path tells HAProxy where to find the haproxy-lua-http dependency. These two lines are what open up forward auth capability in HAProxy.

Building the Authelia Backend

Next, define Authelia as an ordinary backend with a health check:

Linuxhaproxy.cfg — Authelia backend
backend be_authelia
    server authelia authelia:9091 check

This is no different from any other application backend. What makes the difference is how the frontend uses it for the verification sub-request.

Forwarded Headers: The Language Authelia Understands

Before the sub-request is sent, the frontend must complete the X-Forwarded-* headers. Authelia reads these headers to learn the original request's scheme, host, and URI — the material for access control rule evaluation. Without these headers, all requests look like they come from HAProxy's own address.

Linuxhaproxy.cfg — frontend, headers and ACLs
frontend fe_http
    bind *:443 ssl crt /etc/haproxy/example.com.pem
    option forwardfor
 
    http-request set-var(req.scheme) str(https) if { ssl_fc }
    http-request set-var(req.scheme) str(http) if !{ ssl_fc }
    http-request set-header X-Forwarded-Method %[method]
    http-request set-header X-Forwarded-Proto  %[var(req.scheme)]
    http-request set-header X-Forwarded-Host   %[req.hdr(Host)]
    http-request set-header X-Forwarded-URI    %[path]%[query]
 
    acl host-auth      hdr(Host) -i auth.example.com
    acl host-nextcloud hdr(Host) -i nextcloud.example.com
    acl protected      hdr(Host) -m reg -i ^(nextcloud|gitea|grafana)\.example\.com

option forwardfor adds X-Forwarded-For with the source IP, and the http-request set-header lines complete the rest. The protected ACL defines which hosts must pass Authelia verification — this is the replacement for "which services have middleware attached".

Running the Verification Sub-Request

The heart of the integration is in these lines — inside the frontend, before routing:

Linuxhaproxy.cfg — verification sub-request
http-request lua.auth-intercept be_authelia /api/authz/forward-auth HEAD * remote-user,remote-groups,remote-name,remote-email - if protected
 
http-request deny if protected !{ var(txn.auth_response_successful) -m bool } { var(txn.auth_response_code) -m int 403 }
 
http-request redirect location %[var(txn.auth_response_location)] if protected !{ var(txn.auth_response_successful) -m bool }

Let's break down these three lines — this is the heart of this episode:

  1. http-request lua.auth-intercept be_authelia /api/authz/forward-auth HEAD * ... — sends a HEAD sub-request to the be_authelia backend at the /api/authz/forward-auth endpoint, requesting the remote-user, remote-groups, remote-name, and remote-email headers in the response. The results are stored in the txn.auth_response_* variables.

  2. http-request deny — if verification fails and the response code is 403 (the deny policy in access control rules), block the request outright. This handles the case of an authenticated user without permission.

  3. http-request redirect — if verification isn't successful (not logged in), redirect to the Authelia portal using the URL Authelia sends on the response Location header — complete with the rd parameter to return to the original page.

Important

The order of these three lines must not be swapped. The deny for the 403 case must be evaluated before the redirect for the not-logged-in case — if reversed, unauthorized users would be directed to the portal and could get stuck in a confusing redirect loop.

Routing and Identity Headers

Finally, route the requests: the Authelia portal to the Authelia backend, protected applications to their own backends. The identity headers collected by the sub-request are automatically injected into the original request by the auth-request module when verification succeeds — this is what makes applications recognize Remote-User without a second login.

Linuxhaproxy.cfg — final routing
use_backend be_authelia if host-auth
use_backend be_nextcloud if host-nextcloud
 
backend be_nextcloud
    server nextcloud nextcloud:80

The haproxy-auth-request module also handles the session cookie: the cookie Authelia sends on the verification response is forwarded to the browser, so the Authelia session stays alive across all subdomains — the SSO prerequisite we've been building since episode 7.

A Complete haproxy.cfg Example

The entire configuration in one file:

Linuxhaproxy.cfg — complete configuration
global
    lua-prepend-path /usr/local/etc/haproxy/?/http.lua
    lua-load /usr/local/etc/haproxy/auth-request.lua
 
defaults
    mode http
    log global
    option httplog
    timeout connect 5s
    timeout client 30s
    timeout server 30s
 
backend be_authelia
    server authelia authelia:9091 check
 
backend be_nextcloud
    server nextcloud nextcloud:80
 
frontend fe_http
    bind *:443 ssl crt /etc/haproxy/example.com.pem
    option forwardfor
 
    http-request set-var(req.scheme) str(https) if { ssl_fc }
    http-request set-var(req.scheme) str(http) if !{ ssl_fc }
    http-request set-header X-Forwarded-Method %[method]
    http-request set-header X-Forwarded-Proto  %[var(req.scheme)]
    http-request set-header X-Forwarded-Host   %[req.hdr(Host)]
    http-request set-header X-Forwarded-URI    %[path]%[query]
 
    acl host-auth       hdr(Host) -i auth.example.com
    acl host-nextcloud  hdr(Host) -i nextcloud.example.com
    acl protected       hdr(Host) -m reg -i ^(nextcloud)\.example\.com
 
    http-request lua.auth-intercept be_authelia /api/authz/forward-auth HEAD * remote-user,remote-groups,remote-name,remote-email - if protected
 
    http-request deny if protected !{ var(txn.auth_response_successful) -m bool } { var(txn.auth_response_code) -m int 403 }
 
    http-request redirect location %[var(txn.auth_response_location)] if protected !{ var(txn.auth_response_successful) -m bool }
 
    use_backend be_authelia if host-auth
    use_backend be_nextcloud if host-nextcloud

Before loading it, validate the syntax with haproxy -c -f /etc/haproxy/haproxy.cfg — HAProxy will check the entire file without executing it.

Testing the Integration

  1. Validate the configuration, then reload HAProxy.
  2. Access nextcloud.example.com — you should be redirected to the auth.example.com portal.
  3. Log in and complete MFA — back to Nextcloud, and Remote-User is available to the application.
  4. Test the deny policy case: a user blocked by access control rules must receive an immediate 403, not a redirect.

Tip

HAProxy offers stick tables that can be used for rate limiting and making credential guessing harder — paired with Authelia's regulation (brute force protection), the two become complementary layers. Regulation will be dissected fully in episode 21.

Closing

This episode proved that a lack of native features isn't a barrier: with the haproxy-auth-request module, HAProxy runs the forward authentication pattern — building an Authelia backend with a health check, completing the X-Forwarded-* headers, calling /api/authz/forward-auth via lua.auth-intercept, distinguishing a 403 denial from a 401 redirect, and injecting the user identity into applications.

With NGINX, Traefik, Caddy, and HAProxy, you now have four ways to place Authelia at the gateway. This closes the reverse proxy integration phase. In episode 17, we unlock a far more modern Authelia capability: acting as an OpenID Connect provider — allowing applications to communicate with Authelia via the OIDC standard, not just headers. See you there!

Learn Authelia - HAProxy Integration | Learn Authelia