GitHub
Proxy for the GitHub REST API. Each managed resource exposes seven MCP
tools mapped onto specific api.github.com routes, with per-route OAuth
scopes (gh:repo:read, gh:repo:write, gh:issues:read, gh:issues:write,
gh:workflows:write).
Overview
Pick an auth mode at install time:
idp_passthrough(recommended) — each caller's own GitHub OAuth token is forwarded upstream. The agent acts as the user; their GitHub permissions apply on top of PBAC policy. Requires a GitHub IdP registered on the AS (one OAuth App per instance).static— a single Personal Access Token (PAT) is forwarded for every caller. Use for unattended automations where the operator owns the GitHub identity and there is no human in the loop. The PAT is stored as a secret reference; it is not typed into a form field directly.
A managed resource is bound to one auth mode. To serve both shapes of
caller, create two managed resources (e.g. github-user and
github-bot). Policy can key off
input.context.rs_context.resource_name to apply different rules.
Setting up GitHub as an IdP
The admin portal's "Connect a resource" wizard inlines this step the
first time you install GitHub with idp_passthrough. The flow is:
- Open GitHub Developer Settings → OAuth Apps → New OAuth App.
- Set Homepage URL to your AS issuer (e.g.
http://localhost:8080). - Set Authorization callback URL to
<issuer>/oauth2/callback— the wizard shows the exact value with a copy button. - Generate a client secret. Copy both the Client ID and Client secret into the wizard.
GitHub OAuth Apps don't have you pre-pick scopes at registration —
scopes are requested at authorize time and approved by the user. The
connector requests read:user, repo, and read:org so callers can
read user metadata, work with both public and private repos they own,
and resolve org memberships for ABAC decisions. Editing the scopes stored
on the managed resource does not change this set, because the AS reads it
from the connector's manifest, so you cannot swap repo for the narrower
public_repo. You can withhold repo. Open the resource in the Admin
Portal, enter repo under Upstream scope control in Excluded
(space-separated), and choose Save excluded scopes. The AS then
removes repo from the GitHub IdP's scopes unless another enabled resource
on that IdP needs it, and stops requesting it at sign-in. In our run
repo dropped out of the scope the AS sent to the IdP. The portal notes that tools that need an excluded
scope fail.
Policy patterns
Unlike Gmail, the GitHub connector does not ship its own Rego
policy, and no bundled policy carries the rules below. They are examples
to add as policy rules yourself. The managed-gateway demo's seed script
writes only the subject attributes they read
(github_subject_attributes), not the rules.
The three rules below are one Rego file in package
pbac.connectors.identos.github, which the policy aggregator walks. The file
passes opa check on OPA 1.10.1. They read only fields the gateway sends:
input.context.rs_context.resource_type, input.context.rs_context.tool_arguments
and input.subject.sub.
package pbac.connectors.identos.github
import rego.v1
_is_github if input.context.rs_context.resource_type == "urn:connector:identos:github"
_args := object.get(input.context.rs_context, "tool_arguments", {})
# A subject with no entry, or a token with no subject, gets no orgs.
default _allowed_orgs := []
_allowed_orgs := orgs if {
orgs := data.github_subject_attributes[input.subject.sub].allowed_orgs
}
# 1. Repository owner allowlist
deny contains msg if {
_is_github
owner := _args.owner
not owner in _allowed_orgs
msg := sprintf("github: %v is not in your allowed orgs", [owner])
}
# 2. Sensitive file path block
deny contains msg if {
_is_github
path := _args.path
_github_sensitive_path(path)
msg := sprintf("github: path %v is blocked", [path])
}
_github_sensitive_path(p) if contains(p, ".env")
_github_sensitive_path(p) if endswith(p, ".key")
_github_sensitive_path(p) if endswith(p, ".pem")
_github_sensitive_path(p) if contains(p, "secrets/")
# 3. Inactive-user kill switch
deny contains msg if {
_is_github
data.github_subject_attributes[input.subject.sub].employment_status != "active"
msg := "github: inactive employee"
}
1. Repository owner allowlist. Every connector route includes an
{owner} path variable, which the gateway passes as tool_arguments.owner
over REST and MCP. Subject attributes seed each developer with the orgs (or
personal namespaces) they are allowed to touch. A subject with no entry, and a
client_credentials token, which has no subject, are denied every call that
names an owner.
2. Sensitive file path block. Fires for get_repo_file and
upsert_repo_file. No subject attributes needed. Customize the predicates to
match your secret-scanning conventions.
3. Inactive-user kill switch. Flipping employment_status for a user
blocks every GitHub tool call within the OPA bundle poll interval (~2 s). No
token revocation is needed, because the deny fires at introspect time. A
subject with no entry is not blocked by this rule.
On a local instance with allowed_orgs set to ["acme"] for the user,
GET /gateway/<name>/repos/acme/demo/contents/README.md returned 200 for the
user, and 403 for owner evil, for path .env, and for a
client_credentials token.
Subject-attribute schema
The reference rules read from data.github_subject_attributes[<sub>].
The shape used by the demo:
{
"allowed_orgs": ["acme", "acme-research"],
"teams": ["eng", "platform"],
"employment_status": "active",
"clearance": "high"
}
The sub claim on a token is a numeric GitHub user ID (not the
login). Find it via https://api.github.com/users/<login> and store
the id field as the key.
Scope model
| Scope | Used for | |
|---|---|---|
| PBAC scopes (internal) | gh:repo:read, gh:repo:write, gh:issues:read, gh:issues:write, gh:workflows:write | Per-route OAuth gating on the managed gateway. A token without gh:repo:write cannot reach upsert_repo_file regardless of ABAC outcome. |
| Upstream OAuth scopes | read:user, repo, read:org | The grants the user is asked to approve when they sign in to GitHub via the AS. |
Splitting reads from writes at the OAuth scope layer keeps the ABAC policy small — most operator concerns (who can write where, what paths are sensitive) compose with the scope gate rather than reimplementing it.
Static (PAT) mode
When the connector is installed with upstream_auth.type = "static",
the wizard asks for a secret reference (pat_env) instead of running
the IdP setup. Create the secret first via the Secrets page or inline
via the form's Add secret button. The PAT must carry whichever
GitHub OAuth scopes the routes will exercise — typically repo and
workflow.
PAT mode forwards the same token for every caller. Subject attributes
still apply (the PBAC sub and scopes come from the token PBAC issued,
not the upstream PAT) but you lose the per-user GitHub permission
layer. Use this mode for automations where the operator owns the
identity and the permission model is enforced entirely in PBAC.
Verifying the install
A direct introspection shows what policy decides for one call. Authenticate
with a confidential client entitled to introspection or uma_protection on
urn:as:introspect, and pass a token issued for the GitHub resource:
curl -s -X POST "$PBAC_AS_URL/introspect" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
--data-urlencode "token=$TOKEN" \
--data-urlencode 'context={
"resource_type": "urn:connector:identos:github",
"resource_name": "github",
"scopes": ["gh:repo:read"],
"tool_arguments": {"owner": "acme", "repo": "demo", "path": ".env"}
}' | jq '{active, inactive_reason}'
No GitHub rule ships, so on a fresh install both a .env path and a
README.md path return "active": true. Once you add a deny rule to package
pbac.connectors.identos.github that matches only the .env path, such as
rule 2 above on its own, that request
returns {"active": false, "inactive_reason": "insufficient_scope"} and the
README.md request still returns "active": true. The response has no allow
field.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Wizard shows "Step 1 · Set up GitHub sign-in" but you want PAT mode | Pick static from the Authentication dropdown in Step 2; the wizard now treats Step 1 as advisory and lets you submit. |
| First-time PKCE login fails with a JSON parse error | Older AS versions sent no Accept header to GitHub's token endpoint. Upgrade — the fix is in IdpFederationService. |
idp_passthrough returns 401 from api.github.com after PBAC allows | The user's GitHub token does not carry the requested scope (e.g. repo for a private repo). Have them re-authorize the OAuth App and accept the missing scope. |
| All tools return 403 even for an authenticated user | If you use rule 1, confirm the subject attributes are keyed by the user's numeric GitHub ID. An entry keyed by login matches no sub, so rule 1 finds no orgs and denies every call. Rule 3 ignores a subject with no entry, so it does not block a user keyed by login. |
Manifest reference
- ID:
identos.github - Version:
1.0.0 - Resource type:
urn:connector:identos:github
Supported auth modes
| Type | Details |
|---|---|
idp_passthrough | requires IdP github |
static | scheme bearer; setup fields: pat_env |
Setup fields
| ID | Label | Default | Secret? | Notes |
|---|---|---|---|---|
upstream_auth.type | Authentication | idp_passthrough | no | idp_passthrough forwards each user's own GitHub OAuth token (recommended). static uses a single shared Personal Access Token (machine identity). |
pat_env | Personal Access Token | — | yes | Pick a secret containing the GitHub PAT. Required only for the static auth mode. / shown when upstream_auth.type == 'static' |
Scopes
| Scope |
|---|
gh:repo:read |
gh:repo:write |
gh:issues:read |
gh:issues:write |
gh:workflows:write |
Routes
| Method | Pattern | Scope | Resource template |
|---|---|---|---|
GET | /repos/{owner}/{repo} | gh:repo:read | github://{{owner}}/{{repo}} |
GET | /repos/{owner}/{repo}/contents/{path:.*} | gh:repo:read | github://{{owner}}/{{repo}} |
PUT | /repos/{owner}/{repo}/contents/{path:.*} | gh:repo:write | github://{{owner}}/{{repo}} |
GET | /repos/{owner}/{repo}/issues | gh:issues:read | github://{{owner}}/{{repo}} |
POST | /repos/{owner}/{repo}/issues | gh:issues:write | github://{{owner}}/{{repo}} |
PATCH | /repos/{owner}/{repo}/issues/{issue_number} | gh:issues:write | github://{{owner}}/{{repo}}/issues/{{issue_number}} |
POST | /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches | gh:workflows:write | github://{{owner}}/{{repo}}/workflows/{{workflow_id}} |
MCP tools
| Name | Scope | Description |
|---|---|---|
get_repo | gh:repo:read | Get metadata for a GitHub repository. |
get_repo_file | gh:repo:read | Read a file or directory listing from a GitHub repository at a given path. |
upsert_repo_file | gh:repo:write | Create or update a file in a GitHub repository. |
list_issues | gh:issues:read | List issues in a GitHub repository. |
create_issue | gh:issues:write | Open a new issue in a GitHub repository. |
update_issue | gh:issues:write | Update fields on an existing GitHub issue. |
dispatch_workflow | gh:workflows:write | Trigger a manual run of a GitHub Actions workflow on a given ref, with optional inputs. |
Send feedback about this page