I used Microsoft Entra ID (Azure AD) for years. It handled single sign-on across my homelab and professional services. Worked fine. But it came with baggage: vendor lock-in, privacy questions, and that uncomfortable feeling of depending on Microsoft for something as basic as "who am I?"
Last week I switched to Authentik. Fully self-hosted. Open source. No more phoning home to Redmond. Just my own LDAP directory and OAuth2 flows running on my hardware.
Here's how it went.
Why I Left Azure AD
Three things pushed me over the edge.
Privacy. Every login flows through Microsoft's servers. They know when I authenticate, to what, from where. For a homelab? Overkill.
Cost. Azure AD is "free" until you need the good stuff. Conditional access, custom claims, advanced reporting. The bill grows fast.
Control. When Microsoft deprecates an API or changes policy, you adapt. With self-hosted identity, I make the rules.
Target Architecture
Simple goal: replace Azure AD with Authentik. Keep the same SSO experience.
┌─────────────────────────────────────────────────┐
│ Authentik │
│ (Identity Provider + LDAP) │
└──────────────┬──────────────┬──────────────┬────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Grafana │ │ GitLab │ │ Portainer │
│ (SSO) │ │ (OIDC) │ │ (OAuth) │
└───────────┘ └───────────┘ └───────────┘Setting Up Authentik
I deployed Authentik via Docker Compose on my Proxmox cluster. Pretty straightforward:
services:
authentik-server:
image: ghcr.io/goauthentik/server:latest
command: server
environment:
AUTHENTIK_SECRET_KEY: your-secret-key-here
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: your-db-password
ports:
- "9000:9000"
- "9443:9443"After setup, I configured my LDAP directory as the primary user source. All identities live on my server now. No cloud sync. No external calls.
Integrating Services
Grafana
Grafana was first. OAuth2 config in grafana.ini:
[auth.generic_oauth]
enabled = true
name = Authentik
client_id = your-client-id
client_secret = your-client-secret
auth_url = https://authentik.example.com/application/o/authorize/
token_url = https://authentik.example.com/application/o/token/
api_url = https://authentik.example.com/application/o/userinfo/
scopes = openid email profile
role_attribute_path = contains(groups[*], 'Admins') && 'GrafanaAdmin' || 'Viewer'The role_attribute_path maps Authentik groups to Grafana roles. "Admins" group members get full admin access automatically.
GitLab
GitLab uses OIDC. Goes in gitlab.rb:
gitlab_rails['omniauth_providers'] = [
{
name: "openid_connect",
label: "Authentik",
args: {
name: "openid_connect",
scope: ["openid", "profile", "email"],
response_type: "code",
issuer: "https://authentik.example.com/application/o/gitlab/",
client_auth_method: "query",
discovery: true,
uid_field: "preferred_username",
pkce: true,
client_options: {
identifier: "your-client-id",
secret: "your-client-secret",
redirect_uri: "https://gitlab.example.com/users/auth/openid_connect/callback"
}
}
}
]Watch out: add proper property mappings in Authentik. Without openid, email, and profile scopes mapped, you'll hit "Insufficient scope" errors. Learned that one the hard way.
Portainer
Portainer's OAuth setup is simpler. Settings → Authentication → OAuth → Custom:
- Authorization URL:
https://authentik.example.com/application/o/authorize/ - Token URL:
https://authentik.example.com/application/o/token/ - Resource URL:
https://authentik.example.com/application/o/userinfo/ - User Identifier:
preferred_username - Scopes:
openid profile email
Enable "Automatic user provisioning" so users get created on first login.
Or do it via API:
curl -X PUT "https://portainer.example.com/api/settings" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"AuthenticationMethod": 3,
"OAuthSettings": {
"ClientID": "your-client-id",
"ClientSecret": "your-client-secret",
"AuthorizationURI": "https://authentik.example.com/application/o/authorize/",
"AccessTokenURI": "https://authentik.example.com/application/o/token/",
"ResourceURI": "https://authentik.example.com/application/o/userinfo/",
"RedirectURI": "https://portainer.example.com/",
"Scopes": "openid profile email",
"OAuthAutoCreateUsers": true
}
}'Authentik Provider Config
For each service, I created an OAuth2 provider in Authentik:
- Authorization flow: Implicit consent (skip the "authorize?" popup for trusted apps)
- Redirect URIs: Regex matching for flexibility
- Property mappings: Always include openid, email, profile
- Signing key: Self-signed cert from Authentik
The API makes automation easy:
curl -X POST "https://authentik.example.com/api/v3/providers/oauth2/" \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{
"name": "MyService",
"authorization_flow": "implicit-consent-flow-uuid",
"invalidation_flow": "default-invalidation-flow-uuid",
"redirect_uris": [
{"matching_mode": "regex", "url": "https://myservice\\.example\\.com.*"}
],
"property_mappings": ["openid-uuid", "email-uuid", "profile-uuid"]
}'
The LDAP Backend
Real power comes from running your own LDAP directory. Authentik can act as an LDAP provider, or connect to existing OpenLDAP/FreeIPA.
I let Authentik manage users directly, then expose them via its built-in LDAP outpost. This gives me:
- Single source of truth for identities
- LDAP compatibility for legacy apps
- OAuth2/OIDC for modern services
- Full audit logs of every authentication
Results
After migrating three services:
| Metric | Azure AD | Authentik |
|---|---|---|
| Auth latency | ~200ms | ~50ms |
| Data location | Microsoft | My server |
| Monthly cost | "Free"* | $0 |
| Customization | Limited | Unlimited |
*Azure AD free tier has real limitations
What's Next
More services to migrate: Outline, Hoarder, n8n. Goal is complete independence from external identity providers.
Only external auth I'll keep is for services that genuinely require it. Everything internal runs through Authentik now.
Lessons Learned
-
Always add property mappings. "Insufficient scope" almost always means missing scope mapping.
-
Use implicit consent for internal apps. No reason to click "Authorize" every time for your own services.
-
Test with curl first. Before configuring any service, verify the OAuth flow works with simple curl commands.
-
Keep Azure AD as backup. During migration, I kept it configured as fallback. Once stable, removed it.
Self-hosted identity isn't for everyone. But if you value privacy, control, and owning your infrastructure, Authentik delivers. My homelab finally feels like it's actually mine.