| name | add-tinyauth-app |
| description | Put an app behind tinyauth via Envoy Gateway ext_authz, for apps with no native OIDC support. Use for "protect X with tinyauth", "gate X behind tinyauth", "shared login for X", or "ext_authz for X". Scoped to the Artemis cluster. |
Skill: Add Tinyauth Protection to an App
Gate an app behind the shared tinyauth forward-auth service — Pocket-ID passkey login with an
lldap password fallback, single shared login/session across every protected app.
tinyauth is the standard gate for apps without native OIDC (and optionally in front of apps
that have it). components/envoy-oidc was removed 2026-07-15 — it never gained a consumer; apps
with real user models keep their native PocketIDOIDCClient for in-app identity (see
.agents/skills/add-oidc-app/SKILL.md), which tinyauth does not replace.
Read .agents/references/identity-stack.md in full before proceeding — it covers the lldap →
Pocket-ID → tinyauth chain, the ResourceSet grant mechanism, the session handoff, and every
gotcha found while building this.
Since 2026-08-24, TINYAUTH_AUTH_ACLS_POLICY is deny. An app with no TINYAUTH_APPS_*
entry is denied, not open. This inverts the old failure mode: forgetting Step 3 used to
leave an app wide open, and now locks you out of it instead. Fails closed, but do not mistake
the resulting 403 for a broken gate.
Step 1 — Add the component to the app's ks.yaml
components:
- ../../../../components/tinyauth
Also remove any existing oidcclient.yaml for that app and its pocket-id-operator dependency —
tinyauth is the OIDC client now, not the app itself.
Do not add a dependsOn on tinyauth. 244a2623c deliberately removed availability-gating
edges, bazarr -> tinyauth among them: the SecurityPolicy applies with or without a healthy
tinyauth, and Envoy programs it once the backend appears.
Step 2 — Grant the app's namespace access to the tinyauth Service
Gateway API requires a ReferenceGrant declared by the namespace that owns the target resource
(security, where the tinyauth Service lives), never by the consumer. It is generated by the
ResourceSet at kubernetes/apps/security/tinyauth/app/resourceset.yaml — check its
spec.inputs first; if the namespace is already listed (media is), skip this step entirely.
Otherwise add one line:
spec:
inputs:
- namespace: media
- namespace: <new-namespace>
Do not create a per-app or per-namespace file — that was tried and reverted twice. Note also that
inputsFrom on a ResourceSet is namespace-scoped (the CRD: "an input provider resource in
the same namespace as the ResourceSet"), so the external-endpoints provider pattern cannot be
used here to move this next to the app. Evaluated and rejected 2026-08-24.
Step 3 — Set the per-app ACL (three keys, all mandatory)
Add to security/tinyauth/app/helmrelease.yaml, container env:
env:
TINYAUTH_APPS_<APPNAME_UPPERCASE>_OAUTH_WHITELIST: "/.*/"
TINYAUTH_APPS_<APPNAME_UPPERCASE>_OAUTH_GROUPS: app_admin,app_ops
TINYAUTH_APPS_<APPNAME_UPPERCASE>_LDAP_GROUPS: app_admin,app_ops
| Key | Checked for | If unset, under policy: deny |
|---|
_OAUTH_WHITELIST | OAuth email, first | abstains → denies OAuth users |
_OAUTH_GROUPS | Pocket-ID session groups | abstains → denies |
_LDAP_GROUPS | lldap password session | abstains → denies |
Group checks are per login provider, which is why there are two group keys; the same underscored
name works for both, since Pocket-ID syncs its groups from lldap.
APPNAME must match the app's subdomain. ext_authz resolves the ACL from the request host
(bazarr.dcunha.io → bazarr), and auth.subdomainsEnabled defaults true, which is why
_CONFIG_DOMAIN is not needed.
- Group values use underscores (
app_admin), not the hyphenated PocketIDUserGroup CR name
(app-admin). See identity-stack.md § Groups.
"/.*/" is a match-all regex, not a placeholder. Keep it; the group lists are the real gate.
- Never remove the global
TINYAUTH_OAUTH_WHITELIST: "/.*/". Under policy: deny an unset
global whitelist abstains and denies every Pocket-ID login at the OAuth callback, before any
app ACL runs — including Immich's. See identity-stack.md § Gating apps.
Step 4 — Optional: hand the app an authenticated session
By default tinyauth only gates the route, so an app with its own login still shows it. If the app
supports trusted headers, add the ones it needs to headersToBackend in
components/tinyauth/securitypolicy.yaml — tinyauth sets Remote-User, Remote-Name,
Remote-Email, Remote-Groups and Remote-Sub, but none reach the backend unless named there.
If the app has no header-auth mode, tinyauth can mint basic-auth credentials instead:
TINYAUTH_APPS_<APP>_RESPONSE_BASICAUTH_USERNAME: <user>
TINYAUTH_APPS_<APP>_RESPONSE_BASICAUTH_PASSWORDFILE: /secrets/oidc/<app>-basic-password
Put the password in the app's own 1Password item in the artemis vault (check every vault
first — a duplicate title in a higher-priority vault silently shadows the real one), and pull it
into the tinyauth ExternalSecret with an extra dataFrom.extract. It lands under
/secrets/oidc/ because that whole Secret is mounted there.
Authorization is already in headersToBackend. Then turn the app's own auth on. Full worked
example (bazarr): identity-stack.md § Handing the app an authenticated session.
Step 5 — Test live before committing
Follow .agents/skills/modules/test-and-commit.md. apply-ks suspends root and the target for
you; apply both security tinyauth and the app, then:
curl -sI https://<app-hostname>/ 2>&1 | head -5
Dump the parsed config to confirm the ACL actually landed — tinyauth config exists as of v5.1.3
and is the fastest way to see what tinyauth really believes:
kubectl exec -n security <tinyauth-pod> -c app -- /tinyauth/tinyauth config
The dump is not redacted. It prints ldap.bindPassword and OIDC clientSecret in
plaintext. Always filter before pasting it anywhere: | grep -vi 'password\|secret'.
The only reliable way to confirm the group ACL works is a negative test against your own
account: temporarily set the group requirement to a nonexistent group, confirm your own
authenticated session is denied, then set it back and confirm access returns. A positive-only
test cannot distinguish "correctly allowed" from "no ACL enforced at all".
Gotchas
- ext_authz intercepts every external request on the route, not just browsers — mobile apps,
API tokens and webhooks break unless exempted via
TINYAUTH_APPS_<NAME>_PATH_ALLOW, or the
caller's IP via _IP_BYPASS. Internal svc.cluster.local traffic never touches the gateway and
is unaffected, which is why probes and app-to-app calls keep working.
- Label-based ACL discovery watches
networking.k8s.io/v1 Ingress only, never Gateway API
HTTPRoute. This cluster is Gateway API exclusively, so TINYAUTH_LABELPROVIDER: none is
permanent — mounting a ServiceAccount token would give the provider nothing to read. Always use
the explicit TINYAUTH_APPS_* env.
- The login flow shows one page.
TINYAUTH_OAUTH_AUTOREDIRECT: pocketid plus skipConsent on
the client sends users straight to the passkey prompt. Auto-redirect only fires when the request
carries redirect_uri or oidc_ticket, so auth.dcunha.io visited directly still renders
the lldap password form — that is the break-glass path, do not "fix" it.
- Pocket-ID names the gate, not the app. Its consent/authorize screen says Artemis SSO
because tinyauth is the only client it has. Bazarr and friends are invisible to Pocket-ID; that
is why per-app authorization has to live in
TINYAUTH_APPS_*.
- Sessions live in sqlite on an emptyDir (
/data/tinyauth.db) — every tinyauth pod restart
logs everyone out of every protected app. Deliberate for now: restart = cluster-wide session
revocation. Otherwise sessions last SESSIONEXPIRY (7 days), which is also the revocation lag
for a user disabled in lldap. LDAP_GROUPCACHETTL adds up to 15 min of lag on group changes.
- Runs as root. The image has no
USER directive, so runAsUser: 0 and
readOnlyRootFilesystem: false are required — skip the usual non-root review checks for this app.
- Password fallback is tinyauth↔lldap directly, not via Pocket-ID, which is passkey-only even
for LDAP-synced users. Don't configure
TINYAUTH_AUTH_USERS — that would be a second, unsynced
credential store.