Fence GitHub Access to the Company Org Allowlist
Denies any GitHub tool call whose arguments.owner (read from input.payload.args.
- Direction
- ingress
- Rego package
github.ingress.fence_scopes_org_allowlist- App
- github
- Bundle
- soc2
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:53fe009c913de7ae748645f9646b4cd71ce0970e35b81d5e4869dbd3537adc8f
githubfence-sensitive-scopesorg-allowlistanti-exfilingresssoc2
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny when owner is present and off-list; allow otherwise
Package: github.ingress.fence_scopes_org_allowlist
What it does
Denies any GitHub tool call whose arguments.owner (read from
input.payload.args.owner) is not one of the logins in a tenant-configured
company-org allowlist. The allowlist is a Rego array constant
(company_orgs) that each tenant pins to its own org login(s) at import
time.
Because nearly every GitHub tool carries an owner + repo pair, this one
predicate closes two exfiltration paths at once — but only for tools where the
owner argument names the repository actually being read from or written to:
- Read exfiltration — pulling a third-party or personal private repository
into agent context via owner-bearing reads such as
get_file_contents,get_repository_tree, andpull_request_read(method: get_diff/get_files). It does not coversearch_code, which takes only aqquery string and noowner: arepo:owner/name-scoped query reaches a third-party repo unfenced (see Known limitations). - Write exfiltration — pushing the user's token-authorized content into an
attacker- or personally-owned repository via
push_filesandcreate_or_update_file, whoseowneris the write destination. It does not coverfork_repository: there theownerargument is the fork source, so forking a sanctioned company repo into a personal namespace (the actual IP-exfil direction) is allowed by this policy — see the public-exposure companion and Known limitations.
Calls with no resolvable owner argument (e.g. get_me, or search_code
invoked with only a q string) are allowed, because those are already scoped
by the OAuth grant — there is no owner to fence against. Any call where
owner is present and off-list is denied. Beware that "ownerless" also
covers a few writes — create_gist/update_gist and create_repository
(destination keyed on organization, not owner) — which therefore pass this
fence; the public-exposure companion is what covers them (see Known
limitations). The deny reason names the
offending owner and points the agent back to a sanctioned company org.
The check runs at ingress, before the call reaches the GitHub MCP server, so a blocked read never enters agent context and a blocked write never executes.
Compliance alignment
- SOC 2 C1.1 — supports the requirement to identify and protect confidential information by keeping source-code IP inside the sanctioned org boundary on the agent channel. P4.1 — supports limiting use of information to identified, sanctioned purposes (the company org).
- GDPR Art. 5(1)(b) — supports purpose limitation: repository content the agent can reach is confined to the org the processing is authorized for.
- CCPA/CPRA §1798.121 — supports the right to limit sensitive information by preventing the agent channel from reading or writing outside the company's own namespace.
This is a PF-23 (fence-sensitive-scopes) ingress policy: a per-tenant allowlist keyed to the org boundary rather than to IdP groups.
Tool name matching
This policy is not matched by tool name. The fence key is the presence and
value of the owner argument, which almost every GitHub tool carries
(get_file_contents, push_files, fork_repository, create_pull_request,
merge_pull_request, pull_request_read, and so on — see the GitHub landscape
note). Matching on owner rather than enumerating tool names keeps the policy
robust as new tools are added and portable across the official
(github/github-mcp-server) and archived community servers, which share the
owner argument name even where tool names diverge.
The consequence: attach this policy to a GitHub-only pipeline. If it is
attached to a gateway fronting other MCP servers, a non-GitHub tool that
happens to expose an owner argument would also be fenced. See Known
limitations.
Argument shape
input.payload.args.owner— the repository owner login. GitHub logins are case-insensitive, so the policy lower-cases and trims the value before comparing it tocompany_orgs(whose entries must be lowercase). A present-but-non-stringowner(an unusual, malformed shape) is treated as off-list and denied — the policy fails closed rather than open.- Absent or empty-string
owner→ treated as an ownerless call and allowed.
The GitHub MCP tool argument is arguments.owner at the MCP layer; the DTwo
gateway surfaces it to the policy as input.payload.args.owner.
Examples
Allowed — owner is a sanctioned company org
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "github-mcp-get_file_contents", "type": "tool" },
"payload": {
"name": "github-mcp-get_file_contents",
"args": { "owner": "acme-inc", "repo": "billing", "path": "README.md" }
}
}
}
allow = true, no reason.
Allowed — ownerless call (already OAuth-scoped)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "github-mcp-search_code", "type": "tool" },
"payload": {
"name": "github-mcp-search_code",
"args": { "q": "org:acme-inc AKIA" }
}
}
}
allow = true, no reason.
Denied — write into an off-list (personal/attacker) owner
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "github-mcp-push_files", "type": "tool" },
"payload": {
"name": "github-mcp-push_files",
"args": { "owner": "evil-corp", "repo": "loot", "branch": "main", "files": [] }
}
}
}
allow = false, reason names evil-corp and points back to a sanctioned org.
Composition
This policy is single-purpose. Recommended companions (see the GitHub landscape note's candidate list):
- The role-gate policy (read-only GitHub for non-engineers) — closes the ownerless read surface this policy cannot reach.
- The public-exposure policy — forces
create_repositoryprivate, denies publiccreate_gist, and denies personal-namespacefork_repository. - An egress secret-hygiene / IP-redaction policy on
get_file_contents,search_code, andpull_request_readresponses.
Known limitations
- Ownerless read/list tools still read across the OAuth grant. Tools that
take no
owner—search_codewith only aqstring,search_repositories,list_notifications,get_me— remain readable across every repo the OAuth grant reaches, including the user's personal namespace. This policy cannot fence them. Pair it with the role-gate and public-exposure policies to cover that surface. - Ownerless writes also escape the fence — including exfiltration paths.
"Ownerless" is not a synonym for "read-only." Some write tools carry no
ownerargument and so are allowed here even though they move content outward:create_gist/update_gist(content can land in a public gist, a direct exfil channel) andcreate_repository(the destination is theorganizationargument, notowner, so a new repo can be created in an off-list namespace). The gist argument schema is unverified in the landscape note (gists are owned by the authenticated user and expose noowner), so treat that as an assumption to confirm against your livetools/list. This fence does not stop these; the public-exposure companion (forcescreate_repositoryprivate, denies publiccreate_gist) is what covers them. Note that a follow-uppush_filesinto an off-list repo is caught here, becausepush_filescarriesowner— so the residual is the ownerless write tools themselves, not token-powered pushes. - Attach to a GitHub-only pipeline. The fence keys on the
ownerargument, not on the tool name. On a mixed gateway, any other server's tool that exposes anownerargument would also be fenced. If you must share the pipeline, add a tool-name guard. q-embedded owners are not parsed. Asearch_codequery likeq: "repo:torvalds/linux ..."reaches a third-party repo through the query string, not theownerargument, so this policy does not catch it. Fence query strings with a separate rule if that path matters in your environment.fork_repositoryfences the source, not the destination. Itsownerargument is the repo being forked from. Forking a sanctioned company repo into a personal namespace (organizationunset or set to a personal login) is the real IP-exfil direction and is allowed by this policy, because the source owner is on-list. Close that path with the public-exposure companion, which denies personal-namespace forks; do not rely on this fence for it.- Allowlist is pinned per tenant.
company_orgsships with placeholder logins (acme-inc,acme-labs). Replace them with your real org login(s) at import time; a stale or empty allowlist denies every owner-bearing call. - Argument casing is version-dependent for other fields (e.g. the PR-number
parameter), but
owneris stable across the official and archived servers per the landscape note. Verify against your livetools/listbefore relying on any other argument.
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.fence_scopes_org_allowlist
# Deny-by-default: a call is permitted only by an explicit allow rule below.
default allow := false
# --- Per-tenant configuration ------------------------------------------------
# Company GitHub org logins the agent channel is permitted to touch. Pin this
# to YOUR org login(s) at import time. GitHub logins are case-insensitive, so
# keep every entry lowercase — the policy lower-cases the incoming `owner`
# before comparing. A stale or empty allowlist denies every owner-bearing call.
company_orgs := {
"acme-inc",
"acme-labs",
}
# --- Owner resolution --------------------------------------------------------
# Raw owner argument; "" when the argument is absent.
owner_arg := object.get(input.payload.args, "owner", "")
# The call carries an owner when the argument is present and non-empty.
# (A non-string owner, e.g. a number or object, is still "present" here and is
# handled as off-list below — the policy fails closed on malformed shapes.)
has_owner if {
owner_arg != ""
}
# Canonical owner login for allowlist comparison: trimmed + lower-cased.
# Only defined when `owner` is a string; a present-but-non-string owner leaves
# this undefined, so it never matches the allowlist and is denied.
canonical_owner := lower(trim_space(owner_arg)) if {
is_string(owner_arg)
}
# True only when the resolved owner is a sanctioned company org.
owner_on_allowlist if {
company_orgs[canonical_owner]
}
# --- Allow rules -------------------------------------------------------------
# Ownerless calls are already scoped by the OAuth grant (e.g. get_me, or
# search_code with only a `q` string) — nothing to fence.
allow if {
not has_owner
}
# Owner is present and resolves to a sanctioned company org.
allow if {
has_owner
owner_on_allowlist
}
# --- Deny reason -------------------------------------------------------------
# Names the offending owner and points the agent back to the sanctioned org.
reasons contains msg if {
has_owner
not owner_on_allowlist
msg := sprintf("GitHub access is fenced to your company org allowlist. Owner '%v' is not a sanctioned org, so this call is blocked to keep source code inside the company boundary. Re-target the call at a repository your company org owns, or ask your InfoSec team to add '%v' to the allowlist if it is legitimately in scope.", [owner_arg, owner_arg])
}
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)
Used in these guides
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