Zitadel¶
Zitadel is the OIDC identity provider for the portal, the RADIUS SPA and the Monsieur Wifi app. One instance serves every environment:
- Hostname:
auth.digilan-edge.com - Runtime: Scaleway serverless container (
rg.fr-par.scw.cloud/digilan/zitadel) with its own managed PostgreSQL instance (zitadel, backups enabled), separate from the shared platform database. - Source:
infra/terraform/environments/shared/zitadel.tf,security.tf,email.tf(SMTP through Scaleway Transactional Email).
A person has one Zitadel account whichever environment they sign in to.
Projects and applications¶
Each environment has its own Zitadel project, digilan-<env>, holding that
environment's applications. scripts/zitadel-apps.sh creates and reconciles
them on tofu apply (it runs when the env's URLs or the script change) and
records the ids in the environment directory.
| Identifier | Used by |
|---|---|
Project id (zitadel_project_id output) |
NEXUS_ZITADEL_PROJECT_ID, VITE_ZITADEL_PROJECT_ID, radius Zitadel__Audience |
Portal client id (portal_client_id) |
VITE_ZITADEL_CLIENT_ID, supplied to portal CI as DEV_ / STG_ / PRD_ZITADEL_CLIENT_ID |
Monsieur Wifi client (mrwifi_client_id, mrwifi_client_secret) |
Monsieur Wifi web app |
Radius client id (radius_client_id) |
RADIUS SPA |
Nexus validates the token audience against its own environment's project id, so a token issued for one environment is refused by every other environment's nexus.
The nexus service user¶
environments/shared creates one machine user, nexus-service, with
ORG_USER_MANAGER (scripts/zitadel-nexus-service.sh; output
nexus_service_pat). Each environment passes its PAT in
nexus_zitadel_service_token, which becomes NEXUS_ZITADEL_SERVICE_TOKEN on
nexus and Zitadel__ServiceToken on the radius API. Nexus uses it to create
invited users, resend invites, resolve an exact email to an account, and look up
names and emails.
Login policy¶
The live login policy (instance and the one org):
- Registration is closed.
allowRegisterandallowExternalIdparefalse. The login page offers no sign-up.zitadel_registration_closedinsecurity.tfenforces this on the instance policy and on every org with a policy of its own, changing only those two fields. - A second factor is optional.
forceMfaisfalse. The enabled factors are OTP (authenticator app) and U2F (security keys); passwordless login with passkeys is allowed.
security.tf does not match the live MFA setting
null_resource.zitadel_login_policy sends a full policy with
forceMfa: true. It runs only when its triggers change, so the live policy
still has MFA optional. An apply that re-runs that resource would start
forcing MFA for every user.
User provisioning¶
Accounts are created only by an invite from the portal: Team >
Invite Member calls PortalUsersService.Invite, and nexus creates the user
through the Zitadel Management API (or resends the invite if the address already
has an account). The person sets a password from the invite email; adding a second factor is
optional. Who may invite is described in Authz.
Signing in creates nothing in nexus. A new account has no organization and no role until it is granted one; until then the portal shows No organization yet.
Portal OIDC¶
The portal uses oidc-client-ts with the authorization code flow and PKCE.
Redirect URI: https://<portal-host>/callback. Tokens are renewed silently
before they expire.
Nexus token validation¶
At startup nexus loads the JWKS from ${NEXUS_ZITADEL_ISSUER}/oauth/v2/keys.
Every portal RPC passes through PortalAuthInterceptor, which checks:
- the signature against the JWKS,
issequalsNEXUS_ZITADEL_ISSUER,audcontainsNEXUS_ZITADEL_PROJECT_ID.
The interceptor only authenticates. Each handler performs its own authorization check through authz.