Skip to content

RFC: ghpool as MCP reverse proxy for GitHub's official MCP server — zero-token GitHub access for AI agents #15

Description

@chaodu-agent

Summary

Evolve ghpool into a credential-swapping MCP reverse proxy in front of GitHub's official hosted MCP server (https://api.githubcopilot.com/mcp/), giving AI agents full GitHub MCP tool access without ever exposing a GitHub credential to the agent.

Revision 2 (2026-07-12): Major update incorporating community review feedback. Key changes: GitHub App installation token as recommended credential backend (resolving PAT-pooling compliance concern); per-agent default-deny policy model with explicit tool + repository allowlists; revised phase gates with Phase 0 compatibility spike; strengthened security boundaries throughout.

Revision 1 (2026-07-12): Earlier drafts proposed ghpool implementing its own MCP tool surface. This revision narrowed scope: ghpool does not define any MCP tools. It proxies GitHub's official MCP server, swaps credentials, and enforces per-agent policy. Zero tool-schema maintenance.

Motivation

Current approaches for AI agents (e.g. OpenAB on ECS/EKS) to interact with GitHub:

Approach Limitation
Fine-grained PAT Single org only; long-lived; can be exfiltrated
Token Vending Machine Agent still receives a short-lived token — can be misused during its lifetime
ghpool as MCP reverse proxy Agent never sees any GitHub credential. Access bounded by per-agent tool + resource policy.

The MCP proxy approach provides the strongest security boundary: the GitHub credential never leaves ghpool, every request is tied to a verified agent identity, both the action and target resource are authorized, the upstream credential is independently least-privileged, and every mutation has a durable audit trail.

Design Goals

  1. No GitHub credential exposed to the agent — the upstream bearer token (GitHub App installation token) is injected by ghpool; agents hold only a ghpool API key or use ambient IAM identity
  2. Default-deny per-agent policy — each agent has an explicit allowlist of tools and repositories; unlisted tools/repos are denied; new upstream tools are denied until explicitly added to policy
  3. Resource-level authorization — toolset categories (X-MCP-Toolsets) provide coarse upstream filtering; ghpool enforces fine-grained per-tool + per-repo policy at the proxy layer
  4. Simple agent authn — per-agent API keys (delivered via existing secrets infra) mapped to policy; optional secretless SigV4 identity proof via a ghp mcp stdio shim for AWS environments
  5. Centralized durable audit — ghpool logs (agent, tool, target repo, allow/deny decision) per JSON-RPC frame; writes require durable audit (fail-closed when audit backend unavailable)
  6. No git/gh CLI dependency — agents only need MCP client capability
  7. No MCP tool implementation in ghpool — GitHub's official server owns the tool schemas; ghpool proxies, authorizes, and audits

Credential Architecture

Recommended: GitHub App Installation Token

AWS Secrets Manager
    └── GitHub App private key (PEM)
            │
            ▼
    ghpool (on startup / on-demand refresh)
        → Sign JWT with App private key
        → POST /app/installations/{id}/access_tokens
        → Receive installation token (1hr TTL)
        → Use as upstream bearer for MCP / REST / GraphQL
        → Auto-refresh before expiry (5min margin)

Why GitHub App over PAT pool:

Aspect PAT Pool GitHub App
Rate limit compliance ⚠️ Aggregating multiple users' buckets may violate GitHub API Terms §H ✅ Installation token rate limits scale legitimately with repos
Attribution Collapses to PAT owner ✅ App name visible in GitHub audit log
Token lifetime Long-lived (unless rotated) Short-lived (1hr auto-expiry)
Scope User-level ✅ Repository-scoped via installation
Rotation Manual ✅ Automatic (new token every hour)

Configuration:

[credentials]
type = "github-app"
app_id = 123456
private_key = "aws:secretsmanager:ghpool/github-app-key"
installation_id = 78901234

# Legacy PAT support (not recommended for production)
# type = "pat-pool"
# [[identities]]
# id = "bot-1"
# token = "aws:secretsmanager:ghpool/pat-bot-1"

Session-Credential Binding

  • Session lifetime must not exceed credential lifetime
  • ghpool tracks installation token expiry; sessions pinned to an expiring token are proactively terminated (client receives 404, re-initializes with fresh token)
  • No silent mid-session credential rotation

Per-Agent Policy Model

Default-Deny Architecture

Every agent has a policy that explicitly lists allowed tools and allowed repositories. Anything not listed is denied.

