README quick start and configuration updated for init/serve split and paskia.kantadb; API.md server-config endpoints replaced with admin realm CRUD and the well-known webauthn endpoint; proxy guides point at the realm auth-host setting; oidc.md documents per-realm issuers; MultiSite.md rewritten from the implementation plan into documentation of the shipped mechanics, policy model, and design rationale.
127 lines
6.9 KiB
Markdown
127 lines
6.9 KiB
Markdown
# HAProxy Forward-Auth
|
|
|
|
[`/auth/api/forward`](../api/forward.md) · [Proxy guides](index.md)
|
|
|
|
HAProxy does not have a built-in forward-auth primitive, but the community [`haproxy-auth-request`](https://github.com/TimWolla/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`](https://github.com/TimWolla/haproxy-auth-request) Lua script loaded.
|
|
- The [`haproxy-lua-http`](https://github.com/haproxytech/haproxy-lua-http) dependency in the Lua path.
|
|
|
|
## Overview
|
|
|
|
```haproxy
|
|
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.
|
|
|
|
```haproxy
|
|
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:
|
|
|
|
```haproxy
|
|
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](../api/perm.md) and [max_age argument](../api/max-age.md) for query parameter syntax.
|
|
|
|
## Public access
|
|
|
|
For routes where anonymous visitors are allowed but logged-in users should still be identified, add `public=1` to the auth subrequest path:
|
|
|
|
```haproxy
|
|
http-request lua.auth-intercept paskia_auth /auth/api/forward?public=1 GET Host,Cookie,Accept,X-Forwarded-Method,X-Forwarded-Uri Remote-* *
|
|
# or with a permission the backend will check itself:
|
|
http-request lua.auth-intercept paskia_auth /auth/api/forward?public=1&perm=myapp:reports GET Host,Cookie,Accept,X-Forwarded-Method,X-Forwarded-Uri Remote-* *
|
|
```
|
|
|
|
The `Remote-*` success-headers glob already copies the `Remote-Public` header that marks each request as `anonymous`, `forbidden` or `authenticated`. With `public=1` the backend always runs and must check `Remote-Public` before treating the request as authorized; only reauth (`max_age`) still returns the 401 auth flow, so the `http-request deny` safety net simply never triggers on public routes. See [public access](../api/forward.md#public-access) and [trusted headers](../Headers.md#public-access).
|
|
|
|
## 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 (the realm's auth-host setting), route `auth.example.com` to the Paskia backend instead of exposing `/auth/` on every site.
|