Read-Only GitHub for Non-Engineers
Establishes the least-privilege baseline for the GitHub MCP connector on the agent channel.
- Direction
- ingress
- Rego package
github.ingress.role_gate_writes_engineering- App
- github
- Bundles
- soc2sox
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:298231c9480137bc89f70d5708ca8650a20bd01da91a16475c2d6c73c880f425
githubrole-gate-writesingresssoc2sox
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny gated writes, allow otherwise
Package: github.ingress.role_gate_writes_engineering
What it does
Establishes the least-privilege baseline for the GitHub MCP connector on the
agent channel. It denies the enumerated write and destructive GitHub tools
(the suffix list below) unless the caller's IdP groups include
engineering, while leaving all read tools
(get_*, list_*, search_*, and the consolidated *_read tools) available
to everyone.
At ingress — before the call reaches the GitHub MCP server, so a blocked write never executes and produces no side effect — the policy:
- Allows any tool for callers in the
engineeringgroup. Membership is read from the IdP-issued JWT viaobject.get(input.subject, "claims", {})thengroups. Group matching is case-insensitive (Engineering==engineering). - Allows any non-gated tool for everyone. Read tools are never gated, so every caller keeps read access to code, issues, pull requests, and search.
- Denies the enumerated write/destructive tools for everyone else. The
default-deny takes effect when the caller is not in
engineeringand the tool matches a gated write suffix.
Group membership fails closed: a caller with no subject, no claims, or
no groups claim resolves to an empty group set and is therefore treated as
read-only.
Compliance alignment
- SOC 2 CC6.1 — logical access security over protected assets: restricts who can mutate source code and change tooling on the agent channel (family PF-12, Enforceable).
- SOC 2 CC6.3 — role-based access, least privilege, and separation of
duties: write capability is bound to the
engineeringIdP group; everyone else is read-only (PF-12, Enforceable). - SOC 2 CC6.2 — authorize/de-provision credentials: gating on live IdP
group claims means a de-provisioned or reassigned user loses write access as
soon as their token stops asserting
engineering(PF-12, Partial). - SOC 2 PI1.2 — inputs complete, accurate, and authorized: only authorized (engineering) principals may create or update repository content (PF-12, Partial).
- SOX ITGC — access to programs and data — least-privilege access to the systems that hold financial-application source code and CI change tooling (PF-12, Enforceable).
- SOX SoD (COSO Principle 10) — supports separation of initiate-vs-approve: the read-only default keeps non-engineers out of the change path (PF-12, Partial; pair with the merge/approval policy for the approval half).
Why ingress and not egress
Writes have permanent, externally visible side effects — a pushed file, a created branch, a triggered CI run, a filed issue visible to every repo watcher. Egress can only mask the response after the mutation already happened. Denying at ingress is the only way to actually prevent the unauthorized write.
Tool name matching
The gateway prefixes tool names with the configured MCP server name (e.g.
github-mcp-create_or_update_file), and that prefix is not standardized, so
the policy matches on the suffix of the lower-cased input.resource.name
for portability. Verify the exact names your gateway sends with the dump-input
debug technique before relying on this in production.
Gated write/destructive suffixes (official github/github-mcp-server):
create_or_update_file, push_files, delete_file, create_branch,
create_repository, fork_repository, issue_write, sub_issue_write,
add_issue_comment, create_pull_request, update_pull_request,
update_pull_request_branch, pull_request_review_write,
add_comment_to_pending_review, add_reply_to_pull_request_comment,
discussion_comment_write,
label_write, projects_write, create_gist,
update_gist, actions_run_trigger, assign_copilot_to_issue,
create_pull_request_with_copilot.
Also gated (archived community @modelcontextprotocol/server-github):
create_issue, update_issue, create_pull_request_review — the archived
server uses granular one-tool-per-operation names instead of the consolidated
*_write tools, so these are included to keep the baseline holding on
brownfield installs.
Read tools are identified only by exclusion: anything not matching a gated
suffix is allowed for everyone. Because matching is by suffix, a single rule on
issue_write also covers sub_issue_write; both are listed explicitly for
documentation.
Argument shape
This policy inspects only the principal (IdP groups) and the tool name — it
reads no tool arguments. That makes it robust against argument-key tricks:
there is no owner/repo/method/content field to spoof, and method-
multiplexed tools (issue_write, pull_request_review_write, label_write,
projects_write, sub_issue_write) are gated at the tool level, so every
method they multiplex is denied for non-engineers regardless of the method
argument.
Examples
Allowed — read tool, any caller
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "github-mcp-get_file_contents", "type": "tool" },
"payload": {
"name": "github-mcp-get_file_contents",
"args": { "owner": "acme", "repo": "web", "path": "README.md" }
}
}
}
allow = true, no reason. (No subject/groups required for reads.)
Allowed — write tool, engineering caller
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "github-mcp-push_files", "type": "tool" },
"subject": { "sub": "google-apps|dev@acme.ai", "claims": { "groups": ["engineering"] } },
"payload": { "name": "github-mcp-push_files", "args": { "owner": "acme", "repo": "web" } }
}
}
allow = true.
Denied — write tool, non-engineering (or unauthenticated) caller
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "github-mcp-create_or_update_file", "type": "tool" },
"subject": { "sub": "google-apps|sales@acme.ai", "claims": { "groups": ["sales"] } },
"payload": { "name": "github-mcp-create_or_update_file", "args": { "owner": "acme", "repo": "web" } }
}
}
allow = false, reason = "Access denied: \github-mcp-create_or_update_file` is a write or destructive GitHub tool restricted to the `engineering` IdP group. …"`.
Composition
This is the connector's baseline posture. Layer these companions on top — the gateway ANDs all attached ingress policies, so each narrows further:
require-human-approval-merge(PF-15): deniesmerge_pull_requestand approvingpull_request_review_writesubmissions even for engineers. This baseline deliberately does not gatemerge_pull_request— the merge policy owns that concern.fence-scopes-org-allowlist(PF-23 / anti-exfil): confinesowner/repoto the company org so an engineer cannot push to a personal or third-party repo with their token.redact-secrets-egress(PF-02): redacts credentials from file-content and search responses on the read path that this policy leaves open — including the sensitive secret-scanning reads.- A public-exposure policy (PF-27) forcing
private:trueoncreate_repositoryand denying publiccreate_gist/personal-namespacefork_repository.
Known limitations
engineeringis a placeholder. Replace it with your own IdP's group name at import time — group names are placeholders, not shipped defaults.groupsclaim must be an array of strings. The policy iteratesinput.subject.claims.groupsas an array (the common Auth0/Okta/Entra shape). An IdP that encodes groups as a single space- or comma-delimited string, or under a namespaced claim (e.g.https://acme.com/groups), will not match — the caller would be treated as read-only. Adaptis_engineeringto your claim shape; confirm the actual shape with the dump-input technique ordtwo-list-claims.- Only enumerated write suffixes are gated. Other mutating tools not in the
list —
merge_pull_request(owned by the merge policy), notification writes (dismiss_notification,mark_all_notifications_read,manage_notification_subscription,manage_repository_notification_subscription),star_repository/unstar_repository,request_copilot_review— are not gated by this policy and pass through for non-engineers. (The notification-subscription and star tools are deliberately treated as low-risk and left ungated;request_copilot_reviewonly requests a Copilot review and does not hand a code-writing task to an autonomous agent the way the gatedassign_copilot_to_issue/create_pull_request_with_copilotdo. The red-team pass added the PR-content/PR-review writesupdate_pull_request_branch,add_comment_to_pending_review, andadd_reply_to_pull_request_comment, and the public/watcher-visiblediscussion_comment_write, to the gated set above after finding they slipped through.) Add their suffixes togated_write_suffixesif your posture requires it, or rely on the sibling policies that own them.mark_all_notifications_readends in_readbut is a write; it is intentionally left ungated here (it is not in the enumerated set). - Tool inventory drifts. GitHub adds toolset tools over time; a newly
introduced write tool with a suffix not on the list would be allowed for
everyone until added. This is the blocklist trade-off; pair with a
default-deny-unknown-tools(PF-28) allowlist policy if you need drift-proof coverage. - Placeholder-claim trust boundary. Group membership is only as trustworthy
as the IdP that issued the JWT and the gateway's
jwt_audiencevalidation.is_admin,teams, and the internaluserclaim are stripped by the gateway and are deliberately not used here.
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 github.ingress.role_gate_writes_engineering
# Least-privilege baseline for the GitHub MCP connector.
# Deny-by-default: a request is permitted only by an explicit allow rule below.
default allow := false
# --- Gated write / destructive tool suffixes (official github/github-mcp-server) ---
# The gateway prefixes tool names with the configured MCP server name
# (e.g. `github-mcp-create_or_update_file`), so we match by suffix for
# portability across server naming conventions.
gated_write_suffixes := {
"create_or_update_file",
"push_files",
"delete_file",
"create_branch",
"create_repository",
"fork_repository",
"issue_write", # a suffix match on this also covers `sub_issue_write`
"sub_issue_write",
"add_issue_comment",
"create_pull_request",
"update_pull_request",
"update_pull_request_branch", # sibling of update_pull_request; distinct suffix, must be listed separately
"pull_request_review_write",
"add_comment_to_pending_review", # PR-review write not covered by any other suffix
"add_reply_to_pull_request_comment", # PR review-comment write; `add_issue_comment` suffix does not cover it
"discussion_comment_write", # public/watcher-visible content write; parity with add_issue_comment (red-team addition)
"label_write",
"projects_write",
"create_gist",
"update_gist",
"actions_run_trigger",
"assign_copilot_to_issue",
"create_pull_request_with_copilot",
}
# --- Archived community server (@modelcontextprotocol/server-github) write names ---
# That server uses granular one-tool-per-operation names instead of the
# consolidated `*_write` tools; gated here so the baseline holds on brownfield
# installs.
archived_write_suffixes := {
"create_issue",
"update_issue",
"create_pull_request_review",
}
# A tool is gated if its lower-cased name ends with any gated suffix.
is_gated_write_tool if {
some suffix in gated_write_suffixes
endswith(lower(input.resource.name), suffix)
}
is_gated_write_tool if {
some suffix in archived_write_suffixes
endswith(lower(input.resource.name), suffix)
}
# --- Identity: engineering group membership ---
# Group membership is read from the IdP-issued JWT claims. Fails closed: a
# missing `subject`, missing `claims`, or missing `groups` yields no match, so a
# caller with no groups claim is treated as read-only. Matching is
# case-insensitive. NOTE: `engineering` is a placeholder — replace it with your
# IdP's group name at import time.
is_engineering if {
claims := object.get(input.subject, "claims", {})
some group in object.get(claims, "groups", [])
lower(group) == "engineering"
}
# --- Allow rules ---
# Engineering group members may call any GitHub tool.
allow if is_engineering
# Everyone may call any tool that is not a gated write/destructive tool. This
# leaves all read tools (get_*, list_*, search_*, *_read) available to all
# callers.
allow if not is_gated_write_tool
# --- Deny reason ---
# The only deny condition is a gated write by a non-engineering caller, so a
# single inline reason suffices.
reason := sprintf("Access denied: `%s` is a write or destructive GitHub tool restricted to the `engineering` IdP group. Read tools (get_*, list_*, search_*, *_read) remain available to everyone. Ask an admin to add you to the `engineering` group, or contact your platform team if this is a false positive.", [input.resource.name]) if not allow 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