Files
paskia/docs/proxy/haproxy.md
T
2026-08-10 21:35:46 +00:00

5.9 KiB

HAProxy Forward-Auth

/auth/api/forward · Proxy guides

HAProxy does not have a built-in forward-auth primitive, but the community haproxy-auth-request Lua script provides an auth-intercept action that works very similarly to Nginx's auth_request. It makes an internal HTTP request to Paskia and copies the response headers to the backend request.

Requirements

  • HAProxy 2.2 or newer (2.0+ may work but 2.2+ supports all features shown here).
  • Compiled with USE_LUA=1.
  • The haproxy-auth-request Lua script loaded.
  • The haproxy-lua-http dependency in the Lua path.

Overview

global
    lua-load /usr/share/haproxy/auth-request.lua

defaults
    mode http
    timeout connect 5s
    timeout client 30s
    timeout server 30s

# Backend that runs the Paskia auth check.
backend paskia_auth
    server paskia 127.0.0.1:4401

# Backend that runs the Paskia UI / WebSocket / API.
backend paskia_ui
    server paskia 127.0.0.1:4401

# Your protected application.
backend app_backend
    server app 127.0.0.1:3000

frontend app
    bind *:80

    # 1. Route /auth/ straight to Paskia, bypassing the auth check.
    acl is_auth path_beg /auth/
    use_backend paskia_ui if is_auth

    # 2. Add the headers Paskia logs / expects. These are then copied to the
    #    auth subrequest by auth-intercept.
    http-request set-header X-Forwarded-Method %[method]
    http-request set-header X-Forwarded-Uri %[url]

    # 3. Run the auth check. The parameters are:
    #    backend       path                                   method
    #    req-headers   success-headers                        failure-headers
    #
    #    - req-headers:   headers copied from client to Paskia.
    #    - success-headers: headers copied from Paskia response to backend request.
    #    - failure-headers: headers copied from Paskia response to client response.
    http-request lua.auth-intercept paskia_auth /auth/api/forward?perm=myapp:login GET Host,Cookie,Accept,X-Forwarded-Method,X-Forwarded-Uri Remote-* *

    # 4. If the subrequest was not successful, deny the request.
    http-request deny if ! { var(txn.auth_response_successful) -m bool }

    default_backend app_backend

What the configuration does

  1. Route /auth/ to Paskia — users must be able to reach the login/profile UI without already being authenticated. HAProxy will proxy WebSocket upgrade headers automatically for this backend when the client requests them.
  2. Set X-Forwarded-Method and X-Forwarded-Uri — HAProxy adds these headers to the incoming request so the Lua script can copy them to the auth subrequest. %[method] returns the HTTP method and %[url] returns the path and query string.
  3. lua.auth-intercept — sends a GET request to /auth/api/forward?perm=myapp:login on the paskia_auth backend. It copies the listed request headers (including the dynamic ones we just set) to the auth subrequest.
  4. On success (2xx) — copies every response header matching Remote-* from Paskia to the backend request. This overrides any client-supplied Remote-* headers, so the backend can trust them.
  5. On failure (4xx) — copies all response headers (*) to the client response and uses Paskia's response body, so the browser gets the login HTML or the JSON auth URL.
  6. Deny if auth failed — the final http-request deny rule is a safety net. In practice, auth-intercept with * as the failure-headers already terminates the transaction with Paskia's response.

Backend definition for the auth subrequest

The paskia_auth backend can be the same physical server as paskia_ui, but using a separate backend is convenient because the Lua script will use the first available server in the backend. The auth subrequest is a plain HTTP request, so no special WebSocket options are needed here.

backend paskia_auth
    server paskia 127.0.0.1:4401

Per-route permissions

You can run different auth checks for different paths by using HAProxy ACLs. Place the more specific rules before the generic one:

frontend app
    bind *:80

    acl is_auth path_beg /auth/
    use_backend paskia_ui if is_auth

    acl is_reports path_beg /reports
    http-request set-header X-Forwarded-Method %[method] if is_reports
    http-request set-header X-Forwarded-Uri %[url] if is_reports
    http-request lua.auth-intercept paskia_auth /auth/api/forward?perm=myapp:reports&max_age=5min GET Host,Cookie,Accept,X-Forwarded-Method,X-Forwarded-Uri Remote-* * if is_reports

    http-request set-header X-Forwarded-Method %[method]
    http-request set-header X-Forwarded-Uri %[url]
    http-request lua.auth-intercept paskia_auth /auth/api/forward?perm=myapp:login GET Host,Cookie,Accept,X-Forwarded-Method,X-Forwarded-Uri Remote-* *

    http-request deny if ! { var(txn.auth_response_successful) -m bool }
    default_backend app_backend

See perm argument and max_age argument for query parameter syntax.

Notes

  • The Lua script strips the request body from the auth subrequest, so Paskia's /auth/api/forward will only see the headers.
  • HAProxy variables are limited to alphanumeric characters, dots, and underscores, but the script already normalizes header names for you (e.g. Remote-User becomes req.auth_response_header.remote_user). The Remote-* glob pattern in the success-headers argument handles this automatically.
  • The auth backend must be reachable without TLS. If you need TLS to Paskia, run a local TCP forwarder or use HAProxy's Lua HTTP support directly (not covered by this script).
  • If you use a dedicated authentication host (--auth-host), route auth.example.com to the Paskia backend and start Paskia with --auth-host auth.example.com instead of exposing /auth/ on every site.