Migrating Linkding to Native OIDC and Hardening the Cluster

Migrating Linkding from Authentik forward-auth to native OIDC, closing the last https-scheme gap on Grafana, and fixing three operational issues found during a full cluster audit.

Migrating Linkding to Native OIDC and Hardening the Cluster

Today's session was a cleanup and hardening pass across the homelab. The headline item: migrating Linkding from Authentik forward-auth to native OIDC. But the real value was in the operational audit that followed: three subtle issues across Ghost, Mealie, and Homepage that would have bitten me eventually.

The Grafana Ingress Gap

Quick win first. During last week's audit, I added the https-scheme middleware to all 15 public ingresses. Missed one: Grafana. Because Grafana's ingress is Helm-managed through kube-prometheus-stack, it lives in the HelmRelease values, not a standalone ingress.yaml. Easy to overlook when grep-ing through app directories.

The fix was a one-line change in infrastructure/monitoring/helmrelease.yaml:

grafana:
  ingress:
    annotations:
      traefik.ingress.kubernetes.io/router.middlewares: auth-https-scheme@kubernetescrd,auth-security-headers@kubernetescrd

All 16 public ingresses now have https-scheme. No more exceptions.

Linkding: Forward-Auth to Native OIDC

Linkding has built-in OIDC support via mozilla-django-oidc. There's no reason to route every request through Authentik's forward-auth proxy when the app can handle the OAuth flow directly. Native OIDC is faster (no extra hop to Authentik on every request), more reliable, and gives users a proper login page with an OIDC button.

Infrastructure Side (Authentik)

The migration touches three files in infrastructure/auth/:

  1. Blueprint: Replace the proxyprovider with an oauth2provider. Set the old provider to state: absent for cleanup. The new provider needs redirect_uris pointing to /oidc/callback/, a signing key, and the standard openid email profile scope mappings.
  2. HelmRelease: Add LINKDING_OIDC_CLIENT_ID and LINKDING_OIDC_CLIENT_SECRET env vars that pull from the SOPS-encrypted credentials secret.
  3. Outpost routes: Delete the Linkding IngressRoute. OIDC apps don't use the embedded outpost.

App Side (Linkding)

Linkding's OIDC uses a different env var convention than most apps:

env:
  - name: LD_ENABLE_OIDC
    value: "true"
  - name: OIDC_OP_AUTHORIZATION_ENDPOINT
    value: "https://auth.naviauxlab.com/application/o/authorize/"
  - name: OIDC_OP_TOKEN_ENDPOINT
    value: "https://auth.naviauxlab.com/application/o/token/"
  - name: OIDC_OP_USER_ENDPOINT
    value: "https://auth.naviauxlab.com/application/o/userinfo/"
  - name: OIDC_OP_JWKS_ENDPOINT
    value: "https://auth.naviauxlab.com/application/o/linkding/jwks/"
  - name: LD_CSRF_TRUSTED_ORIGINS
    value: "https://bookmarks.naviauxlab.com"
envFrom:
  - secretRef:
      name: linkding-oidc  # OIDC_RP_CLIENT_ID + OIDC_RP_CLIENT_SECRET

Client credentials use OIDC_RP_* (Relying Party). Provider endpoints use OIDC_OP_* (OpenID Provider). The JWKS endpoint is slug-specific: /application/o/linkding/jwks/, not a generic path. And LD_CSRF_TRUSTED_ORIGINS is required or the callback POST fails Django's CSRF check.

Remove authentik-forwardauth from the ingress middleware chain. Keep https-scheme and security-headers.

What Didn't Work

I tried adding LD_DISABLE_LOGIN_FORM=True to hide the username/password form and show only the OIDC button. Turns out that env var doesn't exist. The PR that proposed it was rejected by the maintainer. There's no auto-redirect to OIDC either. Users see a standard login page with a "Login with OIDC" link. Not ideal, but functional.

Startup Time Gotcha

OIDC dependencies added enough startup time that the liveness probe (set at 45 seconds from a previous fix) started failing. Linkding with OIDC takes about 50 seconds to boot: DB migrations, Django initialization, then OIDC library loading. Bumped initialDelaySeconds to 60 seconds.

The Operational Audit

With the OIDC migration done, I ran a full operational audit: pod health, resource usage, warning events, Longhorn volumes, Flux status. Three findings worth fixing.

Ghost Probe Redirect Spam

Ghost was generating a ProbeWarning event every 5 seconds. The readiness probe hits /ghost/api/admin/site/ via HTTP, but Ghost is configured with url: https://steven.naviauxlab.com, so it returns a 301 redirect to HTTPS. Kubernetes 1.28+ treats 3xx responses as probe success but logs a warning for each one.

Fix: add X-Forwarded-Proto: https as an HTTP header on the probe. Ghost sees the header, believes the connection is already HTTPS, and responds normally instead of redirecting.

livenessProbe:
  httpGet:
    path: /ghost/api/admin/site/
    port: 2368
    httpHeaders:
      - name: X-Forwarded-Proto
        value: https

Mealie Memory Pressure

Mealie was running at 387Mi against a 448Mi limit: 86% utilization with 61Mi of headroom. Mealie is a Python/FastAPI app with a high memory baseline; one slightly larger recipe import could push it over. Bumped the limit to 512Mi for a comfortable ~75% utilization ratio.

Homepage Liveness Timeout

Homepage had a single transient liveness probe timeout. The default timeoutSeconds: 1 is tight for a Node.js app that occasionally pauses for garbage collection or widget fetches. Bumped to 3 seconds. Not exciting, but it prevents an unnecessary pod restart during a momentary slow response.

Current SSO Inventory

With Linkding migrated, the homelab SSO breakdown is now:

  • Forward-auth (7 apps): Homepage, IT-Tools, Stirling PDF, Life Hub, Uptime Kuma, Navidrome, Audiobookshelf
  • OIDC (4 apps): Grafana, Miniflux, Mealie, Linkding
  • None (4 apps): Ghost, Actual Budget, Vaultwarden, ntfy

The trend is clear: anything with native OIDC support should use it. Forward-auth is the fallback for apps that don't speak OAuth. Next candidates for migration would be if Navidrome or Audiobookshelf ever add OIDC support upstream.

Takeaways

The pattern I keep seeing: the initial deployment gets you to "working," but the second pass (the audit, the hardening, the migration to native protocols) is where reliability comes from. Forward-auth worked fine for Linkding. OIDC works better. The Ghost probe warning wasn't breaking anything. Fixing it means cleaner event streams and fewer false signals. None of these were urgent. All of them matter over time.