Audit them by privilege and by use, not by count. Service accounts are non-interactive users created for integrations, and they accumulate the admin or security_admin role because it is the fastest way to make an integration work — after which nobody revisits it. The two questions worth answering are which of them hold platform-wide roles they do not need, and which have not authenticated in months while their credentials remain valid.
What This Is
Service accounts in ServiceNow are non-interactive user records created for system integrations, automated processes, MID Server connectivity, and API-based access. These accounts typically have the web_service_access_only flag set (restricting them to SOAP/REST access only) but frequently accumulate excessive privileges — particularly the admin or security_admin roles — because integration developers default to the highest privilege level during setup and never reduce it.
Service Account Landscape in ServiceNow
Service Account Types
├── Integration Users (web_service_access_only = true)
│ ├── REST/SOAP integration accounts
│ ├── LDAP import user
│ ├── Discovery/CMDB integration user
│ └── IntegrationHub / Flow Designer service user
├── MID Server Accounts
│ ├── mid_server role user
│ └── Often has additional roles for discovery/orchestration
├── Process Automation Accounts
│ ├── Scheduled job runners
│ ├── Workflow/Flow context users
│ └── Event processing accounts
└── Vendor / Partner Accounts
├── Third-party integration users
├── Managed service provider accounts
└── SaaS connector users
Where Service Accounts Live
| Component | Table / Field | What It Stores |
|---|---|---|
| User Record | sys_user |
Account record with web_service_access_only flag |
| Role Assignments | sys_user_has_role |
Roles granted to the service account |
| Group Membership | sys_user_grmember |
Groups (and inherited roles) |
| Password | sys_user.user_password |
Hashed password (no enforced rotation for service accounts) |
| Last Login | sys_user.last_login_time |
Last authentication timestamp |
| OAuth Tokens | sys_oauth_token |
OAuth tokens issued to the service account |
| MID Server Config | ecc_agent |
MID Server credential references |
Why Service Accounts Are the #1 Audit Finding
| Risk Factor | Description |
|---|---|
| Admin role by default | Integration developers grant admin "to make it work," never reduce |
| No password rotation | ServiceNow has no built-in service account password rotation |
| No MFA | Service accounts are exempt from MFA by necessity — and by exploit |
| Shared credentials | Multiple integrations share the same service account |
| No SSO | Service accounts use local authentication, bypassing IdP controls |
| Stale accounts | Vendor or project accounts persist long after the need ends |
| No ownership | No documented owner responsible for the account lifecycle |
Why This Is Dangerous
Attack Scenario: Compromised Service Account with Admin Role
Precondition: A service account (svc_integration) has the admin role and its password has not been rotated in 3 years. The password is stored in a shared integration configuration file on a developer's workstation.
Attack chain:
An attacker compromises a developer's workstation via phishing or malware. They find integration configuration files containing ServiceNow credentials:
# integration-config.properties (found on developer laptop) servicenow.url=https://company.service-now.com servicenow.user=svc_integration servicenow.password=Integration2021!The attacker authenticates directly to the ServiceNow REST API using the service account credentials:
GET /api/now/table/sys_user?sysparm_query=roles=admin Authorization: Basic c3ZjX2ludGVncmF0aW9uOkludGVncmF0aW9uMjAyMSE=Because the service account has admin role, the attacker can:
- Read all data including PII, financial records, and security configurations
- Create new admin accounts for persistence
- Modify ACLs to open additional attack paths
- Export entire tables via CSV or JSON
- Modify business rules to inject backdoors
- Disable security controls and audit logging
The attack goes undetected because:
- The service account authenticates via API, not UI — no login page monitoring
- Service accounts are exempt from MFA
- The account's activity blends in with legitimate integration traffic
- No password rotation means the credential remains valid indefinitely
Impact: Complete instance compromise via a service account that was never secured. The attacker has persistent admin access with no MFA gate, no session timeout enforcement, and no SSO visibility.
Attack Scenario: MID Server Account Lateral Movement
Precondition: The MID Server user account has roles beyond mid_server — such as admin, discovery_admin, or itil — granted during initial setup.
Attack chain:
An attacker compromises the MID Server host in the customer's data center (the MID Server runs inside the corporate network by design).
The attacker extracts the MID Server credentials from the local configuration:
# Found in <MID_HOME>/agent/config.xml <parameter name="mid.instance.username" value="mid_server_user"/> <parameter name="mid.instance.password" value="encrypted:AAABBBCCC..." />If the MID Server encryption key is on the same host (common default), the attacker decrypts the password.
The attacker uses the MID Server credentials to authenticate directly to the ServiceNow instance REST API — bypassing the MID Server's intended communication channel.
With admin role on the MID Server account, the attacker has full API access to the ServiceNow instance from within the corporate network.
Impact: Lateral movement from the corporate network to the ServiceNow cloud instance via a compromised MID Server. The MID Server account becomes a bridge from on-premise compromise to cloud compromise.
Attack Scenario: Shared Service Account Credential Sprawl
Precondition: A single service account (svc_snow_api) is shared across 12 different integrations managed by 4 different teams.
One team stores the credential in a Git repository (in a configuration file committed years ago).
The Git repository is public or accessible to contractors who have since left the organization.
Password rotation is impossible because changing the password would break all 12 integrations, and no one has a complete list of where the credential is used.
The organization is aware the credential may be compromised but cannot rotate it without a coordinated change across all 12 systems.
Impact: Known-compromised credential that cannot be remediated due to credential sprawl. Every additional integration sharing the credential increases the attack surface geometrically.
How to Detect
Service Account & Integration User Audit
/*
* IDN-006 Detection Script
* Audits service accounts and integration users for:
* excessive roles, stale passwords, MFA exemptions,
* shared credentials, and orphaned accounts
*
* Run as: Background script with admin role
* Impact: Read-only, safe for production
* Versions: Washington+
*/
gs.info('=== IDN-006: SERVICE ACCOUNT & INTEGRATION USER AUDIT ===');
gs.info('Scan started: ' + new GlideDateTime().getDisplayValue());
gs.info('');
var totalFindings = 0;
// 1. Find all service accounts (web_service_access_only = true)
gs.info('--- SERVICE ACCOUNTS (web_service_access_only) ---');
var svcAccounts = new GlideRecord('sys_user');
svcAccounts.addQuery('web_service_access_only', true);
svcAccounts.addQuery('active', true);
svcAccounts.query();
var svcCount = svcAccounts.getRowCount();
gs.info('Active service accounts: ' + svcCount);
gs.info('');
while (svcAccounts.next()) {
var userName = svcAccounts.getValue('user_name');
var lastLogin = svcAccounts.getValue('last_login_time') || 'Never';
var passwordNeedsReset = svcAccounts.getValue('password_needs_reset');
var sysCreated = svcAccounts.getValue('sys_created_on');
var sysUpdated = svcAccounts.getValue('sys_updated_on');
var locked = svcAccounts.getValue('locked_out');
gs.info(' Account: ' + userName);
gs.info(' Created: ' + sysCreated + ' | Last login: ' + lastLogin);
// Check for admin or security_admin roles
var highPriv = new GlideRecord('sys_user_has_role');
highPriv.addQuery('user', svcAccounts.getUniqueValue());
highPriv.addEncodedQuery('role.name=admin^ORrole.name=security_admin^ORrole.name=maint');
highPriv.query();
var highRoles = [];
while (highPriv.next()) {
highRoles.push(highPriv.getDisplayValue('role'));
}
if (highRoles.length > 0) {
gs.info(' [CRITICAL] High-privilege roles: ' + highRoles.join(', '));
totalFindings++;
}
// Check all assigned roles
var allRoles = new GlideRecord('sys_user_has_role');
allRoles.addQuery('user', svcAccounts.getUniqueValue());
allRoles.query();
var roleCount = allRoles.getRowCount();
gs.info(' Total roles assigned: ' + roleCount);
if (roleCount > 5) {
gs.info(' [HIGH] Excessive role count (' + roleCount + ') — review for least privilege');
totalFindings++;
}
// Check password age
var pwdChanged = svcAccounts.getValue('password_needs_reset');
var lastPwdChange = svcAccounts.getValue('sys_updated_on');
if (sysCreated && sysUpdated) {
var created = new GlideDateTime(sysCreated);
var now = new GlideDateTime();
var accountAge = Math.round(
(now.getNumericValue() - created.getNumericValue()) / 86400000
);
if (accountAge > 365) {
gs.info(' [HIGH] Account age: ' + accountAge + ' days — verify password rotation');
totalFindings++;
}
}
// Check last login (stale account detection)
if (lastLogin !== 'Never') {
var lastLoginDate = new GlideDateTime(lastLogin);
var nowDate = new GlideDateTime();
var daysSinceLogin = Math.round(
(nowDate.getNumericValue() - lastLoginDate.getNumericValue()) / 86400000
);
if (daysSinceLogin > 180) {
gs.info(' [WARNING] Last login ' + daysSinceLogin +
' days ago — potentially stale');
totalFindings++;
}
} else if (sysCreated) {
var createdDate = new GlideDateTime(sysCreated);
var todayDate = new GlideDateTime();
var daysSinceCreation = Math.round(
(todayDate.getNumericValue() - createdDate.getNumericValue()) / 86400000
);
if (daysSinceCreation > 90) {
gs.info(' [WARNING] Account created ' + daysSinceCreation +
' days ago but never logged in — orphaned account');
totalFindings++;
}
}
gs.info('');
}
// 2. Find non-service accounts used for integrations
gs.info('--- NON-SERVICE ACCOUNTS WITH API ACCESS PATTERNS ---');
gs.info('(Active users without web_service_access_only that may be used as service accounts)');
var possibleSvc = new GlideRecord('sys_user');
possibleSvc.addQuery('active', true);
possibleSvc.addQuery('web_service_access_only', false);
possibleSvc.addEncodedQuery('user_nameLIKEsvc_^ORuser_nameLIKEint_^ORuser_nameLIKEapi_^ORuser_nameLIKEservice^ORuser_nameLIKEmid_^ORuser_nameLIKEintegration');
possibleSvc.query();
while (possibleSvc.next()) {
gs.info(' [REVIEW] Possible service account without web_service_access_only: ' +
possibleSvc.getValue('user_name') +
' | Active: ' + possibleSvc.getValue('active') +
' | Last login: ' + (possibleSvc.getValue('last_login_time') || 'Never'));
totalFindings++;
}
gs.info('');
// 3. Check MID Server user accounts
gs.info('--- MID SERVER USER ACCOUNTS ---');
var midUsers = new GlideRecord('sys_user_has_role');
midUsers.addQuery('role.name', 'mid_server');
midUsers.addQuery('user.active', true);
midUsers.query();
while (midUsers.next()) {
var midUser = midUsers.getDisplayValue('user');
var midUserId = midUsers.getValue('user');
gs.info(' MID Server user: ' + midUser);
// Check for excessive roles beyond mid_server
var midRoles = new GlideRecord('sys_user_has_role');
midRoles.addQuery('user', midUserId);
midRoles.query();
var extraRoles = [];
while (midRoles.next()) {
var roleName = midRoles.getDisplayValue('role');
if (roleName !== 'mid_server') {
extraRoles.push(roleName);
}
}
if (extraRoles.length > 0) {
gs.info(' [HIGH] Additional roles beyond mid_server: ' + extraRoles.join(', '));
totalFindings++;
}
}
gs.info('');
// 4. Check for OAuth tokens issued to service accounts
gs.info('--- OAUTH TOKENS FOR SERVICE ACCOUNTS ---');
var svcTokens = new GlideRecord('sys_oauth_token');
if (svcTokens.isValid()) {
svcTokens.addQuery('user.web_service_access_only', true);
svcTokens.query();
var svcTokenCount = svcTokens.getRowCount();
if (svcTokenCount > 0) {
gs.info(' OAuth tokens issued to service accounts: ' + svcTokenCount);
gs.info(' [REVIEW] Ensure token lifetimes and scopes are appropriate');
}
}
gs.info('');
// Summary
gs.info('=== SUMMARY ===');
gs.info('Total service account findings: ' + totalFindings);
gs.info('');
gs.info('CRITICAL ACTIONS:');
gs.info('1. Remove admin/security_admin roles from all service accounts');
gs.info('2. Create dedicated roles with minimum required permissions');
gs.info('3. Implement password rotation (90-day cycle minimum)');
gs.info('4. Set web_service_access_only = true for all integration accounts');
gs.info('5. Assign an owner to every service account');
gs.info('6. Deactivate service accounts not used in 180+ days');
gs.info('7. Eliminate shared credentials — one account per integration');
gs.info('8. Restrict MID Server accounts to mid_server role only');
Remediation
Step 1: Inventory and Classify Service Accounts
1. Generate a complete list of service accounts:
- All users with web_service_access_only = true
- All users with names matching svc_*, int_*, api_*, service*, mid_*
- All users with mid_server, soap, rest_api_explorer, or snc_internal roles
2. For each account, document:
- Owner (team/individual responsible)
- Purpose (which integration, what it does)
- Required roles (minimum privilege analysis)
- Password last changed date
- Associated integrations (where the credential is used)
- Expiry date (when should this account be reviewed/decommissioned)
3. Classify accounts by risk:
| Classification | Criteria | Action |
|---------------|----------|--------|
| Critical | Has admin/security_admin role | Immediate role reduction |
| High | Password age > 365 days | Schedule password rotation |
| Medium | No documented owner | Assign owner within 30 days |
| Low | Properly configured | Annual review |
Step 2: Implement Least-Privilege Roles
For each service account, replace admin with custom roles:
Example: Integration that reads incidents and creates changes
BEFORE: admin role (full access to everything)
AFTER: Custom role 'svc_incident_change' with:
- Read access to incident table
- Create/write access to change_request table
- Read access to cmdb_ci table (for CI references)
- No access to sys_user, sys_properties, or security tables
Steps:
1. Navigate to User Administration > Roles
2. Create a new role for each integration use case
3. Add table-level ACLs granting minimum required access
4. Assign the custom role to the service account
5. Remove admin/security_admin roles
6. Test the integration to verify it still functions
7. Monitor for 403 errors indicating missing permissions
8. Add specific permissions as needed (not admin)
Step 3: Implement Password Rotation
ServiceNow does not enforce password rotation for service accounts by default.
Implement manual or automated rotation:
Option A: Scheduled password rotation (recommended)
1. Create a scheduled job that runs monthly
2. For each service account approaching rotation date:
- Generate a new random password
- Update the service account password
- Store the new password in your secrets vault (CyberArk, HashiCorp, etc.)
- Notify the integration owner
- Update the integration's credential store
Option B: Use OAuth instead of passwords (preferred)
1. Migrate service accounts from Basic Auth to OAuth Client Credentials
2. OAuth tokens have automatic expiry and can be revoked
3. Client secrets can be rotated independently of user passwords
4. See IDN-005 for OAuth security configuration
Rotation schedule:
| Account Type | Rotation Frequency |
|-------------|-------------------|
| Admin-privileged (during remediation) | Immediately, then 30 days |
| Standard service accounts | 90 days |
| MID Server accounts | 90 days |
| Vendor/partner accounts | 60 days |
Step 4: Enforce Account Hygiene
1. Deactivation policy:
- Service accounts not used in 180 days: notify owner
- Service accounts not used in 270 days: deactivate
- Service accounts not used in 365 days: remove roles, schedule deletion
2. Naming convention:
- svc_<system>_<purpose> (e.g., svc_jira_incident_sync)
- Set web_service_access_only = true
- Set internal_integration_user = true (where applicable)
3. Eliminate shared credentials:
- Each integration gets its own dedicated service account
- No two systems share the same credential
- Document all credential locations per account
Post-Remediation Verification
/*
* Verify service account hardening is complete
*/
gs.info('=== IDN-006: POST-REMEDIATION CHECK ===');
var issues = 0;
// Check no service accounts have admin role
var svcAdmin = new GlideRecord('sys_user_has_role');
svcAdmin.addQuery('user.web_service_access_only', true);
svcAdmin.addQuery('user.active', true);
svcAdmin.addEncodedQuery('role.name=admin^ORrole.name=security_admin');
svcAdmin.query();
if (svcAdmin.getRowCount() > 0) {
gs.info('FAIL: ' + svcAdmin.getRowCount() +
' service accounts still have admin/security_admin');
issues++;
}
// Check MID Server accounts have only mid_server role
var midAccounts = new GlideRecord('sys_user_has_role');
midAccounts.addQuery('role.name', 'mid_server');
midAccounts.addQuery('user.active', true);
midAccounts.query();
while (midAccounts.next()) {
var midUid = midAccounts.getValue('user');
var otherRoles = new GlideRecord('sys_user_has_role');
otherRoles.addQuery('user', midUid);
otherRoles.addQuery('role.name', '!=', 'mid_server');
otherRoles.query();
if (otherRoles.getRowCount() > 0) {
gs.info('FAIL: MID Server user "' +
midAccounts.getDisplayValue('user') +
'" has ' + otherRoles.getRowCount() + ' extra roles');
issues++;
}
}
if (issues === 0) {
gs.info('PASS: Service account configuration meets security requirements');
} else {
gs.info('FAIL: ' + issues + ' service account issues remaining');
}
Regulatory Impact
NIS2 Mapping
| Article | Requirement | How Service Account Mismanagement Violates It | Evidence After Fix |
|---|---|---|---|
| Art.21§2(a) | Risk analysis and IS security policies | Service accounts with admin privileges and no password rotation represent unmanaged high-risk access pathways not captured in risk assessments | Service account inventory completed, admin roles removed, password rotation implemented, risk register updated with service account controls |
DORA Mapping
| Article | Requirement | How Service Account Mismanagement Violates It | Evidence After Fix |
|---|---|---|---|
| Art.9§4(d) | Manage ICT-related access rights | Service accounts with excessive privileges, no rotation, and no ownership violate access rights management requirements for ICT assets | Least-privilege roles assigned, 90-day rotation enforced, account ownership documented, quarterly access reviews implemented |
ISO 27001:2022 Mapping
| Control | Requirement | How Service Account Mismanagement Violates It | Evidence After Fix |
|---|---|---|---|
| A.8.2 | Privileged access rights | Service accounts with admin role represent uncontrolled privileged access; shared credentials prevent individual accountability | Admin roles replaced with custom least-privilege roles, one account per integration, owners assigned, rotation policy enforced |
Expert Notes
Practitioner annotations pending — article content has been technically validated.