Learn Authelia - NGINX Integration
Episode 13 of 31

Learn Authelia - NGINX Integration

This episode turns NGINX into an authentication gateway: understanding auth_request as a verification sub-request, building an internal location for the Authelia endpoint, forwarding the X-Original-URL and Remote-User headers, and redirecting to the portal when a request isn't authenticated.

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

Introduction

In episode 12 you managed the user side — passwords, resets, up to disabling accounts. But all those features only matter if something is actually guarding the door. Starting this episode we enter a new phase: reverse proxy integration. And we start with the most used proxy in the self-hosted ecosystem: NGINX.

Authelia is never a replacement for the reverse proxy. It's a guard that asks first, lets through later. This role is carried out via the forward authentication pattern: before a request is forwarded to the application, the proxy sends a sub-request to Authelia asking "is this user allowed in?". A 2xx answer means proceed; 401 means deny.

An analogy: you work in a building whose door is guarded by a security officer (NGINX). Every time a guest arrives, the officer calls the receptionist (Authelia) to make sure the guest's name is registered. Only if the receptionist answers "safe" does the officer let them in. NGINX provides this feature through the auth_request directive.

Understanding auth_request

auth_request is an NGINX directive that sends a sub-request to an internal location. Its behavior is simple and firm:

  • If the sub-request replies with a 2xx status, the original request is forwarded to the backend.
  • If the sub-request replies with a 401 or 403, the original request is denied and NGINX handles the error — usually with a redirect to the Authelia portal.

Important: this sub-request doesn't alter the response status the browser receives, and NGINX marks the verification endpoint location as internal so it can't be accessed directly from the internet.

Building the Authelia Verification Endpoint

Authelia's verification endpoint for this pattern is /api/authz/auth-request. We hide it behind an internal location:

Internal Authelia endpoint location
set $upstream_authelia http://authelia:9091/api/authz/auth-request;
 
location /internal/authelia/authz {
    internal;
    proxy_pass $upstream_authelia;
 
    proxy_set_header X-Original-Method $request_method;
    proxy_set_header X-Original-URL $scheme://$host$request_uri;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header Content-Length "";
    proxy_set_header Connection "";
 
    proxy_pass_request_body off;
    proxy_http_version 1.1;
}

A few things to understand:

  • internal; — this location can only be called from within NGINX (by auth_request), not from the browser.
  • X-Original-URL and X-Original-Method — tell Authelia what the user is requesting. Authelia uses these values to evaluate access control rules (episode 6) and determine the redirect URL.
  • proxy_pass_request_body off; — the verification sub-request doesn't need to carry the original request body. This saves resources while also preventing request data from leaking to the verification endpoint.

The X-Forwarded-* headers at this location also matter: Authelia reads the scheme, host, and real IP to evaluate network-based policies. Without these headers, all requests look like they come from NGINX's internal address — and the IP-based rules we learned in episode 6 won't work.

Wiring auth_request to an Application

Now the server block for the protected application:

Protected application server block
server {
    listen 443 ssl;
    server_name app.example.com;
 
    include /etc/nginx/snippets/ssl.conf;
    set $upstream_app http://app:8080;
    set $upstream_authelia http://authelia:9091/api/authz/auth-request;
 
    location /internal/authelia/authz {
        internal;
        proxy_pass $upstream_authelia;
        proxy_set_header X-Original-Method $request_method;
        proxy_set_header X-Original-URL $scheme://$host$request_uri;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Content-Length "";
        proxy_set_header Connection "";
        proxy_pass_request_body off;
        proxy_http_version 1.1;
    }
 
    location / {
        proxy_pass $upstream_app;
        proxy_set_header Host $host;
 
        auth_request /internal/authelia/authz;
 
        auth_request_set $user $upstream_http_remote_user;
        auth_request_set $groups $upstream_http_remote_groups;
        auth_request_set $name $upstream_http_remote_name;
        auth_request_set $email $upstream_http_remote_email;
 
        proxy_set_header Remote-User $user;
        proxy_set_header Remote-Groups $groups;
        proxy_set_header Remote-Name $name;
        proxy_set_header Remote-Email $email;
 
        auth_request_set $redirection_url $upstream_http_location;
        error_page 401 =302 $redirection_url;
    }
}

Key parts of the block above:

  • auth_request /internal/authelia/authz; — the point connecting the application to the verification endpoint.
  • auth_request_set — stores Authelia's response headers into NGINX variables. Authelia returns the user identity on the Remote-User, Remote-Groups, Remote-Name, and Remote-Email headers.
  • error_page 401 =302 $redirection_url; — when Authelia denies, NGINX throws a 302 redirect to the URL Authelia sent via the Location header (the login portal with an rd parameter to return to the original page).

Tip

An important rule: proxy_set_header Host $host; must be present so the application behind it receives the correct domain name. Without it, some applications think the request comes from an internal address and refuse or misrender links.

Why Forward the Remote-* Headers?

Protected applications never see passwords or Authelia sessions — they only see headers. This is what's called Trusted Header SSO: applications trust the Remote-User value as the user identity because only the proxy can set it.

This has a big impact on applications that support this pattern: Grafana, Gitea, and many others can auto-login based on the Remote-User header without ever providing their own login form. You get true SSO — one login at the Authelia portal, then all applications consider the user known.

But this power is a responsibility: these headers can be forged if an application is reachable directly, bypassing NGINX. Make sure applications can only be accessed through the proxy, and make sure NGINX overwrites the Remote-* values from clients — with proxy_set_header, fake values from outside won't leak through to the backend.

Testing and Troubleshooting

Once the configuration is active, test with a calm flow:

  1. Try accessing the application in the browser without logging in. You should be redirected to the Authelia portal.
  2. Log in and complete MFA. You should be redirected back to the application.
  3. Check the Authelia logs to make sure the /api/authz/auth-request sub-request returns 200.
  4. If the application supports headers, verify that Remote-User is populated — for example with a simple app that displays all request headers.

The most common problems aren't in Authelia but in the headers: forgetting X-Original-URL makes the rd redirect wrong, forgetting proxy_set_header Host confuses the application, and without error_page 401 the browser just sees a blank 401 page instead of the portal. Before reloading, test the config syntax with nginx -t.

Important

Remember the pattern from episode 6: access control rules are evaluated against the original URL sent via X-Original-URL. If a rule denies, Authelia returns 401 and the redirect flow works. If you get a persistent 403 for a user who should be allowed in, check the domains in the rules — not the NGINX configuration.

Closing

This episode turned NGINX into an authentication gateway: understanding auth_request as a verification sub-request, building the internal location for the /api/authz/auth-request endpoint, forwarding the X-Original-URL and X-Forwarded-* headers, taking user identity from the Remote-* headers, and directing denied requests to the portal via error_page 401.

The pattern you mastered here — proxy asks, Authelia answers, proxy forwards — is the same language used throughout the rest of this phase. In episode 14, we apply that language to the proxy popular in the container world: Traefik, with the forwardAuth middleware and Docker-label-based configuration. See you there!

Learn Authelia - NGINX Integration | Learn Authelia