[Day 87] Cisco ISE Mastery Training: Automating User Creation via API
Table of Contents
Introduction
“Today we’ll automate Internal User lifecycle in Cisco ISE using ERS (External RESTful Services) API and validate authentications with MnT and NAD CLI. You’ll build single-user and bulk (CSV) workflows, enforce identity groups, password policy, and prove end-to-end RADIUS success in logs and on the switch.”
You will build & validate:
- ERS enablement, least-privilege API admin, TLS trust
- Create/Read/Update/Delete (CRUD) for Internal Users
- Assign Identity Groups, set expiry, password change flags
- Bulk user creation from CSV via Python
- Validate with ISE GUI, Live Logs, MnT session queries, and switch CLI (
test aaa
/ 802.1X)
Problem Statement
- Helpdesks manually create hundreds of guest/contractor accounts → slow, error-prone, zero audit trail across systems.
- SOC/IR needs rapid user disables or password resets during incidents.
- Projects require bulk onboarding (contractors, pilots) with consistent group membership and expiry controls.
- Engineers need repeatable APIs that integrate with ITSM/SOAR/HR.
Solution Overview
- ERS API (HTTPS:9060) exposes
/ers/config/internaluser
for full CRUD. - Identity Groups bind users to policy outcomes; set during creation.
- Password policy & expiry enforced at API layer (same as GUI).
- MnT API (HTTPS:443) validates authentications/sessions.
- Auditability: ANC/AAA/ERS actions visible in Operations and application logs.
Sample Lab Topology (VMware/EVE-NG)
Nodes:
- ISE 3.2/3.3 (PAN+MnT+PSN; ERS enabled)
- Catalyst 9300 (17.x) as NAD (802.1X/MAB/CoA; also used for
test aaa
) - WLC 9800-CL (optional Wi-Fi validation)
- Windows 10/11 endpoint (wired or Wi-Fi supplicant)
- Automation Workstation (Postman + Python 3.10+)

Pre-reqs: DNS/NTP aligned; ISE reachable; NAD configured as Network Device in ISE with shared secret; a Policy Set that allows Internal Users.
Step-by-Step GUI Configuration Guide (with CLI + API runbooks)
Format: each phase = do it → validate (GUI + CLI).
Phase 0 — Platform health (CLI quick checks)
ISE CLI
show clock show ntp show application status ise | include Application|Database|pxGrid ping <NAD-IP>
Switch CLI
show clock show authentication sessions
Phase 1 — Enable ERS & create API admin (GUI)
- Enable ERS
GUI: Administration → System → Settings → ERS Settings → Enable ERS for Read/Write → Save
[Screenshot: ISE ERS Settings – Enabled]

- Create ERS admin (least privilege)
GUI: Administration → System → Admin Access → Administrators → Add- User:
ers-user-svc
- Roles: ERS Admin (+ MnT Read Only if you’ll call MnT with same account)
- Optional: Restrict Allowed IPs (security)
[Screenshot: Admin User with ERS role]
- User:

- Certificates (recommended)
GUI: Administration → System → Certificates → System Certificates → bind CA-signed cert for HTTPS; import CA chain on Automation WS.
[Screenshot: System Cert – CA signed]

- Developer Resources
GUI: Administration → Developer Resources → confirm ERS doc available.
[Screenshot: ERS API Docs]
Phase 2 — Identity Groups & Password Policy (GUI)
- Create Identity Groups
GUI: Administration → Identity Management → Groups → User Identity Groups → AddContractors
Guests-24h
[Screenshot: Identity Groups]

