Onboard an Organisation & Activate a User (Air-Gap)¶
Runbook for creating a customer organisation in the ASK admin UI, adding a user, and activating them in an air-gapped environment where invite emails cannot be delivered.
Context¶
| Concept | Detail |
|---|---|
| Platform org | ask-default — the superadmin / platform organisation. Never shown in the customer org list. |
| Customer org | Created via the admin UI. Users belong here, not to ask-default. |
| Tenant subdomain | Each org gets its own subdomain: {subdomain}.ask.mod.auh1.dev.dir |
| Air-gap problem | Invite emails cannot be delivered → invited users stay PENDING indefinitely |
Step 1 — Get an Admin API Session Token¶
The admin UI calls core-service internally. For direct API work, obtain a token from
the core-service hostname (not the frontend):
RESP=$(curl -sk -X POST https://core-service.ask.mod.auh1.dev.dir/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"org_id": "378447542100166005",
"email": "[email protected]",
"password": "<superadmin-password-from-vault>"
}')
SESSION_TOKEN=$(echo $RESP | jq -r '.sessionToken')
echo $SESSION_TOKEN
Wrong hostname
ask.mod.auh1.dev.dir is the Next.js frontend. Posting /api/v1/auth/login
there returns a 404 Next.js page. Always use core-service.ask.mod.auh1.dev.dir
for direct API calls.
org_id here is the Zitadel organisation ID (found in core-service logs on first login
attempt). The superadmin password is in Vault:
export VAULT_ADDR=https://vault.ask.mod.auh1.dev.dir
vault kv get secret/ask/superadmin # adjust path to match your deployment
Step 2 — Create a Customer Organisation¶
- Log in to
https://admin.ask.mod.auh1.dev.dirassuperadmin. - Navigate to Organisations → Create Organisation.
- Fill in:
- Organisation name — e.g.
mod - Tenant subdomain — e.g.
mod-chat(becomesmod-chat.ask.mod.auh1.dev.dir) - Admin first/last name + email — this triggers an invite email
Air-gap: the invite email will not arrive
Note the admin email you entered. You will activate the account manually in Step 4.
Step 3 — Add the Tenant Subdomain to DNS + Ingress¶
After org creation the chat URL {subdomain}.ask.mod.auh1.dev.dir will 404 because:
- The Mac
/etc/hostshas no entry for the new subdomain. - The nginx ingress has no rule for it.
3a — Mac /etc/hosts¶
Repeat for each new tenant subdomain.
3b — Frontend Helm values (GitOps)¶
Edit mod-ask-frontend/helm/environments/values-mod-auh1-dev-ask.yaml
and add the new hostname to both ingress.hosts and ingress.tls[0].hosts:
ingress:
hosts:
- host: ask.mod.auh1.dev.dir
paths: [{ path: /, pathType: Prefix }]
- host: admin.ask.mod.auh1.dev.dir
paths: [{ path: /, pathType: Prefix }]
- host: mod-chat.ask.mod.auh1.dev.dir # ← add per tenant
paths: [{ path: /, pathType: Prefix }]
tls:
- secretName: frontend-tls
hosts:
- ask.mod.auh1.dev.dir
- admin.ask.mod.auh1.dev.dir
- mod-chat.ask.mod.auh1.dev.dir # ← add per tenant
Commit, push to main, then sync the frontend ArgoCD application. cert-manager
will reissue frontend-tls to cover the new hostname automatically.
Step 4 — Activate an Invited User (Air-Gap Workaround)¶
Because invite emails cannot be delivered, the user stays in PENDING state in Zitadel
and Invited state in the ASK UI. Two approaches:
Option A — Bulk-create-dummy (preferred, no email required)¶
If the user does not already exist, skip the invite entirely and create them directly
in ACTIVE state:
# Get the organisation ID first
ORG_ID=$(curl -sk "https://core-service.ask.mod.auh1.dev.dir/api/v1/organizations" \
-H "Authorization: Bearer $SESSION_TOKEN" \
| jq -r '.organizations[] | select(.name=="mod") | .id')
curl -sk -X POST \
https://core-service.ask.mod.auh1.dev.dir/api/v1/users/bulk-create-dummy \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"emails\": [\"[email protected]\"],
\"default_password\": \"ChangeMe123!\",
\"organization_id\": \"$ORG_ID\"
}" | jq .
Password rules: 8–70 chars, upper + lower + digit + symbol.
Option B — Zitadel metadata patch (user already invited)¶
If the user already exists in Invited / PENDING state:
- Go to
https://sso.ask.mod.auh1.dev.dir→ Users → find the user. - Open the Metadata tab.
- Change
invite_statusvalue fromPENDING→ACTIVEand save (floppy icon). - Open the Security tab → Set Password → set a known password.
- The user can now log in at
https://mod-chat.ask.mod.auh1.dev.dir.
Production note
Option B is a workaround. The canonical fix is to configure SMTP delivery
(Organisation Settings → General → Email delivery) so invite emails work.
Option A (bulk-create-dummy) is the correct air-gap pattern for new users.
Step 5 — Verify¶
# Confirm user is now ACTIVE
curl -sk "https://core-service.ask.mod.auh1.dev.dir/api/v1/organizations/users" \
-H "Authorization: Bearer $SESSION_TOKEN" \
| jq '.users[] | select(.email=="[email protected]") | {id, email, status}'
Expected: "status": "active".
Then log in at https://mod-chat.ask.mod.auh1.dev.dir with the user credentials and
confirm the chat UI loads.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| Login curl returns Next.js 404 HTML | Used ask.mod.auh1.dev.dir instead of core-service.* |
Use https://core-service.ask.mod.auh1.dev.dir/api/v1/auth/login |
| jq parse error on login response | Wrong password or wrong org_id |
Get password from Vault; get org_id from core-service logs |
Browser redirects to {subdomain}.ask.* → DNS error |
Missing /etc/hosts entry |
Add to /etc/hosts (Step 3a) |
| Browser reaches subdomain → nginx 404 | Ingress rule missing | Push Helm values change + ArgoCD sync (Step 3b) |
| TLS cert error on new subdomain | cert-manager hasn't reissued yet | Wait ~60 s after ArgoCD sync; or kubectl describe cert frontend-tls -n ask |
User stays Invited after Zitadel email verified |
Email verification ≠ ASK accept-invite flow | Use Option A or Option B above |
activateNotAllowed in ASK UI |
ASK blocks activating users who haven't run accept-invite | Use Zitadel metadata patch (Option B) |