feat(gotify): put the Gotify UI behind ZITADEL single sign-on

Gotify has no OIDC support, so it cannot validate a ZITADEL token itself. Put
oauth2-proxy in front of it and let nginx ask the proxy for every request.

The Gotify REST API keeps its own client-token check. /message, /application,
/client, /user, /version, /stream and /health are skipped by the proxy, so
Forgejo webhooks, the harness, poimen-sender and the Android client keep working
without a browser session. Only the web UI requires SSO.

The /oauth2 path needs its own ingress: the auth_request annotation would
otherwise also guard /oauth2/start, and the login entry point would demand a
login first.

The client id and secret come from project role provisioning:
`zitadel-provision.py apply` now creates the `gotify` OIDC app, and the secret is
SOPS-encrypted under k8s/argocd/secrets.

Gotify's own login remains behind the proxy. ZITADEL decides who reaches the
service, Gotify decides what that person may do inside it.
This commit was merged in pull request #72.
This commit is contained in:
2026-10-27 12:06:00 -07:00
parent cc7c30ee20
commit 03cebb0aeb
8 changed files with 178 additions and 0 deletions
+19
View File
@@ -88,6 +88,20 @@ ROLES = {
# permission string, so consumer manifests did not change.
"llm:inference": "LLM inference access (api-gw)",
"llm-inference": "LLM inference access (dash alias)",
# Per-service capabilities. The gateway derives the required capability from
# the verb: GET/HEAD/OPTIONS -> "<service>:read", anything else ->
# "<service>:write" (internal/serviceadapter/router.go:142). The role key
# must match that string exactly, colon included.
"memory:read": "Poimen memory read (api-gw)",
"memory:write": "Poimen memory write (api-gw)",
"workflow:read": "Temporal workflow read (api-gw)",
"workflow:execute": "Temporal workflow execute (api-gw)",
"s3:read": "Object store read (api-gw)",
"s3:write": "Object store write (api-gw)",
"iam:read": "Identity read (api-gw)",
"iam:write": "Identity write (api-gw)",
"sqs:read": "Queue read (api-gw)",
"sqs:write": "Queue write (api-gw)",
}
_WEB = {"app_type_id": "OIDC_APP_TYPE_WEB", "auth_method_id": "OIDC_AUTH_METHOD_TYPE_BASIC"}
@@ -108,6 +122,11 @@ OIDC_APPS = {
"https://forgejo.riotpiao.com/user/oauth2/zitadel/callback",
"http://localhost:3000/user/oauth2/authentik/callback",
"https://forgejo.riotpiao.com/user/oauth2/authentik/callback"]},
# Gotify has no native OIDC, so oauth2-proxy sits in front of it and this app
# is the proxy's client. The callback path is the proxy's, not Gotify's.
"gotify": {**_WEB, "redirect_uris": [
"http://localhost:4180/oauth2/callback",
"https://gotify.riotpiao.com/oauth2/callback"]},
"grafana": {**_WEB, "redirect_uris": [
"http://localhost:3000/login/generic_oauth",
"https://grafana.riotpiao.com/login/generic_oauth"]},