A few weeks ago, I had to built a push-push integration — i.e. the ServiceNow® Instance talked to an API of a third-party tool as well as the third-party tool talked to the instance via a ServiceNow API.
While normal users where already forced to use MFA with an external Authentication by Microsoft Office365, there was the need that integration users were secured similarly. Further condition was that the instance needed to be accessible like before, so no IP address restriction for the whole instance was an option as you are able to configure with ServiceNow IP Access Control. Also the IP Access Control solution ServiceNow, that is out-of-the-box in place, still lets through certain ServiceNow internal IP addresses.
Installation Exists only work for interactive logins which are already effectively prohibited by the Flag “Web service access only” on the corresponding user. So I needed to find a solution that works on any authentication attempt.
A Similar Scenario in the wild
The ServiceNow HI instance is the go-to instance for ServiceNow Customers when having issues or request as well as for many customer-related internal things for ServiceNow.
Therefor this instance needs to be accessible for all customers, but specific integrations, e.g. for automating customer-instance related things, should only be possible from the ServiceNow-internal network.
User Authentication Gates
As I reverse-engineered ServiceNow earlier, I knew I could hook into the login process with User Authentication Gates. These allow to block / double-check certain users. Originally implemented e.g. for MSPs, this feature is also used by the ServiceNow Access Control Plugin (see here and here).
Basically, out of the box, the normal authentication flow looks like the following:
The gray box means, these two actions follow directly after each other, but if one fails, the whole check fails. This is due to the fact that both checks (all factor authentication + gate check(s)) are tied together in a logical AND operation. Also each single User Authentication Gate must return true so that gateUser evaluates to true as well if there are multiple User Authentication Gates in place.
So with Authentication Gates you are able to implement gatekeepers after the user actually already successfully authenticated with either OAuth or Username + Password combination for any connection as long as you are not using an own HTTP Authentication Type (so any other than Basic or Bearer) for your inbound integration. For the latter you could already directly implement the solution from below within your custom HTTP Authentication Type.
What User Authentication Gates technically are
Basically User Authentication Gates are just Script Includes with the Suffix UserAuthenticationGate
which are initialized with the user name, the roles of the user and a user type ("Normal"
, "LDAP"
, "OAuth"
, "OpenID"
, "SNC"
) implementing the function isAllowed
returning either true
or false
.
To be effective, the User Authentication Gate Script Include must be active and it must be listed (comma-separated list) in the System Property glide.user.authentication.gates
without the Suffix UserAuthenticationGate.
The stub / boiler plate for an User Authentication Gate looks as follows:var MyCustomUserAuthenticationGate = Class.create();
MyCustomUserAuthenticationGate.prototype = {
allowLogin: true,
initialize: function(user, roles, user_type) {
this.allowLogin = // implement test for gating the user here ...
},
isAllowed: function() {
return this.allowLogin;
},
type: 'MyCustomUserAuthenticationGate'
};
Introducing an arbitrary additional factor (second factor)
As User Authentication Gates are run synchronously within the transaction, we are able to retrieve all information available from the transaction including HTTP Header information as well as connection information.
For example we can obtain the source / remote address (IP address) of the party trying to authenticate.
Based on this idea I implemented an User Authentication Gate that allows to check IP addresses of specified users only. Only if the remote IP address matches one of the listed IP addresses, user access is granted.
As a reminder, the three factor types are:
- Knowledge (something only you know, e.g. a secret like a password)
- Possession (something only you posses, e.g. a hardware token or a removable smartcard)
- A property specific to the identity (something unique about you, e.g. a fingerprint or an integrated TPM chip)
While something like an API Key or an access token is a secret, an IP address typically can only be possed by one system. For this case, we can leave NATs and IP address spoofing aside: For a NAT an attacker would need to be inside the NATed network that we allowed access to our API and IP address spoofing only works one-way as packets are sent but are not received by the attacker.
The Solution
So there is the turnkey-ready solution for you to take away: a Script Include gating access by IP addresses for any user you specify.
Download and Installation
You can download the solution as an update set here.
The installation is as easy as previewing and committing the Update Set.
If you have the ServiceNow Access Control Plugin (see here) enabled, please ensure to backup your System Property glide.user.authentication.gates
and add the original value to the System Properties afterwards with a prefixed comma (,).
Configuring the solution
After applying the Update Set, there is a System Property called security.list.forced_ip_whitelist_users
, which you can edit with the security_admin role.
This property contains a JSON object with the user IDs as the key and an array of strings as the key’s value. Each String contains exactly one IP address.
Masking IP Addresses
While technically it is possible to “mask” IP addresses, as you can see for integration.user1
in the example, I don’t advise to do so. The suggested solution is to explicitly list all necessary IP addresses as allowing all IP addresses of a range introduces uncertainty.
Basically the IP address is handled as a string literal.
That being said, the asterisk is working like a “begins with” filter: the IP address of the calling party has to start with the string defined up until the asterisk. The consequence is that a String only consisting of an asterisk ("*"
) practically allows logins from all IP addresses.
Having an empty array has the same effect as an asterisk-only string!
Edge case — blocking a user
To block a user, it is sufficient to have an array which only has one string with something that isn’t an IP Address like ["void"]
.
Logging
All login attempts for IP whitelisted users are logged in the system log with the source IPAddressWhitelistUserAuthenticationGate
. For access attempts from masked IP address ranges, the specifically used IP address is included in the log as well.
Additionally, all failed login attempts emit the event “login.failed” with the user name in parm1 (as in the out-of-the-box behaviour) and additionally the blocked IP address in parm2.
Therefor it should be easy for you to monitor, escalate and build SIEM Test Cases on the logging.
Thinking further
As you can have multiple Authentication Gates (just comma-separate them in the System Property glide.user.authentication.gates
), you could implement further gates e.g. to check certain custom HTTP Headers which could include cryptographic information (e.g. from a TPM or Smartcard).
That way you could implement even further factors. For that be inspired by my solution.
Just remember to not include the Suffix UserAuthenticationGate
in the property!
Additional Measures to secure inbound connections
- mark integration users as “Web services access only”
- never make use of the flag “Internal Integration User”
(your integration most possibly is no internal integration as in the sense of ServiceNow) - use long, random passwords
(go with at least 32 characters, randomly generated) - never grant the admin role to integration user
- never grant the user_admin role to integration users
(as they could change themselves then)