Freeze ServiceNow Identity Plane
Freezes the identity-and-access mutation surface of the ServiceNow MCP server. The policy denies, by tool-name suffix:
- Direction
- ingress
- Rego package
servicenow.ingress.freeze_identity_plane- App
- servicenow
- Bundle
- soc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:6990ec2afb77e7266fb9dce93d87d4ab478add739ab98a0ec1db21706d60cc15
servicenowfreeze-identity-planeingressidentitygroupssoc2iso27001-nist
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny identity-and-access mutations, allow everything else
Package: servicenow.ingress.freeze_identity_plane
What it does
Freezes the identity-and-access mutation surface of the ServiceNow MCP server. The policy denies, by tool-name suffix:
*create_user/*update_user— user record creation and property changes*create_group/*update_group— group creation and property changes*add_group_members/*remove_group_members— group membership changes
All other tools pass through unchanged. The denied tools are exempt only for
callers whose IdP groups claim contains the placeholder group identity-admins,
read fail-closed from input.subject.claims.groups — a missing subject, missing
claims, or a missing/empty/non-array groups claim means no exemption and the
mutation is denied.
The escalation risk is concrete in ServiceNow. Group membership drives ACL
evaluation across the entire platform: a role granted to a group flows to every
member, so add_group_members is a privilege-escalation primitive — a
prompt-injected agent that can call it can grant itself (or its caller)
admin-equivalent reach without touching a single role record. create_user /
update_user can mint or re-home an account; create_group / update_group
can stand up or repurpose an access-bearing group. Freezing these at ingress
means the mutation never reaches the instance.
Read-side identity tools stay open by design — get_user, list_users,
list_groups are not matched here. Recon is unaffected so agents operate
without spurious denials; PII exposure on those read paths is the job of the
companion fence-sensitive-tables / PII-redaction policies, not this one.
Compliance alignment
- SOC 2 CC6.1 — supports logical access security over protected information
assets: the identity- and group-mutation tools are identity-gated at the MCP
boundary, so a non-privileged caller (or an injected agent acting as one)
cannot provision users or change group membership on the agent channel.
SOC 2 CC6.3 — supports role-based access and least privilege: the ability
to mutate the ServiceNow identity plane is tied to membership in the
placeholder
identity-adminsIdP group, and removing that group in the IdP removes agent access on the next call. - ISO 27001 A.8.2 / NIST 800-53 AC-6(9), AC-6(10) — supports privileged access restriction: user provisioning and group-membership changes are privileged access-management operations, and this policy prevents non-privileged callers (and injected agents acting as them) from executing those privileged functions on the agent channel. This is the ServiceNow instance of policy family PF-13; the coverage matrix maps A.8.2 / AC-6(9)(10) to PF-13 as an enforceable control on the MCP path, and lists AC-6 → PF-13 again under the public-sector (FedRAMP/CJIS) baseline.
No HIPAA / PCI DSS / GDPR-CCPA / SOX bundle tag is claimed: the coverage matrix cites PF-13 under the SOC 2 CC6.x logical-access criteria and the ISO 27001 / NIST rows (and the public-sector baseline), so this policy supports alignment with those controls and does not assert coverage under the other frameworks.
Tool name matching
The policy matches by suffix on the lowercased, whitespace-trimmed
input.resource.name:
create_user, update_user, create_group, update_group,
add_group_members, remove_group_members
These are the verb_noun snake_case tool names from the
echelon-ai-labs/servicenow-mcp server — the de facto community vocabulary that
most wrappers copy (michaelbuckner uses the same create_*/update_* shapes).
The DTwo gateway prefixes tool names with the configured MCP server name, and
that prefix is not standardized. Because each suffix is the trailing tool token
itself (no leading separator), endswith matches whether the gateway joins the
prefix with a hyphen (servicenow-create_user), an underscore, a dot, or forwards
the bare name (create_user) — the suffix sits at the end of the string in every
case. Matching is case-insensitive. Verify the exact names your gateway sends
with the dump-input debug technique before relying on this in production.
Argument shape
The decision uses only the tool name (input.resource.name) and the caller's
identity (input.subject.claims.groups). Tool arguments are not inspected,
so no argument-shape drift can bypass the deny. Identity is read with
object.get chains: a missing subject, missing claims, or missing groups
claim yields an empty group list, which fails closed — the caller is not exempt
and the mutation is denied. The groups claim must be a JSON array of strings; a
non-array shape (string, object, number) fails closed.
Examples
Allowed — read tool, no identity required
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "servicenow-list_users", "type": "tool" },
"payload": { "name": "servicenow-list_users", "args": { "limit": 20 } }
}
}
allow = true, no reason.
Denied — membership mutation by a non-admin caller
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "servicenow-add_group_members", "type": "tool" },
"subject": { "sub": "auth0|agent-user", "claims": { "groups": ["service_desk"] } },
"payload": {
"name": "servicenow-add_group_members",
"args": { "group": "admins", "members": ["agent-user"] }
}
}
}
allow = false, reason = "ServiceNow identity and group changes are frozen on the agent path (...)".
Allowed — same mutation by an identity-admins member
The same call with "groups": ["identity-admins"] in input.subject.claims
returns allow = true.
Composition
This policy is single-purpose. Useful companions:
default-deny-unknown-tools(PF-28, already in this app dir) — mandatory for ServiceNow because the official MCP Server Console publishes instance-defined tool names (Subflows/Actions/Scripted REST APIs) that could perform identity mutations under names this suffix list cannot anticipate. The allowlist catches drift; this policy freezes the known community tools.- A PF-22 escape-hatch deny — michaelbuckner's
perform_query/natural_language_updateand any generic Table-API tool can writesys_user/sys_user_grmemberdirectly, bypassing every per-tool rule here. Without it, this policy's guarantee holds only for the named tools. role-gate-writes(PF-12) — the baseline write gate for everything else on the ServiceNow surface.fence-sensitive-tables/ PII-redaction egress — owns read-side identity exposure (get_user,list_users), which this policy deliberately leaves open.
Known limitations
- Generic-query and NL-write tools bypass this policy. michaelbuckner's
perform_query,get_record,search_records, and especiallynatural_language_updatetake a table name (or free text) and reachsys_user/sys_user_group/sys_user_grmemberwithout matching any suffix here. Deploy a PF-22 escape-hatch deny and the PF-28 allowlist alongside this one. - Official-server tool names are instance-defined. The ServiceNow MCP Server Console derives tool names from the admin-published skill/subflow/API name, so an identity-mutating Subflow could carry any name. Suffix matching cannot cover that surface — rely on PF-28 default-deny-unknown-tools for the official server and add exact matches per tenant once the published tool list is known.
- Group names are placeholders — replace
identity-adminswith your IdP's group name at import time. The match is an exact, case-sensitive string comparison against entries of thegroupsclaim;Identity-Adminsdoes not matchidentity-admins, and a lookalike likeidentity-admins-viewersdoes not match either (the comparison is==, not substring/prefix). - The
groupsclaim must be an array of strings. If your IdP emits a single string or a namespaced custom claim (e.g.https://acme.com/groups), adjustcaller_groupsin the Rego. The exemption is guarded byis_array, so every non-array shape fails closed (deny) — including an object-shaped claim such as{"role": "identity-admins"}, whose values would otherwise have been iterated bysome group in caller_groupsand spoofed the admin exemption. - Tool-name normalization. The suffixes are the server's
verb_nounsnake_case tokens, so any gateway or SDK that rewrites the separator defeats theendswithmatch. Two distinct rewrites both slip through: separator replacement — kebab-case (create-userinstead ofcreate_user) — and separator collapse — camelCase (createUser,addGroupMembers), where the_disappears entirely solower("...createUser")=...createuserends with none of the snake_case suffixes. DTwo's gateway forwards the upstream tool name verbatim (snake_case), so this does not arise on the DTwo path, but some MCP SDKs auto-camelCase tool names. Verify with the dump-input debug technique; if your gateway rewrites separators, add the rewritten forms (create-user,createuser, …) toidentity_mutation_suffixes. - Reordered / non-
verb_nountool vocabularies bypass the suffix list. The suffixes assume the echelon/michaelbucknerverb_nounconvention (create_user). Servers that invert the token order — LokiMCPUniverse ships anoun_verbServiceNow server (incident_create), so its identity tools would beuser_create,user_update,group_create,group_update, and a membership tool such asgroup_member_add— end with none of these suffixes, soservicenow-user_createpasses through and the mutation reaches the instance. This is not a gateway rewrite (DTwo forwards the upstream name verbatim); it is a genuinely different upstream tool vocabulary. Verify your server's actual names with the dump-input debug technique, and for a noun_verb server add the reordered forms (user_create,user_update,group_create,group_update,group_member_add,group_member_remove, …) toidentity_mutation_suffixes, or rely on the PF-28 default-deny-unknown-tools allowlist to catch the un-anticipated names. - Reads stay open by design.
get_user,list_users, andlist_groupsare not gated here — recon and read-side PII belong to the fence/redaction companions. If directory recon itself is a concern, add a separate read-gating policy rather than widening this one. - A request with no resolvable tool name passes through. This is a blocklist
keyed on
input.resource.name: a missing or empty name matches no suffix and is allowed. The gateway reliably populatesresource.nameontool_pre_invoke, so this is inherent blocklist semantics; if you need fail-closed-on-unknown, deploy a PF-28 default-deny allowlist policy instead of (or alongside) this one.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package servicenow.ingress.freeze_identity_plane
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# Placeholder IdP group permitted to perform identity-plane mutations.
# Replace "identity-admins" with your IdP's group name at import time.
identity_admin_group := "identity-admins"
# Lowercased, whitespace-trimmed tool name. The gateway prefixes tool names
# with the configured MCP server name, so matching is case-insensitive and
# suffix-based to stay portable across server names. trim_space so a trailing
# space/newline in the tool name cannot defeat the endswith suffix match.
tool_name := trim_space(lower(object.get(object.get(input, "resource", {}), "name", "")))
# Identity-and-access mutation tools on the echelon-ai-labs/servicenow-mcp
# server (verb_noun snake_case, the de facto community vocabulary). Each suffix
# is the trailing tool token itself (no leading separator), so endswith matches
# a bare name (`create_user`) and any prefix/separator the gateway prepends
# (`servicenow-create_user`, `servicenow.create_user`, `servicenowcreate_user`).
# Read-side identity tools (get_user, list_users, list_groups) are deliberately
# absent — read exposure belongs to the fence/redaction companions.
identity_mutation_suffixes := [
"create_user",
"update_user",
"create_group",
"update_group",
"add_group_members",
"remove_group_members",
]
is_identity_mutation if {
some suffix in identity_mutation_suffixes
endswith(tool_name, suffix)
}
# --- Identity (fail closed) ---
# Missing subject, missing claims, a missing groups claim, or a groups claim
# that is not an array all yield "not an identity admin" — mutations then deny.
caller_groups := object.get(object.get(object.get(input, "subject", {}), "claims", {}), "groups", [])
# Guard on is_array. Without it, `some group in caller_groups` iterates the
# *values* of an object-shaped groups claim, so a claim like
# {"role": "identity-admins"} would spoof the exemption and fail OPEN. Requiring
# an array makes every non-array shape (string, object, number) fail closed —
# matching the documented "must be an array of strings" contract.
caller_is_identity_admin if {
is_array(caller_groups)
some group in caller_groups
group == identity_admin_group
}
# Allow any tool that is not an identity-plane mutation (reads such as
# list_users, get_user, list_groups stay open for recon-free operation).
allow if {
not is_identity_mutation
}
# Allow identity-plane mutations only for members of the identity-admin group.
allow if {
is_identity_mutation
caller_is_identity_admin
}
reasons contains msg if {
is_identity_mutation
not caller_is_identity_admin
msg := sprintf("ServiceNow identity and group changes are frozen on the agent path — group membership drives ServiceNow ACLs, so user and group mutations are privilege-escalation primitives. Make these changes in the ServiceNow UI, or ask your identity admin to add you to the '%s' IdP group if your role requires making them through the gateway. Contact your InfoSec team if this looks like a false positive.", [identity_admin_group])
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa