Ditching Azure AD: Self-Hosted Identity with Authentik

How I replaced Microsoft Entra ID with Authentik for self-hosted SSO across Grafana, GitLab, and Portainer. Full control, better latency, zero cloud dependencies.

4 min read
Ditching Azure AD: Self-Hosted Identity with Authentik

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"]
  }'
Self-hosted identity server with connected services

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

  1. Always add property mappings. "Insufficient scope" almost always means missing scope mapping.

  2. Use implicit consent for internal apps. No reason to click "Authorize" every time for your own services.

  3. Test with curl first. Before configuring any service, verify the OAuth flow works with simple curl commands.

  4. 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.