A partner or tenant admin could complete the dezky-operator OIDC flow and
land on the operator portal. The platform-api OperatorGuard already 403s
their data, but the login/UI layer had no authorization check at all — the
only gate was a manual Authentik UI setting with nothing in git enforcing it.
Close it with defense-in-depth across three independent layers:
1. IdP — operator-application.yaml blueprint binds an
ak_is_group_member("dezky-platform-admins") policy to the dezky-operator
app, so Authentik denies the OIDC flow for non-admins. The blueprint also
provisions the provider + application (state: created, so a fresh env is
built from code while an existing hand-made provider is left untouched).
Wire OPERATOR_OIDC_* into both authentik containers and mount the
blueprints dir on the worker (it applies blueprints, and previously lacked
the mount).
2. Operator app — require-platform-admin.global.ts requires platformAdmin and
routes a non-admin to not-authorized.vue, which triggers a full sign-out
(local + Authentik IdP) for shared-workstation safety. Fails open on a
transient /api/me error by design, to avoid mass-signout on platform-api
restarts; layers 1 and 3 contain the exposure.
3. platform-api — OperatorGuard (unchanged) requires dezky-operator audience
plus platformAdmin resolved from the DB on every request.
Also harden the partner surface: it shares the dezky-portal client with tenant
users so it has no IdP gate, and its /partner/* route middleware now fails
CLOSED when identity can't be confirmed.
Docs (AUTHENTIK-SETUP.md) and .env.example updated; the operator client secret
must be set before first boot since the blueprint now consumes it.
O.0 prep from OPERATOR-PLAN.md. Mechanical refactor before adding partner
management and operator-specific endpoints. The service now owns more than
just provisioning orchestration (it'll soon own partners, tenant lifecycle
actions, multi-audience JWT validation), so the name 'platform-api' reflects
its scope better.
What changed:
- Directory: services/provisioning/ -> services/platform-api/
- Package: @dezky/provisioning -> @dezky/platform-api
- Docker: container_name dezky-provisioning -> dezky-platform-api;
compose service key 'provisioning' -> 'platform-api'; volume
provisioning_node_modules -> platform_api_node_modules
- Portal: PROVISIONING_INTERNAL_URL env var -> PLATFORM_API_INTERNAL_URL,
default URL http://provisioning:3001 -> http://platform-api:3001 in all
three proxy routes (me.get.ts, tenants/index.post.ts, tenants/[slug]/
reconcile.post.ts), plus NUXT_API_BASE updated
- Health endpoint service identifier and main.ts log lines updated to
'dezky-platform-api'
- Docs swept: README, CLAUDE.md, SERVICES.md, AUTHENTIK-SETUP.md,
NEXT-STEPS.md, TROUBLESHOOTING.md, OPERATOR-PLAN.md, traefik/dynamic.yml
What deliberately stays:
- Internal module names ProvisioningService / ProvisioningModule (those
describe an orchestration sub-concern, not the service's purpose)
- Tenant.provisioningStatus / provisioningErrors field names (state
per integration, not service name)
- File services/platform-api/src/tenants/provisioning.service.ts
- 'Hetzner provisioning' references in production-prep docs (infrastructure
provisioning, unrelated)
Verified end-to-end after rename: /api/me returns 200 with profile + 2
tenants + subscription, /api/tenants/dezky/reconcile returns 200 with
Authentik integration still ok.
OPERATOR-PLAN.md O.0 checkboxes ticked.
Brings up Dezky's local development environment end-to-end:
Infrastructure (docker-compose):
- Traefik v3.7 reverse proxy with mkcert TLS (v3.2 couldn't speak Docker API 1.54)
- Postgres + Mongo + Redis with healthchecks and init script for per-service users
- Authentik 2025.10 (server + worker) as OIDC IdP
- Stalwart v0.16 mail server (image renamed from stalwartlabs/mail-server)
- OCIS 7.0 with PROXY_TLS=false and OCIS_CONFIG_DIR=/etc/ocis so init writes
where the server reads
- Collabora office, plus the portal + provisioning service stubs
- Docker network aliases on Traefik so containers resolve auth.dezky.local etc.
through the network (not host /etc/hosts)
- Docker socket mount parameterized for macOS Docker Desktop symlink path
Authentik provisioning (done via API after stack boot):
- ocis-provider (public client) + OCIS Files application
- dezky-portal provider (confidential) + Dezky Portal application
- Admin API token bound to akadmin manually since 2025.10's
AUTHENTIK_BOOTSTRAP_TOKEN env var doesn't auto-materialize a token row
Portal (apps/portal):
- Nuxt 3 with nuxt-oidc-auth 1.0.0-beta.11 against generic 'oidc' preset
- Global auth middleware; login at /auth/oidc/login redirects to Authentik
- Visual implementation of Claude Design 'Auth' canvas: AuthShell, NodeMark,
Auth* sub-components, design tokens as CSS custom properties
- Pages: auth/login, auth/expired, auth/disabled, index (post-login landing)
- mkcert root CA mounted into the portal so Node fetch trusts Authentik's
self-signed cert (NODE_EXTRA_CA_CERTS) — dev only
Docs:
- AUTHENTIK-SETUP.md updated with manual token bind + portal provider scripted
alternative
- NEXT-STEPS.md: Phase 1 and Phase 2 marked done with file locations and
dev-mode caveats
Dev-mode shortcuts that need to be revisited before prod:
- skipAccessTokenParsing on the OIDC config
- NODE_EXTRA_CA_CERTS mkcert mount
- Bootstrap password still the generated value in .env
- Authentik admin token (dezky-bootstrap-token) is non-expiring