Every self-hosted-SSO comparison on the internet starts from the same assumption: you need an identity provider, so pick one — Keycloak, Authentik, Zitadel — and stand it up. That assumption is right if you're issuing logins to tenants. It's the wrong frame entirely for a narrower, more common problem: putting a login wall in front of the operator's own internal tooling — a Cluster API fleet's admin dashboard, a Grafana instance, an internal status page — where the audience is a handful of people on the platform team, not an open signup flow. Standing up Keycloak for that is like buying a forklift to move a filing cabinet.
The tool that actually fits is a forward-auth gateway, not a full IdP, and it's a materially smaller thing to run. Below is a working config for both of the lightweight options that fit this job — Authelia and Pocket ID paired with OAuth2-Proxy — the real footprint numbers behind "lightweight," and the specific point where this setup stops being enough and a real IdP becomes the right call.
Forward-auth in one paragraph
A full IdP like Keycloak issues tokens, manages realms, and expects applications to speak OIDC or SAML to it directly. Forward-auth flips that: the reverse proxy in front of your app (Traefik, Caddy, NGINX) makes a sub-request to a tiny auth service on every incoming request. That service answers with a 200 (plus identity headers) or a 401. A 200 lets the request through to the backend unmodified; a 401 redirects the browser to a login page. The app behind the proxy never has to know an auth flow exists — it just sees a request with Remote-User or X-Auth-Request-User already set in the headers. No client libraries, no OIDC integration in the app itself, because the app was often never built to have one — that's exactly the situation for an internal Grafana dashboard, a Traefik dashboard, or a homegrown fleet-status page.
Two tools fill that gateway role at a fraction of a full IdP's weight: Authelia, which bundles the forward-auth check and the login portal into one binary, and Pocket ID, a passkey-only OIDC provider that needs OAuth2-Proxy in front of it to do the forward-auth part itself. Here's both, wired up for real.
Build 1: Authelia in front of Traefik
Authelia ships as a single ~20MB Go binary, configured entirely in files (no admin UI to click through), and answers forward-auth checks against its /api/authz/forward-auth endpoint. The Traefik side is one middleware block:
# traefik dynamic config
http:
middlewares:
authelia:
forwardAuth:
address: "http://authelia:9091/api/authz/forward-auth"
trustForwardHeader: true
maxResponseBodySize: 8192
authResponseHeaders:
- "Remote-User"
- "Remote-Groups"
- "Remote-Email"
- "Remote-Name"
routers:
fleet-dashboard:
rule: "Host(`fleet.internal.example.com`)"
service: fleet-dashboard-svc
middlewares: ["authelia"]Authelia itself needs an access-control policy telling it which routes require which factor:
# authelia configuration.yml (excerpt)
access_control:
default_policy: deny
rules:
- domain: "fleet.internal.example.com"
policy: two_factor
subject: "group:platform-team"That's the whole integration. Every request to fleet.internal.example.com bounces through Authelia's forward-auth endpoint; unauthenticated requests get redirected to Authelia's own login portal, which handles password + TOTP/WebAuthn out of the box. No separate database is required by default — Authelia's session store and user backend can run against flat files or SQLite for a deployment this size.
Build 2: Pocket ID + OAuth2-Proxy in front of Caddy
Pocket ID takes a different shape: it's a pure OIDC provider, not a forward-auth gateway. It's also deliberately narrower than Authelia in one specific way — passkeys are the only authentication method, no passwords at all, which is a reasonable constraint for a five-person platform team who already carry a YubiKey or use a password manager's passkey support. Because Pocket ID only speaks OIDC, it needs OAuth2-Proxy sitting in front of the protected app to do the actual forward-auth interception:
# oauth2-proxy config.yaml
provider: oidc
oidc_issuer_url: "https://id.internal.example.com"
client_id: "fleet-dashboard"
client_secret: "REPLACE_WITH_POCKET_ID_CLIENT_SECRET"
cookie_secret: "REPLACE_WITH_32_BYTE_BASE64"
cookie_secure: true
email_domains:
- "*"
set_xauthrequest: true
pass_user_headers: true
reverse_proxy: trueAnd the Caddy side, using the forward_auth directive:
fleet.internal.example.com {
handle {
forward_auth oauth2-proxy:4180 {
uri /oauth2/auth
copy_headers X-Auth-Request-User X-Auth-Request-Email
@error status 401
handle_response @error {
redir * /oauth2/sign_in?rd={scheme}://{host}{uri}
}
}
reverse_proxy fleet-dashboard:8080
}
handle /oauth2/* {
reverse_proxy oauth2-proxy:4180
}
}Pocket ID itself is a single container backed by SQLite by default — no Postgres, no Redis, no JVM. First-run setup takes under a minute and the whole stack (Pocket ID + OAuth2-Proxy) still lands well under what a full IdP needs at idle.
The gateway becomes a single point of failure — plan for that, not around it
Putting a login wall in front of the fleet dashboard also means that wall is now on the critical path to fixing anything if the fleet is unhealthy. Two operational details matter more than the auth protocol choice:
- Session persistence across restarts. Authelia's default file-based session backend loses every logged-in session on a container restart, forcing a re-login (with MFA) at the worst possible moment — mid-incident, when the admin dashboard is the thing you need open. Point Authelia at Redis for session storage in anything beyond a single-operator homelab, and do the same for OAuth2-Proxy's
session_storeif running Pocket ID. Both support it as a config flag, not a rewrite. - A break-glass path that doesn't depend on the gateway being healthy. If Authelia or OAuth2-Proxy itself is down — a bad config push, a database connection issue — and the fleet dashboard is unreachable behind it, that's the exact moment an operator needs direct access most. A documented fallback (SSH tunnel straight to the dashboard's internal port, bypassing the proxy) costs nothing to set up ahead of time and is the difference between a five-minute fix and being locked out of your own tooling during an incident.
Neither of these needs a heavier tool — Keycloak has the identical failure mode if its own database connection drops. They're just details that "add a login page" glosses over, and a forward-auth setup this cheap to run is worth doing right rather than fast.
The footprint numbers that justify skipping Keycloak
"Lightweight" is a claim worth pricing out rather than taking on faith:
| Authelia | Pocket ID + OAuth2-Proxy | Authentik | Keycloak | |
|---|---|---|---|---|
| Idle RAM | ~30MB | ~40–60MB combined | 200–400MB+ | ~1GB |
| Required datastore | none (file/SQLite) | SQLite (bundled) | PostgreSQL + Redis | PostgreSQL (or embedded) |
| Auth methods | password, TOTP, WebAuthn, push | passkey only | password, WebAuthn, social, SAML upstreams | password, WebAuthn, social, SAML, LDAP |
| Protocol surface | forward-auth native; OIDC provider role still in open beta | OIDC provider (needs a forward-auth companion) | full OIDC + SAML IdP, multi-application | full OIDC + SAML IdP, multi-realm |
| Admin surface | YAML config files, no click-through UI | minimal web UI | full web UI, RBAC, flows editor | full web UI, realms, federation |
| Fits | protecting the operator's own tooling behind one proxy | same, if passkey-only auth is acceptable | tenant-facing apps needing a real IdP without Keycloak's weight | multi-realm, LDAP federation, enterprise SSO requirements |
The gap isn't marginal. A Cluster API management node running Authelia or Pocket ID spends single-digit percentage points of a small VM's RAM on auth; the same node running Keycloak dedicates roughly a third of a 4GB box to an identity provider serving, in this use case, fewer than a dozen logins a day. That's the entire argument for picking the smaller tool — not that Keycloak is bad, but that its weight is priced for a job (tenant-facing, multi-realm identity) this use case doesn't have.
Where forward-auth stops being enough
The honest limit is real, and it isn't about scale — a two-person team and a two-hundred-person team both fit comfortably behind Authelia if the audience is "our own staff." It's about who's logging in and what they need to do once they're in:
- Self-service signup. Forward-auth gateways assume an operator provisions accounts by hand or via config file. The moment strangers need to create their own accounts, that's IdP territory.
- Per-tenant OIDC clients or SAML. If tenants — not the platform team — need to log into their own views (a per-tenant dashboard, a billing portal), each one is a separate OIDC/SAML relying party with its own claims and scopes. Authelia and Pocket ID aren't built to manage a directory of external client registrations at that scale; Keycloak and Authentik are.
- Delegated administration. A platform team managing its own dozen accounts in a YAML file is fine. A support team that needs to reset a tenant's password without platform-engineer involvement needs an admin UI with role-scoped delegation — the full-IdP feature set exists for exactly this.
- Federation and LDAP. Once there's an existing corporate directory to bind against, or a requirement to federate with an upstream enterprise IdP, that's squarely Keycloak/Authentik's job, not a forward-auth gateway's.
For a platform like this — Cluster API underneath, a Render-compatible deploy API, an admin dashboard that only the operating team touches — the two problems stay genuinely separate for a long time: protecting your own internal surfaces is a forward-auth job today, and it stays one until the day tenants themselves need to authenticate against the platform directly rather than through their own app's login. That's the day to open a Keycloak or Authentik ticket, not before.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.



