Skip to content

User Provisioning Runbook

Audience: HUPH admins adding counselors/admins. All steps are in Zitadel admin console unless noted.

Add user

Via admin console (preferred):

  1. https://auth.huph.val.id/ui/console → Users → New.
  2. Fields: Email, First/Last name, Username (= email).
  3. Initial password: generate, send via out-of-band channel (not the same email).
  4. Require "Change password on first login" — on.
  5. Enforce MFA (TOTP + WebAuthn) — on.
  6. Grant role in the HUPH project:
  7. admin → project huph-admin, role IAM_OWNER if cluster-admin, else ORG_OWNER.
  8. counselor → project huph-admin, role counselor, set cluster_id claim.

Via API (scripted / bulk):

Bash
# Requires PAT with org.user.write
curl -X POST "https://auth.huph.val.id/management/v1/users/human" \
  -H "Authorization: Bearer $ZITADEL_PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "userName": "new.counselor@huph.val.id",
    "profile": { "firstName": "New", "lastName": "Counselor", "displayName": "New Counselor", "preferredLanguage": "en" },
    "email":   { "email": "new.counselor@huph.val.id", "isEmailVerified": false },
    "password": { "password": "'"$(openssl rand -base64 24)"'", "changeRequired": true }
  }'

Post-create checklist:

Text Only
[ ] User exists in Zitadel + admin.huph.val.id /admin/users shows them
[ ] Role is correct
[ ] cluster_id claim set (counselors only)
[ ] User received credentials via secure channel
[ ] Row added to HUPH audit_log (auto from admin proxy if using provisioning UI)

Deactivate user

Target revocation time: <5 minutes end-to-end.

  1. Zitadel console → Users → search → Deactivate.
  2. Console → Sessions (if visible) → Terminate all. If not available: API DELETE /management/v1/users/{id}/sessions.
  3. Verify in HUPH admin:
    Bash
    # Should return 401 within 60s of deactivation once JWT expires,
    # or immediately if admin proxy checks Zitadel on each request.
    curl -w '%{http_code}\n' https://admin.huph.val.id/api/v1/dashboard/counselor/me \
         -H "Cookie: <their-session>"
    
  4. Zitadel signs new JWTs every 1h. Existing JWTs remain valid until expiry. For immediate kill:
  5. Shorten access token lifetime to 5m in Zitadel project settings (recommended default anyway), or
  6. Rotate the project's signing key: Projects → HUPH → Keys → Rotate. Invalidates all outstanding tokens system-wide (expect user-facing friction — only use for compromise).

Role change

  1. Who can: any user with ORG_OWNER on the HUPH org.
  2. Zitadel console → Users → select → Authorizations → edit role.
  3. User must re-login for new claims to appear in JWT (claims are minted at token issue).
  4. Log the change: Zitadel emits user.grant.changed event; also append to docs/ops/zitadel/role-change-log.md:
    Text Only
    2026-05-10 | fariz@val.id | changed new.counselor@ from counselor → cluster_admin(CASS) | reason: promotion ticket #123
    

Audit trail

  • Canonical: Zitadel events eventstore.events — query via console (Admin → Events) or SQL:
    SQL
    SELECT created_date, event_type, aggregate_id, editor_user, payload
    FROM eventstore.events
    WHERE aggregate_type = 'user'
      AND created_date > now() - interval '30 days'
    ORDER BY created_date DESC;
    
  • HUPH side: admin_audit_log table (if wired) captures actions done via HUPH admin dashboard (not direct Zitadel console).
  • Review both monthly; mismatches = someone bypassed the dashboard.