[[agents]]
id = "agent-a"
key = "aws:secretsmanager:ghpool/agent-a-key"
# OR for IAM-based authn:
# iam_role = "arn:aws:iam::123456789:role/agent-a-task-role"

[agents.policy]
readonly = true
tools = ["issue_read", "get_pull_request", "list_issues", "search_code"]
repos = ["openabdev/ghpool", "openabdev/openab"]

[[agents]]
id = "agent-b"
key = "aws:secretsmanager:ghpool/agent-b-key"

[agents.policy]
readonly = false
tools = ["issue_read", "create_issue", "add_comment", "create_pull_request"]
repos = ["openabdev/*"]  # wildcard: all repos in org

Enforcement Flow

Agent → POST /mcp { tools/call: create_issue, params: { owner, repo, ... } }
    │
    ▼
1. AUTHN — verify X-Ghpool-Key → resolve agent identity
    │         (or SigV4 proof → STS → IAM role → agent mapping)
    │
    ▼
2. PARSE — extract JSON-RPC method + tool name + target resource from params
    │
    ▼
3. AUTHZ — check policy (all must pass):
    │   ├── tool in agent.policy.tools?          → deny if not listed
    │   ├── target repo in agent.policy.repos?   → deny if not listed
    │   ├── is_write_tool AND agent.readonly?    → deny
    │   └── resource resolvable?                 → deny if ambiguous
    │
    ▼
4. FORWARD — inject upstream headers:
    │   ├── Authorization: Bearer <GitHub App installation token>
    │   ├── X-MCP-Toolsets: <from agent policy, coarse filter>
    │   └── X-MCP-Readonly: <from agent policy>
    │
    ▼
5. AUDIT — structured log:
        { agent, session, rpc_id, tool, repo, decision: allow/deny,
          upstream_credential_id (not secret), timestamp, duration }

Policy Principles

  • Default-deny: unlisted tool → denied; unlisted repo → denied
  • New tools are safe: when GitHub adds a new upstream tool, no agent can use it until an admin adds it to that agent's tools list
  • Resource validation: if target resource cannot be resolved from tool arguments, the call is denied (safe fallback)
  • Outbound header allowlist: ghpool strips ALL client-supplied X-MCP-* and Authorization headers; policy-derived headers are injected from scratch — a client cannot widen its own policy

Proposed Architecture

