Authentik 2025.8 to 2026.8: The Breakable Parts Nobody Warns You About

· 9 min read · devops authentikssodockerportainerself-hostingoidcupgrade

I run authentik as the single sign-on gate in front of my self-hosted stack — email, dashboard, Synology apps, a remote-access outpost. For a long while it sat on 2025.8.3, and a year of releases piled up. This is the story of bringing it all the way to 2026.8.1 in one sitting, and everything that broke along the way — especially the one mistake that took Single Sign-On completely offline and made every internal app ask for a password again.

What is authentik, and why run it?

authentik is an open-source identity provider (IdP) — the thing that answers one question over and over: “who is this person, and are they allowed in?” Think of it as the front door to a whole building of apps. Instead of every app running its own username/password screen (and its own list of bugs, its own “forgot password” flow, its own 2FA), you make all of them ask authentik instead.

It speaks the protocols that matter for real deployments:

The practical benefits, in order of how much they actually matter:

  1. One login for everything. Log in once, move freely between every app. Users stop juggling a dozen passwords (and the support tickets that go with them).
  2. One place to lock things down. MFA, password policy, session limits, and account recovery live in authentik once — not re-implemented per app.
  3. One place to audit. Every login, on every app, for every user, in one log. That’s the difference between “we think nothing happened” and “we can prove it” when a question comes up.
  4. Self-contained and self-hosted. You own the data. No per-user SaaS fees that scale with your headcount, no lock-in — it’s AGPL, runs in Docker.

For a small business or a solo operator running twenty-something services (email, dashboards, NAS apps, internal tools), authentik is the difference between “every app has its own flimsy password” and “one hardened front door.”

Why upgrade at all

Version 2025.8.3 wasn’t broken. But it had fallen far enough behind that a stack of CVEs had landed in the releases after it, and I was getting ready to do per-application branding. authentik’s own policy is that you can’t jump major versions — it enforces a stepwise path. So the plan was:

2025.8.3 → 2025.10 → 2025.12 → 2026.2 → 2026.5 → 2026.8

Six hops, one at a time, with a migration and a health-check between each. Before touching anything, the one non-negotiable step: back up the database. authentik doesn’t support downgrades. If a migration half-runs, you’re restoring from dump, not rolling back an image tag.

sudo docker exec authentik-postgres pg_dump -U authentik authentik > authentik-backup.sql

Pitfall 1: Portainer is the source of truth, not the compose file

My first instinct was to edit the docker-compose.yml on disk and up it. Wrong. The stack is managed by Portainer (stack 143), which keeps the real compose and the real environment variables in its own store. The .env on disk was stale — its PG_PASS didn’t match what Portainer actually ran.

The correct update path is via the Portainer API, not the filesystem:

  1. Update the image tags in the compose content.
  2. docker pull the new images first (so the API call doesn’t time out mid-pull).
  3. docker stop + docker rm the running containers (fixed container_name will otherwise collide on redeploy).
  4. PUT /api/stacks/143?endpointId=2 with the new StackFileContent + Env.

I hit the colliding-container error, the pull-timeout error, and a network-attach problem where the rebuilt authentik-server only joined one of its two networks and couldn’t resolve postgres-server. Each one is a five-minute fix once you know what you’re looking at, but together they ate the better part of the evening.

Pitfall 2: the storage mount moved (2025.12)

Up to 2025.12, brand assets lived under /media/ and were served at /media/.... After 2025.12, the storage layout changed: files moved to a /data/media structure served under a new /files/media/public/<name>?token=... URL with a JWT signature. My containers were still mounting ./media:/media, so every logo, favicon, and background image 404’d the moment I crossed that version.

The fix is the documented migration:

mkdir -p data && mv media data/media

…and change the mounts to ./data:/data. The new file backend also refuses to work unless /data is an actual mount point — the earlier version of my fix used a symlink, which the backend’s is_mount() check rejected with No file management backend configured.

Pitfall 3: RBAC migration leaves a poisoned session table

2025.12 removed the old authentik_core.User_groups model in favor of the RBAC rework. The migration ran clean, but old sessions still held references to the deleted model. Result: the login page kept throwing LookupError: App 'authentik_core' doesn't have a 'User_groups' model.

Not django_session — that was empty. The real culprit was authentik’s own authentik_core_session table. Clearing it (and the other session tables) forced everyone to log in again and cleared the error:

TRUNCATE authentik_core_session;

One side effect worth knowing: this also invalidates every OIDC refresh token your apps were holding. They’ll bounce the user to a fresh login once, then recover. It’s a one-time annoyance, not a bug.

Pitfall 4: trusted proxies are now opt-in (2026.8)

2026.8 tightened the default forwarded-header handling. Previously authentik trusted all the private ranges; now it only trusts what you list explicitly. Behind a Synology reverse proxy forwarding to localhost, that means:

environment:
  AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS: 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1/128

Skip this and the proxy headers get rejected, which surfaces as auth failures that look like almost anything except what they actually are.

Pitfall 5: the one that broke SSO — authorization_flow vs authentication_flow

This was the expensive mistake, and it’s the kind of thing that’s easy to get wrong if you’re doing per-application branding.

A provider in authentik has two flow fields, and they mean different things:

# "Flow used for authentication when the associated application is
#  accessed by an un-authenticated user."      ← the LOGIN page
authentication_flow = models.ForeignKey(...)

# "Flow used when authorizing this provider."  ← the OAuth consent page
authorization_flow = models.ForeignKey(...)

I wanted different background images per app, so I created one flow per application and pointed the provider’s authorization_flow at it. Instant breakage: a user who was already authenticated still got walked through the full identification → password → MFA flow every time they opened another app. Single Sign-On was, effectively, gone.

The fix was a single UPDATE to put the two fields back where they belong:

UPDATE authentik_core_provider
SET authorization_flow_id = '1d85b1b1-...',   -- explicit-consent flow
    authentication_flow_id = 'aa##-per-app-flow'
WHERE ...;

And the branding itself — the per-app background and title — goes on the flow’s own background and title fields (writable since 2026.8), not on the brand’s domain match.

There was a second, related gotcha hiding behind this one. When I created the 18 per-app flows, they all landed with designation=authentication, and my auth.hoelee.com brand had flow_authentication set to NULL. authentik’s fallback when a brand has no explicit auth flow is to pick the first authentication flow by slug, alphabetically — which happened to be auth-agent, not default-authentication-flow. So the root login page started showing my agent’s background image. Setting the brand’s flow_authentication to the real default flow fixed it.

And one more that compounds with the RBAC pitfall: creating a flow sets its background but not its stagesstages is read-only on the flow object. An empty flow with no stage bindings is exactly what produced an infinite redirect loop on the login page earlier in the migration. Stage bindings are created separately:

POST /api/v3/flows/bindings/   # { target: "<flow pk>", stage: "<stage pk>", order: N }

What I’d do differently

The whole ordeal came down to three preventable patterns:

  1. Never guess at a field’s semantics — I treated authorization_flow as “the login flow” when the source-of-truth is the model definition, which spells out the difference in the field’s own docstring.
  2. Keep the API surface at arm’s length — API tokens kept expiring mid-run with every server restart, so I ended up doing the critical fixes directly against the database with psql. Reliable, but worth scripting before the cluster is on fire, not during.
  3. One breaking change per restart — I tried to reason about storage, RBAC, and proxy changes all in one go. Each would have been trivial if isolated and verified independently.

The result

authentik now runs 2026.8.1 — current, patched, all containers healthy — with 18 applications each showing their own background and title on the login page, and SSO working across every subdomain. Seventeen of the applications I use daily went from “asks me to log in again every time I switch apps” back to “log in once, move freely.”

The lesson worth carrying: a major-version upgrade on auth infrastructure is roughly 10% “change the image tag” and 90% “the data model, storage layout, and proxy rules all shifted underneath you.” Back up, go one version at a time, and when something behaves in a way that makes no sense, read the field name again before you reach for another config.


Want single sign-on for your business?

If you’re running several internal apps — a dashboard, an email server, a help desk, a file server — and your team is still logging into each one separately (or reusing the same password everywhere), I set up and maintain exactly this kind of infrastructure. I’ll deploy authentik, wire it to your existing apps, add MFA, and make sure “log in once” actually works — then leave you with a handover so you’re never locked in.

Reach me at [email protected] or WhatsApp +60 12-797 2969, or see what I do at hoelee.com.

Lee Teong Hoe

Full-stack developer & DevOps engineer. I build web apps, self-host infrastructure, and automate things — this blog is my living portfolio.