Skip to content

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

  1. Log in to https://admin.ask.mod.auh1.dev.dir as superadmin.
  2. Navigate to Organisations → Create Organisation.
  3. Fill in:
  4. Organisation name — e.g. mod
  5. Tenant subdomain — e.g. mod-chat (becomes mod-chat.ask.mod.auh1.dev.dir)
  6. 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:

  1. The Mac /etc/hosts has no entry for the new subdomain.
  2. The nginx ingress has no rule for it.

3a — Mac /etc/hosts

sudo sh -c 'echo "100.115.2.210  mod-chat.ask.mod.auh1.dev.dir" >> /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:

  1. Go to https://sso.ask.mod.auh1.dev.dirUsers → find the user.
  2. Open the Metadata tab.
  3. Change invite_status value from PENDINGACTIVE and save (floppy icon).
  4. Open the Security tab → Set Password → set a known password.
  5. 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)