When the number of services on my homelab server reached double digits, managing separate passwords for each application and keeping track of which ones had two-factor authentication (2FA) enabled became a massive operational burden. Open tabs in the browser, user tables kept in separate databases for each service, and the risk of passwords traveling in the clear—even on a local network—forced me to set up a single, centralized authentication point. In this post, I will explain how to build a Single Sign-On (SSO) in your Homelab architecture from scratch using Authentik, maximizing security for both your local and publicly exposed services.
By the time you complete this guide, you will have a single authentication wall standing in front of all your services running on Docker. Once users log in, they will be able to access all internal services within their authorization scope (Portainer, Grafana, Home Assistant, etc.) without entering passwords, using just a single OTP (One-Time Password) or a hardware key.
Why Do You Need Single Sign-On (SSO) in Your Homelab?
Many services running on a local network either come without authentication by default or offer built-in login mechanisms that are vulnerable to brute-force attacks. When I realized that even a simple monitoring interface running on my own server could expose my entire network topology if leaked to the internet, I knew that putting a security layer in front of every service was a must. Setting up a Single Sign-On (SSO) in your Homelab doesn’t just save you the hassle of memorizing passwords; it also standardizes logging and monitoring by routing all network traffic through a single gateway.
Traditionally, defining strong passwords for each application individually is not a sustainable method. When an application update is delayed or a zero-day vulnerability emerges, that service’s login screen becomes a direct target for attackers. Thanks to the SSO architecture, we completely disable (or bypass) the application’s own login mechanism and hand over the authentication load entirely to a tool dedicated to this job, which is constantly updated and adheres to security standards (OAuth2, OIDC, SAML).
graph TD; User["User"] --> Proxy["Nginx Reverse Proxy"] Proxy -->|Auth Check| Authentik["Authentik SSO (2FA)"] Authentik -->|Approval/Token| Proxy Proxy -->|Redirect| ServiceA["Portainer (Service A)"] Proxy -->|Redirect| ServiceB["Grafana (Service B)"]
What is Authentik and Why Did I Choose It Over Other Alternatives?
There are popular open-source authentication solutions on the market like Authelia, Keycloak, and Authentik. While Keycloak is the enterprise standard, its resource consumption (due to being JVM-based) and its complex interface at a homelab scale make it a bit heavy for those of us who want maximum performance with minimal resources. Authelia, on the other hand, is an extremely lightweight and successful alternative, but the lack of a visual management interface and having to do all configuration via YAML files makes the process sluggish when dynamically adding new services or managing users.
Authentik brings together the best of both worlds. Its modern architecture written in Python and Go offers a rich web interface while allowing you to define highly flexible policies and flows in the background. Thanks to its Outpost architecture, it natively supports reverse proxy integration (Forward Auth) and can talk seamlessly with directory services like LDAP/Active Directory on your local network.
Authentik Installation: Spinning It Up with Docker Compose
The most stable way to run Authentik is by using Docker Compose. Before starting the installation, make sure Docker and the Docker Compose plugin are installed on your server. Authentik consists of several components: a web server, a background worker, Redis for caching, and a PostgreSQL database for data storage.
The docker-compose.yml file below defines a network structure optimized for a homelab environment, featuring local volume shares and isolation against external attacks.
version: '3.8'
services:
postgresql:
image: postgres:16-alpine
container_name: authentik-postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_DB: ${PG_DB:-authentik}
env_file:
- .env
redis:
image: redis:7-alpine
container_name: authentik-redis
command: --save 60 1 --loglevel warning
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- redisdata:/data
server:
image: ghcr.io/goauthentik/server:2026.5.1
container_name: authentik-server
restart: unless-stopped
command: start
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
volumes:
- ./media:/media
- ./custom-templates:/templates
env_file:
- .env
ports:
- "${AUTHENTIK_PORT_HTTP:-9000}:9000"
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
worker:
image: ghcr.io/goauthentik/server:2026.5.1
container_name: authentik-worker
restart: unless-stopped
command: start_worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
env_file:
- .env
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
volumes:
pgdata:
driver: local
redisdata:
driver: local
Before running this configuration, we need to create a .env file in the same directory. This file will contain the encryption keys and database credentials. You can use the openssl rand -base64 36 command in your terminal to generate the AUTHENTIK_SECRET_KEY value.
PG_PASS=Generate_A_Secure_Password_Here
PG_USER=authentik
PG_DB=authentik
AUTHENTIK_PORT_HTTP=9000
AUTHENTIK_PORT_HTTPS=9443
AUTHENTIK_SECRET_KEY=your_secure_key_generated_with_openssl
AUTHENTIK_ERROR_REPORTING__ENABLED=false
You can spin up the entire stack using the docker compose up -d command. It may take about a minute for the services to become fully ready. Navigate to http://<your-server-ip-address>:9000/if/flow/initial-setup/ in your browser to create your initial administrator account.
Reverse Proxy Integration: Nginx Proxy Manager and Authentik Forward Auth
The heart of the SSO system is the reverse proxy layer, which intercepts all incoming requests and asks Authentik, “Is this user logged in?” In my homelab environment, I use Nginx Proxy Manager (NPM) or direct raw Nginx for its flexibility and ease of management. To integrate the proxy with Authentik, we must choose between the Forward Auth or Proxy Provider methods.
The Forward Auth method works on the principle that Nginx sends an HTTP subrequest to Authentik’s lightweight control endpoint (Outpost) on every request. If the user is authenticated, Nginx allows the request to proceed to the actual destination; if not, it redirects the user to the Authentik login page.
Below is an example of a custom_locations or location block you can use for any service (e.g., Portainer) you want to protect on Nginx:
# Authentik Forward Auth Integration
location / {
# The local service port where the user actually wants to go
proxy_pass http://192.168.1.50:9000;
# Authentik Outpost redirection
auth_request /outpost.goauthentik.io/auth/nginx;
error_page 401 = @goauthentik_search_redirection;
# Pass user information returned from the auth request to the service as headers
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
auth_request_set $authentik_username $upstream_http_x_authentik_username;
proxy_set_header X-Remote-User $authentik_username;
}
location = /outpost.goauthentik.io/auth/nginx {
proxy_pass http://192.168.1.100:9000/outpost.goauthentik.io/auth/nginx;
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Save bandwidth by not sending the body in subrequests
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
location @goauthentik_search_redirection {
return 302 $scheme://$http_host/outpost.goauthentik.io/start?rd=$request_uri;
}
Putting Applications Behind SSO: Step-by-Step Integration Process
After spinning up the system and configuring the proxy, we need to define our applications in the Authentik panel. In the Authentik world, every protected service matches an Application and a Provider that defines how to access this application.
| Term | Description | Homelab Example |
|---|---|---|
| Application | The application card and name that the user will see | Portainer, Grafana, Home Assistant |
| Provider | The authentication protocol and authorization type | OIDC, SAML, Forward Auth (Single App) |
| Outpost | The service that processes proxy requests and validates tokens | Authentik Embedded Outpost |
The steps to add an application are as follows:
- Creating a Provider: In the Authentik Admin interface, navigate to
Applications -> Providers. Click the “Create” button and select “Proxy Provider”. - Configuring Settings: Enter the Provider name. Select the default
default-provider-authorization-implicit-consentflow as the “Authorization flow”. Enter the full URL where the application will be accessed from the outside in the “External host” field (e.g.,https://portainer.local.lan). - Defining the Application: Go to the
Applications -> Applicationsmenu. Create a new application using the “Create with Provider” option. Bind the Provider you just created to this application. - Updating the Outpost: Edit the “authentik Embedded Outpost” entry under the
Applications -> Outpoststab and add your newly created application to this outpost’s “Applications” list. If this step is skipped, proxy requests will return authorization errors (500 or 401).
Multi-Factor Authentication (MFA/2FA) Configuration and Security Hardening
If you really want to take your homelab’s security to the next level, just a username and password are not enough. Authentik natively supports TOTP (Google Authenticator, Aegis, Bitwarden) and WebAuthn (Yubikey, Passkey, TouchID). I enforce TOTP usage for all my users (including family members).
To configure this, we will use the “Flows” mechanism in Authentik. Go to the Flows and Stages -> Flows tab. Find the default-authentication-flow flow. This flow defines the stages a user goes through when logging into the system.
To enable mandatory MFA, follow these steps:
- Create a new “Authenticator Validation Stage” from
Flows and Stages -> Stages. Select which 2FA methods you want to allow here (TOTP, WebAuthn). - Go into the
default-authentication-flowflow and click on the “Bindings” tab. - Add the “Authenticator Validation Stage” you just created right after the username and password stages.
- Users will automatically be greeted with a QR code screen on their first login and will not be able to access the system without completing the 2FA setup.
Common Issues and Solutions in Homelab SSO Infrastructure
In my own deployments and based on feedback from others, I’ve seen that SSL certificates and redirect loops are the most time-consuming issues during SSO integration. If your browser is giving a “Too many redirects” error, make sure the X-Forwarded-Proto header is set correctly on Nginx.
# Add this to Nginx proxy blocks to prevent redirect loops
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
Another common issue is Docker containers not being able to talk to each other. If your Authentik server and Nginx Proxy Manager are on different Docker networks, Forward Auth requests will result in a 502 Bad Gateway error. To solve this, you need to connect both services to a common Docker network (e.g., proxy-net):
# Create a shared network
docker network create proxy-net
Then, define this network as external in the docker-compose.yml files of both services and add it under the services. This way, the services can access each other securely using container names instead of IP addresses.
Conclusion
With this guide, you have put complex password management behind you in your homelab environment and gathered all your services behind a single secure gateway. This Single Sign-On (SSO) in your Homelab setup that we built with Authentik will standardize your security template for every new service you add in the future. Now, you can securely manage your entire local network with just a single strong password and a physical security key (or TOTP app).
As a next step, you can try creating user groups in Authentik to grant your family members access to only specific services (e.g., only Jellyfin or Plex).