Password History Limit Errors
When repeatedly importing user configurations with password credentials, Keycloak's password history policy can cause imports to fail once the history limit is reached. Understanding how password history works and how to configure credentials properly in keycloak-config-cli is essential for reliable, repeatable imports.
Related issues: #1112
The Problem
Users encounter password history errors when repeatedly importing realms because: - Password history policy tracks previously used passwords - Each import attempt with the same password adds to the history - Once the history limit is reached, Keycloak rejects the password - keycloak-config-cli treats the error as a failure, stopping the import - Keycloak itself only logs a warning, not an error - It's unclear how to configure idempotent password imports - Existing configuration files assume first-time user creation
Understanding Password History
Related Issues
- #1112 - Password history limit errors on repeated imports
- #904 - Error when updating users with credentials
Additional Resources
How Password History Works
Keycloak's password history policy prevents users from reusing recent passwords:
- History Tracking
- Keycloak stores hashes of previous passwords
- Number of tracked passwords defined by policy
-
History per user, not global
-
Validation on Password Change
- New password compared against history
- Rejected if found in recent history
-
Applies to user-initiated and admin-initiated changes
-
Configuration Import Behavior
- Each import with credentials counts as password change
- Repeated imports with same password fill history
- Eventually hits limit and fails
The Error
Typical Error Scenario
Configuration:
{
"realm": "master",
"passwordPolicy": "hashIterations(27500) and passwordHistory(3)",
"users": [
{
"username": "john.doe",
"email": "john.doe@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
]
}
]
}

step2

step1

step2

Error Message:
Failed to update user 'john.doe'
Error: invalidPasswordHistoryMessage - Invalid password: must not be equal to any of last 3 passwords
Solutions
Solution 1: Remove Credentials from Repeated Imports
Scenario: Set passwords once, then remove from configuration.
Initial Import:
{
"realm": "master",
"passwordPolicy": "hashIterations(27500) and passwordHistory(3)",
"users": [
{
"username": "john.doe",
"email": "john.doe@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
]
}
]
}
Subsequent Imports:
{
"realm": "master",
"passwordPolicy": "hashIterations(27500) and passwordHistory(3)",
"users": [
{
"username": "john.doe",
"email": "john.doe@example.com",
"enabled": true
}
]
}
Workflow:
java -jar keycloak-config-cli.jar \
--import.files.locations=initial-setup.json
java -jar keycloak-config-cli.jar \
--import.files.locations=realm-config.json
Benefits: - Credentials set once - No password history conflicts on repeated imports - Clear separation of initial setup vs. configuration updates
Solution 2: Use Unique Passwords per Import
Scenario: Development/testing environments where password uniqueness is needed.
{
"realm": "dev-realm",
"passwordPolicy": "hashIterations(27500) and passwordHistory(3)",
"users": [
{
"username": "test.user",
"email": "test@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "TestPass-20260226143022",
"temporary": true
}
]
}
]
}
Result: - Each import uses different password - Never hits history limit - Useful for automated testing
Note: Requires variable substitution enabled:
java -jar keycloak-config-cli.jar \
--import.var-substitution.enabled=true \
--import.files.locations=realm-config.json
Solution 4: Reduce or Disable Password History
Scenario: Development environments where password history isn't needed.
{
"realm": "dev-realm",
"passwordPolicy": "hashIterations(27500) and passwordHistory(0)",
"users": [
{
"username": "john.doe",
"email": "john.doe@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
]
}
]
}
Result: - No password history tracking - Repeated imports work without errors - Same password can be set repeatedly
Warning: Only for development. Production should have password history enabled.
Solution 5: Separate User Creation from Updates
Scenario: Manage users separately from other realm configuration.
Structure:
config/
├── realm-base.json # Realm settings, clients, roles
├── users-initial.json # User creation with passwords
└── users-updates.json # User updates without passwords
realm-base.json:
{
"realm": "myrealm",
"passwordPolicy": "hashIterations(27500) and passwordHistory(3)",
"clients": [
{
"clientId": "my-app"
}
]
}
users-initial.json (run once):
{
"realm": "myrealm",
"users": [
{
"username": "john.doe",
"email": "john.doe@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "TempPassword123",
"temporary": true
}
]
}
]
}
users-updates.json (repeatable):
{
"realm": "myrealm",
"users": [
{
"username": "john.doe",
"email": "john.doe@example.com",
"enabled": true,
"realmRoles": ["user"],
"groups": ["/Employees"]
}
]
}
Workflow:
java -jar keycloak-config-cli.jar \
--import.files.locations=config/realm-base.json
java -jar keycloak-config-cli.jar \
--import.files.locations=config/users-initial.json
java -jar keycloak-config-cli.jar \
--import.files.locations=config/realm-base.json
java -jar keycloak-config-cli.jar \
--import.files.locations=config/users-updates.json
Password Policy Configuration
Understanding Password History Policy
Format: passwordHistory(N) where N is number of previous passwords to track
Values:
- 0 = Disabled (any password accepted)
- 1 = Previous password cannot be reused
- 3 = Last 3 passwords cannot be reused (common)
- 12 = Last 12 passwords cannot be reused (high security)
Complete Password Policy Example
{
"realm": "secure-realm",
"passwordPolicy": "length(12) and upperCase(1) and lowerCase(1) and digits(1) and specialChars(1) and notUsername and passwordHistory(5) and hashIterations(27500)"
}
Breakdown:
- length(12): Minimum 12 characters
- upperCase(1): At least 1 uppercase letter
- lowerCase(1): At least 1 lowercase letter
- digits(1): At least 1 number
- specialChars(1): At least 1 special character
- notUsername: Password cannot contain username
- passwordHistory(5): Last 5 passwords cannot be reused
- hashIterations(27500): Password hash iterations
Credential Configuration Best Practices
Development Environment
{
"realm": "dev",
"passwordPolicy": "length(8) and passwordHistory(0)",
"users": [
{
"username": "dev.user",
"email": "dev@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "devpass123",
"temporary": false
}
]
}
]
}
Characteristics: - Simple password requirements - No password history - Non-temporary passwords for convenience - Repeated imports work without issues
Staging Environment
{
"realm": "staging",
"passwordPolicy": "length(10) and upperCase(1) and lowerCase(1) and digits(1) and passwordHistory(3)",
"users": [
{
"username": "staging.user",
"email": "staging@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "StagingPass123",
"temporary": true
}
]
}
]
}
Characteristics: - Moderate password requirements - Limited password history (3) - Temporary passwords - Balances security and usability
Production Environment
{
"realm": "production",
"passwordPolicy": "length(14) and upperCase(1) and lowerCase(1) and digits(1) and specialChars(1) and notUsername and passwordHistory(12) and hashIterations(27500)",
"users": [
{
"username": "prod.user",
"email": "prod@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "ComplexTempP@ss123!",
"temporary": true
}
],
"requiredActions": ["UPDATE_PASSWORD", "CONFIGURE_TOTP"]
}
]
}
Characteristics: - Strong password requirements - Extensive password history (12) - Always temporary passwords - Additional required actions (MFA)
Working with Existing Users
Scenario: Update User Without Changing Password
{
"realm": "myrealm",
"users": [
{
"username": "existing.user",
"email": "updated@example.com",
"enabled": true,
"realmRoles": ["user", "manager"]
}
]
}
Result: - Email updated - Role added - Password remains unchanged - No password history impact
Scenario: Force Password Reset
{
"realm": "myrealm",
"users": [
{
"username": "existing.user",
"email": "user@example.com",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "NewTemporaryPassword123",
"temporary": true
}
],
"requiredActions": ["UPDATE_PASSWORD"]
}
]
}
Result: - Password changed to temporary value - User required to change on next login - Password history incremented by 1
Common Pitfalls
1. Repeated Imports with Same Password
Problem:
{
"users": [
{
"username": "john.doe",
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
]
}
]
}
After 3 imports (passwordHistory(3)):
Solution: Use temporary: true:
2. Not Understanding Temporary Password Behavior
Misconception: Temporary passwords change on every import
Reality: Temporary passwords remain the same on subsequent imports, avoiding password history issues
Behavior:
Subsequent imports with same config: - Password remains "TempPass123" - No new password history entries - User still required to change on first login
3. Copying Production Config to Dev
Problem: Production password policies in dev environment
{
"realm": "dev",
"passwordPolicy": "length(14) and passwordHistory(12)",
"users": [
{
"username": "dev.user",
"credentials": [
{
"type": "password",
"value": "TestPass123",
"temporary": false
}
]
}
]
}
Result: Repeated imports quickly hit history limit in dev
Solution: Use appropriate policy for environment:
4. Not Removing Credentials After Initial Setup
Problem: Credentials remain in config file indefinitely
{
"users": [
{
"username": "john.doe",
"credentials": [
{
"type": "password",
"value": "Password123"
}
]
}
]
}
Result: Every import attempts to set password
Solution: Separate initial setup from updates:
5. Forgetting Variable Substitution Flag
Problem: Using dynamic passwords without enabling substitution
Error: Literal string used, not evaluated
Solution: Enable variable substitution:
java -jar keycloak-config-cli.jar \
--import.var-substitution.enabled=true \
--import.files.locations=config.json
Best Practices
-
Always Use Temporary Passwords in Config Files
-
Separate Initial Setup from Updates
users-initial.json- Run once with passwords-
users-updates.json- Repeatable without passwords -
Use Environment Variables for Sensitive Data
-
Document Password Policy Requirements
-
Different Policies per Environment
- Dev: Lenient (passwordHistory(0))
- Staging: Moderate (passwordHistory(3))
-
Prod: Strict (passwordHistory(12))
-
Use Required Actions
-
Test Import Repeatability
-
Monitor Password History Count
- Check user credential history in Admin Console
- Users → select user → Credentials tab
- View password history entries
Troubleshooting
Import Fails with Password History Error
Symptom:
Diagnosis:
-
Check password policy in Admin Console: Realm Settings → Security defenses → Password policy
-
Check user's password history count: Admin Console → Users → john.doe → Credentials
-
Check if credentials in config:
Solution:
Password Policy Too Strict for Testing
Symptom: Cannot create test users due to password requirements
Solution: Override for test realm:
Or create separate test user:
{
"users": [
{
"username": "test.user",
"credentials": [
{
"type": "password",
"value": "Test1234!",
"temporary": true
}
]
}
]
}
User Cannot Login After Import
Symptom: User exists but cannot authenticate
Possible causes: 1. Password set as temporary 2. Required actions pending 3. User not enabled
Check configuration:
{
"users": [
{
"username": "john.doe",
"enabled": true,
"credentials": [
{
"type": "password",
"value": "Password123",
"temporary": false
}
],
"requiredActions": []
}
]
}
Password History Not Clearing
Symptom: Old passwords still in history after time
Explanation: Password history doesn't expire by time, only by count
Solution: - Increase passwordHistory count to accommodate more password changes - Or manually clear via Admin Console if necessary (not recommended) - Or have user change password N+1 times to clear old entries
Configuration Options
Consequences
When dealing with password history in keycloak-config-cli:
- Repeated Imports Increment History: Each import with credentials counts as password change
- History Limit Causes Failures: Once limit reached, import fails completely
- Temporary Passwords Are Idempotent: Using
temporary: trueprevents history accumulation - No Credentials = No Password Change: Omitting credentials preserves existing passwords
- Environment-Specific Policies Required: Dev/staging/prod need different password policies
- User Must Change Temporary Password: First login requires password change
Complete Example: Production-Ready Configuration
{
"realm": "production",
"enabled": true,
"passwordPolicy": "length(14) and upperCase(1) and lowerCase(1) and digits(1) and specialChars(1) and notUsername and passwordHistory(12) and hashIterations(27500)",
"roles": {
"realm": [
{
"name": "user",
"description": "Standard user"
},
{
"name": "admin",
"description": "Administrator"
}
]
},
"users": [
{
"username": "john.doe",
"email": "john.doe@company.com",
"firstName": "John",
"lastName": "Doe",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "${JOHN_DOE_TEMP_PASSWORD}",
"temporary": true
}
],
"realmRoles": ["user"],
"requiredActions": ["UPDATE_PASSWORD", "CONFIGURE_TOTP"]
},
{
"username": "admin.user",
"email": "admin@company.com",
"firstName": "Admin",
"lastName": "User",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "${ADMIN_TEMP_PASSWORD}",
"temporary": true
}
],
"realmRoles": ["admin"],
"requiredActions": ["UPDATE_PASSWORD", "CONFIGURE_TOTP"]
},
{
"username": "service-account-backend",
"enabled": true,
"serviceAccountClientId": "backend-api"
}
],
"clients": [
{
"clientId": "backend-api",
"serviceAccountsEnabled": true,
"standardFlowEnabled": false,
"directAccessGrantsEnabled": false
}
]
}
Import with environment variables:
export JOHN_DOE_TEMP_PASSWORD="ComplexTemp123!"
export ADMIN_TEMP_PASSWORD="AdminTemp456!"
java -jar keycloak-config-cli.jar \
--keycloak.url=https://keycloak.company.com \
--keycloak.user=admin \
--keycloak.password=${KEYCLOAK_ADMIN_PASSWORD} \
--import.var-substitution.enabled=true \
--import.files.locations=production-realm.json \
--import.validate=true \
--import.remote-state.enabled=true
Result: - Strong password policy enforced - Temporary passwords require change - MFA required for all users - Passwords from environment variables - Repeatable imports (temporary passwords don't increment history) - Service accounts use client credentials, not passwords