- Password Policy
GUI: Administration → System → Settings → Password Policy- Complexity, length, reuse, expiration as per org policy.
[Screenshot: Password Policy]
- Complexity, length, reuse, expiration as per org policy.
Phase 3 — Postman/cURL sanity (ERS + MnT)
Environment vars (Postman):ISE_HOST
, ERS_USER
, ERS_PASS
, BASE_ERS=https://{{ISE_HOST}}:9060/ers
, BASE_MNT=https://{{ISE_HOST}}/admin/API/mnt
[Screenshot: Postman Environment]
ERS list version (smoke test):
curl -k -u $ERS_USER:$ERS_PASS -H "Accept: application/json" \ https://$ISE_HOST:9060/ers/config/version
Expect JSON with version.
MnT session (if any active):
curl -k -u $ERS_USER:$ERS_PASS \ "https://$ISE_HOST/admin/API/mnt/Session/ActiveList"
(Usually XML; set Accept: application/xml
.)
Phase 4 — Create a single Internal User (ERS)
POST {{BASE_ERS}}/config/internaluser
Headers: Accept: application/json
, Content-Type: application/json
Body (JSON):
{ "InternalUser": { "name": "contractor01", "password": "C1sco!234", "enabled": true, "changePassword": false, "email": "contractor01@lab.local", "firstName": "Cont", "lastName": "Ractor", "identityGroups": "Contractors", "description": "Created by ERS API Day87", "expiryDate": "2025-12-31" } }
Expected: HTTP 201 Created with Location header containing object ID.
Validate (GUI):
Administration → Identity Management → Identities → Users → search contractor01
[Screenshot: User present with Contractors group]
Phase 5 — Read / Search / Update / Delete
A) GET user by nameGET {{BASE_ERS}}/config/internaluser/name/contractor01
Expect JSON with user object (contains id
).
B) UPDATE (PUT) – force password change at next loginPUT {{BASE_ERS}}/config/internaluser/{id}
Body (JSON):
{ "InternalUser": { "name": "contractor01", "enabled": true, "changePassword": true, "identityGroups": "Contractors", "description": "Force PW change on next login" } }
Expected: 200/204 success.
C) DELETE user (cleanup)DELETE {{BASE_ERS}}/config/internaluser/{id}
Expected: 204 No Content.
[Screenshots: GET result / PUT success / DELETE success]
Phase 6 — Bulk creation (CSV → Python)
CSV example (users.csv
):
name,password,email,firstName,lastName,identityGroup,expiryDate contractor02,C1sco!234,contractor02@lab.local,Cont,Two,Contractors,2025-12-31 guest001,C1sco!234,guest001@lab.local,Guest,One,Guests-24h,2024-12-31
Python script (minimal, idempotent create/update):
import csv, requests, sys BASE="https://ise.lab.local:9060/ers" AUTH=("ers-user-svc","<password>") H={"Accept":"application/json","Content-Type":"application/json"} VERIFY=False # set CA bundle in prod def get_user_by_name(name): r=requests.get(f"{BASE}/config/internaluser/name/{name}",auth=AUTH,headers=H,verify=VERIFY) return r.json() if r.status_code==200 else None def create_user(row): body={"InternalUser":{ "name":row["name"],"password":row["password"],"enabled":True, "changePassword":False,"email":row["email"], "firstName":row["firstName"],"lastName":row["lastName"], "identityGroups":row["identityGroup"],"expiryDate":row["expiryDate"] }} r=requests.post(f"{BASE}/config/internaluser",json=body,auth=AUTH,headers=H,verify=VERIFY) if r.status_code not in (201,202): print("Create failed:", r.status_code, r.text) def update_user(id, row): body={"InternalUser":{ "name":row["name"],"enabled":True,"changePassword":False, "email":row["email"],"firstName":row["firstName"],"lastName":row["lastName"], "identityGroups":row["identityGroup"],"expiryDate":row["expiryDate"] }} r=requests.put(f"{BASE}/config/internaluser/{id}",json=body,auth=AUTH,headers=H,verify=VERIFY) if r.status_code not in (200,204): print("Update failed:", r.status_code, r.text) with open("users.csv") as f: for row in csv.DictReader(f): obj=get_user_by_name(row["name"]) if obj: update_user(obj["InternalUser"]["id"], row) print("Updated:", row["name"]) else: create_user(row) print("Created:", row["name"])
Validate (GUI): Users appear with correct groups & expiry.
[Screenshot: Bulk users present]
Phase 7 — Policy & Authentication validation
A) Policy Set (ensure path for Internal Users)
GUI: Policy → Policy Sets
- Allowed Protocols for this Policy Set include PAP/ASCII + MSCHAPv2 (for
test aaa
flexibility). - AuthC:
Internal Users
- AuthZ: simple “PermitAccess” or your lab profile.
[Screenshot: Policy Set with Internal Users rule]
B) Validate via Switch CLI (RADIUS test aaa
)
(uses PAP/CHAP; ensure Allowed Protocols permit PAP in the Policy Set used by the NAD)
conf t aaa new-model radius server ISE1 address ipv4 <ISE-IP> auth-port 1812 acct-port 1813 key ISEsecret123 aaa group server radius ISE-GRP server name ISE1 aaa authentication login TEST group ISE-GRP local end test aaa group radius ISE-GRP contractor01 C1sco!234 legacy ! expect "User was successfully authenticated"
C) Validate on ISE (GUI)
- Operations → RADIUS → Live Logs → entry shows
contractor01
Passed - Drill into details → Authentication Policy matched + Authorization Profile
[Screenshot: Live Logs success]
D) Validate via MnT API
curl -k -u $ERS_USER:$ERS_PASS \ "https://$ISE_HOST/admin/API/mnt/Session/UserName/contractor01"
Expect XML with session attributes (IP/MAC/AuthZ result).
Phase 8 — Break & Fix drills (common errors)
- HTTP 401 Unauthorized → wrong creds / ERS role missing / ERS disabled.
- HTTP 409 Conflict on create → username exists → use PUT to update.
- HTTP 400 Bad Request → password violates policy or bad
expiryDate
format. - User created but
test aaa
fails → Policy Set not matching the NAD / Allowed Protocols exclude PAP. - Live Logs empty → request never hit ISE (NAD shared secret or IP not in Network Devices).
Phase 9 — Audit & Logs
ISE GUI:
- Operations → Reports → RADIUS Authentications
- Operations → Audit → Administrator Logins / Change Logs (ERS changes appear)
ISE CLI quick watchers:
show logging application ise-psc.log | include (Auth|User|InternalUser|Policy) show logging application catalina.out | include ERS
Troubleshooting (User Creation via ERS API):
0) Rapid Triage Playbook (read top-to-bottom)
- API Up?
curl -k -u ers:pass https://ISE:9060/ers/config/version -H "Accept: application/json"
- 200 = OK → go next; 401/403/5xx → see §1.
- AuthZ path OK? (for validation with
test aaa
)- GUI: Policy Sets → confirm NAD matches → Allowed Protocols include method you’ll test (PAP for
test aaa
, EAP for 802.1X).
- GUI: Policy Sets → confirm NAD matches → Allowed Protocols include method you’ll test (PAP for
- Create 1 user (golden payload)
- POST
/ers/config/internaluser
with minimal body from §3. - 201 + Location header → verify in GUI; see §2–§4.
- POST
- Validate login
test aaa group radius ISE-GRP <user> <pw> legacy
(switch CLI).- success in switch + ISE Live Logs; see §5.
- Bulk?
- Run Python CSV importer (§7).
- If failures: examine HTTP code → jump to matching section.
1) Connectivity, Auth, and Service State
A) ERS disabled or wrong port
Symptom: curl: (7) Failed to connect
or HTTP 404 at port 9060.
Fix: GUI → Administration → System → Settings → ERS Settings → Enable ERS for Read/Write → Save.
CLI check:
show application status ise | include (Application|pxGrid|Database|M&T|API) # Expect ERS/Tomcat components running
B) 401 Unauthorized / 403 Forbidden
Causes: Bad creds, wrong role, IP-restricted account.
Fixes:
- GUI → Admin Access → Administrators → user = ERS Admin role.
- Remove/adjust Allowed IPs restriction for this admin.
- Verify Basic Auth in Postman (no extra spaces).
Re-test:
curl -k -u ers-user-svc:'<password>' https://ISE:9060/ers/config/version -H "Accept: application/json"
C) TLS / Certificates
Symptom: Postman SSL error or Python SSLError
.
Fix:
- Lab: allow insecure (
-k
in curl, “Disable SSL verification” in Postman,verify=False
in Python). - Prod: install CA-signed server cert on ISE; trust CA on client.
2) Payload & Schema Errors
A) 400 Bad Request
Common reasons:
- Wrong JSON shape (missing
InternalUser
wrapper). - Field typo (
identityGroup
vsidentityGroups
). - Password violates policy.
- Date format wrong.
Golden minimal POST body (known-good):
{ "InternalUser": { "name": "contractor01", "password": "C1sco!234", "enabled": true, "identityGroups": "User Identity Group" } }
Fix sequence:
- Switch to JSON headers:
Accept: application/json Content-Type: application/json
- Remove optional fields; add back one by one.
- If still 400, check ISE Password Policy (GUI) and date format
YYYY-MM-DD
.
B) 415 Unsupported Media Type
Cause: Missing/incorrect Content-Type
.
Fix: Set Content-Type: application/json
.
C) 422 Unprocessable Entity
Cause: Field valid syntactically but invalid logically (e.g., group path doesn’t exist).
Fix: Confirm group name exactly as shown in GUI (case-sensitive). Use GET to enumerate groups:
GET /ers/config/identitygroup
3) Object Conflicts & Idempotency
A) 409 Conflict (user exists)
When: duplicate name
.
Fix:
- GET by name to retrieve
id
:GET /ers/config/internaluser/name/<username>
- Then PUT to update:
PUT /ers/config/internaluser/{id}
B) Idempotent create-or-update pattern (Python)
obj = get_user_by_name("contractor01") if obj: put_update(obj["InternalUser"]["id"], payload) else: post_create(payload)
4) Paging & Bulk Operations
A) Large result sets return paged
Symptom: only first page visible.
Fix: follow nextPage
link or HTTP Link
header until none remains.
B) 429 Too Many Requests / perceived slowness
Best practices:
- Throttle to ~10 req/s per ISE node.
- Use session reuse in Python (
requests.Session()
); keep-alive. - Stagger bulk imports (e.g., 100 users/batch with 0.2s delay).
5) Authentication Validation Failures (RADIUS)
A) test aaa
fails but user created
Checklist:
- NAD registered in ISE (Network Devices) with correct shared secret & correct NAD IP/subnet.
- Policy Set selected by NAD conditions.
- Allowed Protocols include PAP if using
test aaa legacy
. - AuthC rule: includes Internal Users as identity source.
Switch CLI quick config (lab):
radius server ISE1 address ipv4 <ISE-IP> auth-port 1812 acct-port 1813 key ISEsecret123 aaa group server radius ISE-GRP server name ISE1 aaa authentication login TEST group ISE-GRP local test aaa group radius ISE-GRP contractor01 C1sco!234 legacy
ISE GUI validation:
- Operations → RADIUS → Live Logs:
- Passed vs Failed reason (e.g., “User not found”, “Password policy”).
- Click details → Policy Set & AuthC rule hit.
B) 802.1X endpoint fails but test aaa
passes
- 802.1X uses EAP (PEAP/EAP-MSCHAPv2). Ensure Allowed Protocols include EAP methods.
- Supplicant config on endpoint correct? Username/password?
- Realtime check: Operations → Live Sessions; WLC/switch
show authentication sessions interface …
.
6) Expiry, Disable, and Password Policy
A) Expiry date format
- Use
YYYY-MM-DD
. - If error persists, omit
expiryDate
to confirm creation then set via PUT.
B) Force password change next login
PUT /internaluser/{id}
with"changePassword": true
- Validate in GUI user properties; Live Logs will show change prompt flow for web portals.
C) Disable vs Delete
- Disable =
"enabled": false
(audit-friendly, reversible). - Delete =
DELETE /internaluser/{id}
(irreversible).
7) Bulk CSV Import – Failure Patterns & Fixes
CSV sample (good):
name,password,email,firstName,lastName,identityGroups,expiryDate contractor02,C1sco!234,contractor02@lab.local,Cont,Two,User Identity Group,2025-12-31 guest001,C1sco!234,guest001@lab.local,Guest,One,User Identity Group,2024-12-31
Common breakages:
- Header mismatch (
identityGroup
vsidentityGroups
). - Hidden spaces/UTF-8 BOM → strip/normalize.
- Password violates policy → 400; log it and continue next rows.
Python resilience tips:
try/except
per row; collect failures to a CSV errors.csv.- Validate group existence once (cache group names).
- Sleep(0.2) between requests, or batch 50–100.
8) Postman Troubleshooting
- Auth tab: Basic Auth (username/password).
- Headers:
Accept: application/json
(ERS),application/xml
(MnT typical).Content-Type: application/json
on POST/PUT.
- SSL: Settings → disable verification in lab.
- Tests (quick guardrail):
pm.test("201 Created", function () { pm.response.to.have.status(201); });
- Variables: Keep
BASE_ERS
,BASE_MNT
,ERS_USER
,ERS_PASS
in Environment.
9) Sample Error/Success Payloads (recognize them fast)
A) 201 Created (Location header)
HTTP/1.1 201 Created Location: https://ISE:9060/ers/config/internaluser/1a2b3c4d-...
B) 400 Bad Request (password policy)
{ "ERSResponse": { "operation": "POST-create-internaluser", "messages": [{ "title": "Invalid password: does not meet policy", "type": "ERROR", "code": "password-policy-violation" }] } }
C) 409 Conflict (duplicate user)
{ "ERSResponse": { "messages": [{ "title": "Duplicate resource", "type": "ERROR", "code": "duplicate" }] } }
D) 422 Invalid group
{ "ERSResponse": { "messages": [{ "title": "Identity group not found: 'Contractors'", "type": "ERROR", "code": "invalid-reference" }] } }
10) Logs & Where to Look (GUI + CLI)
GUI:
- Operations → Audit (who/what via ERS).
- Operations → RADIUS → Live Logs (auth success/fail reason).
- Operations → Reports → Authentication Summary.
CLI (typical quick grep):
show logging application ise-psc.log | include (InternalUser|ERS|Auth|Policy) show logging application catalina.out | include ERS # If needed: # application stop ise ; application start ise (last resort in lab)
11) Security & Governance (prod readiness)
- Least-privilege ERS account; IP-restrict admin user.
- CA-signed certs; enforce TLS verification in Postman/Python.
- Secrets mgmt (Vault/Azure Key Vault); avoid plain CSV passwords at rest.
- SIEM: ship Audit & RADIUS logs for immutable evidence.
- Change control: scripts include ticket/reference IDs in comments/log lines.
12) Ready-to-Use “Golden” Requests
Create user (JSON)
POST https://ISE:9060/ers/config/internaluser Headers: Accept: application/json, Content-Type: application/json Body: { "InternalUser": { "name": "contractor01", "password": "C1sco!234", "enabled": true, "identityGroups": "User Identity Group", "description": "ERS Day87" } }
Get user by name
GET https://ISE:9060/ers/config/internaluser/name/contractor01 Accept: application/json
Update (force password change)
PUT https://ISE:9060/ers/config/internaluser/{id} { "InternalUser": { "name": "contractor01", "enabled": true, "changePassword": true, "identityGroups": "User Identity Group" } }
Disable user
PUT https://ISE:9060/ers/config/internaluser/{id} { "InternalUser": { "name": "contractor01", "enabled": false } }
Delete user
DELETE https://ISE:9060/ers/config/internaluser/{id}
13) Decision Trees (textual)
Create fails → which code?
- 401/403 → creds/role/IP restrict/ERS off.
- 415 → Content-Type.
- 400 → schema/password/date.
- 409 → user exists → GET id → PUT.
- 422 → fix group path/name.
- 5xx → ISE load/services; restart app in lab; check disk/CPU.
Auth fails in test aaa
- No Live Log → NAD secret/IP mismatch.
- Live Log “User not found” → wrong policy set or wrong ID store.
- Live Log “Authentication failed” → password typo/policy/disabled.
14) Bulk-Run Guardrails (Python)
- Use
requests.Session()
; set timeouts (timeout=10
). - Add retry (
backoff
0.5/1/2 up to 5). - Log CSV row + HTTP code + message to
errors.csv
. - Commit in chunks; print success counts.
15) Clean-Up Checklist
- DELETE lab users you created.
- Re-enable SSL verification in tools.
- Rotate ERS admin password post-lab.
- Export Audit report for your lab journal.
FAQs – ISE ERS API (User Creation)
1. What is the Cisco ISE ERS API and why do we use it instead of the GUI?
The External RESTful Services (ERS) API in Cisco ISE is a programmable interface that allows automation of tasks (like creating, updating, or deleting internal users). While the GUI is manual and time-consuming, the ERS API lets engineers integrate user creation with HR systems, scripts, or bulk automation tools—reducing errors and speeding up provisioning.
2. How do I enable ERS API access in Cisco ISE?
ERS API is disabled by default.
Steps:
- GUI → Administration → System → Settings → ERS Settings → enable ERS for Read/Write.
- Ensure you use an account with the ERS Admin role.
- Access via
https://<ISE-FQDN>:9060/ers/...
.
Without enabling this, you’ll see 401 Unauthorized or 404 Not Found when calling endpoints.
3. What’s the minimum JSON payload required to create a user?
At minimum, you need:
{ "InternalUser": { "name": "contractor01", "password": "C1sco!234", "enabled": true, "identityGroups": "User Identity Group" } }
If you omit required fields or misname them, ISE will respond with 400 Bad Request.
4. How do I troubleshoot 400 Bad Request errors during user creation?
Check for:
- Missing
InternalUser
wrapper. - Wrong header (
Content-Type: application/json
). - Password doesn’t meet ISE’s password policy.
identityGroups
doesn’t match exactly (case-sensitive).
Pro tip: Always test with a golden minimal payload, then expand.
5. Can I bulk import users via ERS API?
Yes. A script (Python/Postman runner) can read a CSV file and loop API calls. Example CSV:
name,password,identityGroups contractor02,C1sco!234,User Identity Group contractor03,C1sco!234,User Identity Group
Bulk automation reduces errors, but throttle requests (e.g., 100 users/batch with slight delay) to avoid API overload.
6. How do I verify if a user was created successfully?
- REST API check:
GET /ers/config/internaluser/name/<username>
- GUI check: Navigate to Administration → Identity Management → Identities.
- RADIUS test: On switch/WLC:
test aaa group radius ISE-GRP contractor01 C1sco!234 legacy
ISE’s Live Logs will confirm authentication attempts.
7. How do I handle duplicate users (409 Conflict error)?
- Error occurs if
name
already exists. - Solution:
GET /ers/config/internaluser/name/<username>
→ fetch user’s ID.PUT /ers/config/internaluser/{id}
→ update user details instead of creating a duplicate.
This makes your script idempotent (safe to re-run without breaking).
8. Can I set expiry dates or force password changes via API?
Yes.
- Expiry date must be
YYYY-MM-DD
. - Example:
{ "InternalUser": { "name": "guest01", "password": "C1sco!234", "enabled": true, "identityGroups": "Guest Identity Group", "expiryDate": "2025-12-31", "changePassword": true } }
If the format is wrong, you’ll see 400 Bad Request.
9. How do I troubleshoot when users are created but can’t authenticate?
Checklist:
- NAD registered in ISE with correct IP and RADIUS shared secret.
- Correct Policy Set selected.
- Allowed Protocols include PAP for
test aaa
or EAP methods for 802.1X. - Authentication rule includes Internal Users as identity source.
Check Operations → RADIUS → Live Logs for the exact reason.
10. What are best practices for securing ERS API automation in production?
- Use a least-privilege ERS account, restricted by IP.
- Deploy CA-signed certificates (disable
-k
insecure mode). - Never hardcode passwords in scripts—use secrets vaults.
- Log all API actions to SIEM for audit trails.
- Test in lab first before running in production.
YouTube Link
For more in-depth Cisco ISE Mastery Training, subscribe to my YouTube channel Network Journey and join my instructor-led classes for hands-on, real-world ISE experience
Closing Notes
- Treat Internal User lifecycle as code: create/update/disable with guarantees and logs.
- Always validate twice: ISE Live Logs and NAD CLI (
test aaa
/ 802.1X session). - Productionize with CA certs, least-privilege ERS admin, IP-restricted access, and SIEM shipping of audit logs.
Upgrade Your Skills – Start Today
“For more in-depth Cisco ISE Mastery Training, subscribe to my YouTube channel Network Journey and join my instructor-led classes.
Fast-Track to Cisco ISE Mastery Pro — 4-month live cohort:
- Full labs: REST/ERS, MnT, ANC, pxGrid, TrustSec, Posture, Wired/Wireless
- Real SOC integrations + automation playbooks + troubleshooting bibles
Course outline & enrollment: https://course.networkjourney.com/ccie-security/ ”
Enroll Now & Future‑Proof Your Career
Email: info@networkjourney.com
WhatsApp / Call: +91 97395 21088