┌───────────────────────────── Private Network / VPC ─────────────────────────────┐
│                                                                                 │
│  ┌──────────────────────────┐        ┌──────────────────────────┐               │
│  │  Agent A (ECS task role) │        │  Agent B (EKS + IRSA)    │               │
│  │  no GitHub credential    │        │  no GitHub credential    │               │
│  │  holds: X-Ghpool-Key     │        │  holds: ambient IAM only │               │
│  └────────────┬─────────────┘        └────────────┬─────────────┘               │
│               │                                   │                             │
│               │   MCP Streamable HTTP             │                             │
│               │   + X-Ghpool-Key header           │                             │
│               │   (or SigV4 proof via ghp shim)   │                             │
│               ▼                                   ▼                             │
│  ┌─────────────────────────────────────────────────────────────┐                │
│  │                    ghpool  (MCP reverse proxy)              │                │
│  │                                                             │                │
│  │  1. AUTHN — verify caller                                   │                │
│  │     API key → agent   (or SigV4 proof → STS)               │───► AWS STS    │
│  │                                                             │                │
│  │  2. AUTHZ — per-agent policy (default-deny)                 │                │
│  │     ┌──────────────────────────────────────────────────┐    │                │
│  │     │ agent-a:                                         │    │                │
│  │     │   tools = [issue_read, get_pull_request]         │    │                │
│  │     │   repos = [openabdev/ghpool, openabdev/openab]   │    │                │
│  │     │   readonly = true                                │    │                │
│  │     │ agent-b:                                         │    │                │
│  │     │   tools = [issue_read, create_issue, add_comment]│    │                │
│  │     │   repos = [openabdev/*]                          │    │                │
│  │     │   readonly = false                               │    │                │
│  │     └──────────────────────────────────────────────────┘    │                │
│  │                                                             │                │
│  │  3. REWRITE headers (from scratch — never forward client's) │                │
│  │     inject: Authorization: Bearer <App installation token>  │                │
│  │             X-MCP-Toolsets: (from agent policy)             │◄── GitHub App  │
│  │             X-MCP-Readonly: (from agent policy)             │    private key │
│  │                                                             │    in AWS SM   │
│  │  4. AUDIT — structured log per JSON-RPC frame               │                │
│  │     (agent, tool, repo, decision, session, timestamp)       │                │
│  └────────────────────────────┬────────────────────────────────┘                │
│                               │                                                 │
└───────────────────────────────┼─────────────────────────────────────────────────┘
                                │  same MCP protocol, GitHub's own tool schemas
                                ▼
                 ┌──────────────────────────────────┐
                 │  https://api.githubcopilot.com/  │
                 │  mcp/            (hosted)        │
                 │  mcp/readonly    (or per-toolset │
                 │  mcp/x/issues     URL variants)  │
                 └──────────────────────────────────┘

Agent Authentication

MCP clients are plain HTTP clients — their config supports a URL and static headers, not a per-request signing loop. So agent authn is layered:

Baseline: per-agent API keys

ghpool issues one key per agent, mapped to policy. Delivery via existing secrets infra (ECS task secret / K8s Secret → env var); MCP clients expand env vars in config headers:

{ "url": "https://ghpool:8080/mcp",
  "headers": { "X-Ghpool-Key": "${GHPOOL_KEY}" } }

Transport security: TLS is required for production (terminated by ghpool, ALB, or service mesh). The key is a bearer credential and must not transit in cleartext.

Key properties: high entropy (256-bit), constant-time comparison, excluded from all logs/traces, support two simultaneously valid values for zero-downtime rotation.

Blast radius: this is a ghpool credential, not a GitHub credential. A leaked key is bounded by that agent's tool+repo policy and revoked centrally in ghpool config without touching GitHub — categorically better than a leaked PAT or vended token.

Optional: secretless via ghp mcp stdio shim

For zero static secrets: a ghp mcp subcommand runs as a stdio MCP server inside the agent container, piping frames to ghpool over HTTP while attaching a periodically-refreshed presigned sts:GetCallerIdentity proof signed with ambient IAM credentials (ECS task role / EKS IRSA).

Security controls for SigV4 proof:

  • TLS required (proof is not encryption)
  • Very short validity window (≤ 60s)
  • Allowlisted STS host and region only
  • Strict validation of signed method/body/headers
  • Replay mitigation via nonce or timestamp check

ghpool forwards the proof to STS and maps the caller ARN to agent policy. The signing capability lives in the shim, so any MCP client works unchanged.

Phasing

Phase 0: Compliance & Compatibility Spike

  • Resolve PAT-pooling compliance concern → decision: GitHub App installation token as primary credential
  • Verify hosted MCP endpoint contract:
    • GitHub App installation token authentication
    • Tool configuration: fixed at initialization vs. per-request
    • Mcp-Session-Id, SSE resumption, Last-Event-ID, session termination
    • Rate-limit and request-ID headers
    • Policy behavior for invalid/unknown toolsets
    • Timeouts, disconnects, credential expiry behavior
    • Protocol-version compatibility
  • Build automated contract test suite (upstream is external dependency outside our release control)
  • Define rollback/fallback path if upstream changes

Phase 1: Read-Only Proxy (PR #20)

Security boundary: network-trust model (same as existing ghpool read routes). No agent authn required — all workloads on the network share the same read-only access.

Scope:

  • Reverse proxy POST/GET/DELETE /mcp → upstream /mcp/readonly
  • Session pinning (moka cache, idle TTL)
  • Header rewrite (strip client auth, inject pooled bearer)
  • Streaming passthrough (JSON + SSE)
  • Audit log (best-effort JSON-RPC frame parse)
  • Opt-in config (enabled = false default)
  • Body size limit + upstream timeout (hardening — shipped in Add read-only MCP reverse proxy (Phase 1 of #15) #20 review revisions)
  • MCP spec-compliant session lifecycle (unknown session → 404 — shipped in Add read-only MCP reverse proxy (Phase 1 of #15) #20 review revisions)

Limitations (documented & accepted for Phase 1):

  • allowed_owners not enforced on MCP path (requires tool-argument inspection — Phase 2)
  • Single-replica only (in-memory session cache; rolling deployment = session re-initialization)
  • Network trust — any workload that can reach /mcp gets read access to all repos available to the credential
  • PAT pool used (GitHub App backend is Phase 2 prerequisite for production)

Phase 2: Agent Authn + Per-Agent Policy + Writes

Gate: ALL of the following must be implemented before any write tool is exposed.

  • Agent authentication (X-Ghpool-Key or SigV4)
  • Per-agent default-deny policy: explicit tool allowlist + repo allowlist
  • Resource-level authorization (resolve target repo from tool arguments; deny if unresolvable)
  • GitHub App credential backend (replaces PAT pool for MCP path)
  • Session-to-agent binding (session ID presented with different agent → reject)
  • Credential/session revocation (key rotation, agent disable, policy change → invalidate sessions)
  • TLS requirement for production deployments
  • Durable audit trail (structured, tamper-resistant; writes fail-closed if audit backend unavailable)
  • Per-tool write classification (distinguish read vs. write tools)
  • Outbound header allowlist (strip and replace all X-MCP-* from client)
  • Safe retry behavior (no auto-retry for non-idempotent writes)
  • tools/list response filtering (prune tools not in agent's allowlist)

Phase 3: Operational Hardening

  • Secretless IAM authentication (ghp mcp stdio shim + SigV4)
  • Horizontal scaling (shared session state or LB affinity with documented failure mode)
  • Per-agent rate quotas + circuit breaking (protect against noisy-neighbor)
  • Dashboards + alerting (MCP request metrics, deny rates, latency)
  • Automated upstream contract testing (detect breaking changes)
  • Cache authorization (ensure cached responses respect caller's policy)

Tracking

Comparison with Token Vending

Token Vending ghpool MCP proxy
Agent gets GitHub token? ✅ Short-lived ❌ Never
Access boundary Token scope Per-agent tool + repo allowlist (default-deny)
git CLI compatible ✅ Direct ❌ MCP tools only
Tool schema maintenance n/a None (GitHub-owned)
New tools Auto-available Default-denied until allowlisted
Best for git-heavy coding agents Issues/PRs/reviews with strict audit

Complementary Design

The two patterns coexist behind ghpool:

  • MCP proxy path — issues, PRs, comments, reviews, workflow ops. GitHub credential never leaves ghpool. Per-agent tool+repo policy enforced.
  • Token vending pathgit clone/push for coding agents that need a real checkout, using the Token Vending Machine pattern internally (GitHub App → scoped installation token → agent).

ghpool's existing REST/GraphQL read proxy (caching + credential isolation) is unchanged.

Known Tradeoffs

  • Proxy vs. direct access: ghpool adds latency and a single point of failure, but provides the only viable path to zero-credential agents with per-tool policy
  • GitHub-side attribution collapses to the GitHub App — ghpool's structured audit log is the authoritative record; commit trailers / PR body attribution supplement but don't replace it
  • Hosted endpoint dependency: requires egress to api.githubcopilot.com. Fallback: run a self-hosted github-mcp-server container next to ghpool — identical headers and schemas
  • Session state is in-memory (Phase 1-2): process restart or rolling deployment terminates all sessions; clients must re-initialize. Shared state is Phase 3.
  • No tool schema ownership means no schema validation: ghpool trusts upstream tool definitions; if upstream adds a dangerous tool, defense is the per-agent allowlist (not auto-exposed)

Security Invariants

These properties must hold across all phases:

  1. No GitHub credential leaves ghpool — agent never receives a PAT, App token, or any GitHub bearer
  2. Default-deny — unlisted tool or repo is always denied, regardless of upstream availability
  3. Session integrity — unknown/stale session → 404 (per MCP spec); no silent identity rotation
  4. Outbound header control — client cannot inject or override Authorization, X-MCP-Toolsets, X-MCP-Readonly, or any policy-affecting header
  5. Audit before action — every tools/call is logged with agent identity, tool, target, and decision before forwarding
  6. Credential isolation — upstream token lifetime bounds session lifetime; no session outlives its credential

Open Questions (Resolved & Remaining)

Resolved

  • Upstream credential type → GitHub App installation token (recommended); PAT pool retained as legacy compatibility option
  • Expose as path or port/mcp on existing listener (conditional route registration)
  • Per-tool allowlist timing → Phase 2 gate (not optional Phase 3)

Remaining

  • Policy config format: extend config.toml ([[agents]]) or separate policy file?
  • Should tools/list responses be cached, or rely on upstream caching?
  • Cross-org GitHub App installations: one App with multiple installations, or multiple Apps?
  • Audit backend: CloudWatch Logs, S3, or pluggable sink?

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions