Fine-Grained Access Control for Hosted MCP Servers You Don't Own

Nick Taylor
Developer Advocate, Pomerium

Nick Taylor crouched on a rugby field in a black jersey, short hair bleached in blonde spots.

Nick Taylor in a red jersey carries the ball through a muddy ruck.

'Ran through me' & 'good value in pub' - ex-NFL star Kelce tries rugby

"At the very start, he sort of went round the back of a ruck and picked the ball up, which he obviously was then penalised for."

Dave Lewis, Hackney RFC, to BBC Sport

Durrans, T. (2026, October 9).
'Ran through me' & 'good value in pub' -
ex-NFL star Kelce tries rugby. BBC Sport.
https://www.bbc.com/sport/articles/cm79pdw50y7wo

Governance

Governance

Some servers ship read-only

  • GitHub: an HTTP header, or /readonly in the URL path.
  • Linear: /mcp/readonly in the URL path, or the read OAuth scope.
  • Atlassian: a query parameter on the URL.

Governance

All of these are deploy-time controls.

  • Read-only, scopes, or a fixed list of tools.
  • Set when deployed. Can't change until the next deploy.
  • Same tools for every caller, on every call.

Governance

A lot of MCP servers still skip OAuth

Of 7,973 live remote servers:

  • 30% use OAuth
  • 29% use a static token or API key
  • 41% have no authentication

Zhou, H., Zhang, X., Zhang, H., Zhang, H.,
Zhang, M., & Yang, M. (2026, May).
A first measurement study on authentication security
in real-world remote MCP servers. arXiv.
https://arxiv.org/abs/2605.22333

Governance

A static token is often one person's key.

  • Share it, and the log can't name the teammate.
  • A key per person fixes that, but doesn't scale and is a maintenance burden.

Governance

OAuth scopes are coarse and static

  • A scope grants broad access, not per-tool control
  • You can't remove one action from a scope
  • Still set when the client is configured

MCP Gateways

MCP Gateways

  • MCP gateways are proliferating
  • An MCP server's own controls aren't enough

MCP Gateways

  • A gateway can enforce policy between the client and the server
  • A central point for managing security posture
  • One kind of gateway is an identity-aware proxy

Identity Aware Proxy

What is an Identity Aware Proxy?

  • Checks identity and policy on every request, not just at login
  • No VPN, identity over network location
  • Policy enforced at the proxy, not in each app
  • Audit logs out of the box

What is an Identity Aware Proxy (IAP)?

You on the left reach an identity-aware proxy in the center. An identity provider sits above it. Allow continues right to internal resources. Deny drops to access denied.

Pomerium

Pomerium

  • An identity-aware proxy
  • Open core
  • Installs at your network's edge

Pomerium

  • Always self-hosted
  • All your data flows through your own infrastructure
  • Handles OAuth for you, including upstream flows
  • Tokens stay in the gateway, whether or not you own the server

Pomerium is an IAP

You, from a browser, MCP client, or terminal, reach Pomerium in the center. An identity provider sits above it. Allow continues right to web apps, MCP servers, SSH servers, databases, and other resources. Deny drops to access denied.

Fine-grained authorization

Who gets in

Control who can access the MCP server.

Fine-grained Tool Authorization

Allow the server, but deny one or more tools.

Servers you own

  • Policy lives at the proxy, not in your app
  • No OAuth code in the server
  • Allow or deny per caller, per tool

Servers you don't own

  • Policy lives at the proxy, not in your app
  • Allow or deny per caller, per tool
  • You don't need to control the server to govern access

Pomerium bridges it

  • Your identity provider signs the person in
  • Pomerium runs the upstream OAuth flow
  • The upstream token is injected into each request — the client never sees it

Route configuration

from: https://github.maisonlab.dev
to: https://api.githubcopilot.com
name: GitHub
mcp:
  server:
    upstream_oauth2:
      client_id: xxxxxxxxxxxx
      client_secret: xxxxxxxxxxxx
      scopes: ["read:user", "user:email", "read:org", "repo"]
      endpoint:
        auth_url: https://github.com/login/oauth/authorize
        token_url: https://github.com/login/oauth/access_token

Policy for a Route

policy:
  - allow:
      and:
        - email:
            in:
              - nick@nickyt.co
              - ntaylor@pomerium.com

Policy for a Route that Denies a Tool Call

policy:
  - allow:
      and:
        - email:
            in:
              - nick@nickyt.co
              - ntaylor@pomerium.com
  - deny:
      and:
        - mcp_tool:
            is: merge_pull_request

Demo Time!

Resources

Homework

Everything demoed today, you can do yourself in this free interactive lesson.

go.pomerium.com/ato26-lesson

QR code for go.pomerium.com/ato26-lesson

Full free course

You can also do the full free course to learn about leveraging Pomerium as an MCP gateway.

go.pomerium.com/ato26-mcp-course

QR code for go.pomerium.com/ato26-mcp-course

Let's connect!

I'm always looking to connect with people in the community. Don't be shy!

nickyt.co/socials

QR code for nickyt.co/socials

Thank you!

Intro: name, pronouns, brief visual description. On screen: All Things Open art. Astronaut on the right. Mark and October 19–20, Raleigh, NC, lower left. I'm Nick Taylor. Developer Advocate at Pomerium. Title says MCP. First mention: Model Context Protocol. Same talk as the shorter GitHub Universe cut. This title is the All Things Open one. Ask the room. Who's building MCPs? Who's shipped one to production? Who's governing them, if anyone? Slot is 11:15–11:45. Thirty minutes, not forty-five. Leave time for the demo.

Left: me, crouched, black jersey, white stripe, polka-dot hair. A bit grainy. Pre-digital print. Leave it. Right: me in red, ball in hand, mud, running through traffic. Ask the room. Who's played rugby? Played for years. Hooker. Front row. The one who hooks the ball in the scrum. The hair. One line, then move. Optional callback: tackling in touch rugby is like read-only mode. The rules say you can touch but not tackle. Baked into the format. Drop it if the rugby thread is landing. Next is the BBC piece. Kelce, the quote, then governance. Do not put OAuth on the pitch.

BBC Sport, 9 October 2026. Tim Durrans. No screenshot. Headline is the title. Quote is Dave Lewis, Hackney prop. On screen: the headline, Lewis's line, cite bottom right. Link is the BBC URL. Jason Kelce. Say Kelce, not Kelsey. Retired NFL center. 30-minute club friendly, Old Streetonians against Hackney. He played 8. Crash course from Joe Marler, former England prop. A couple of pointers from Ilona Maher. She is American. She is not the England prop. You can't go round the back of a ruck and pick the ball up. He did. Penalty. Wrong rulebook. If the room needs an easier difference: you can throw the ball forward in American football. In rugby the pass goes lateral or back. Do not say this penalty was a forward pass. A backward pass is legal in football too. Quote is the turn. Rules, then governance. Every company has its own policies. Next is Governance. Gateways, then the blunt policies. Not the OAuth count.

Title on screen is Governance. Line under it: some servers ship read-only. Then three bullets. Framing: MCP servers are shipping levers. But they're static. You set them when you deploy, and they stay that way until the next deploy. GitHub hosted server is api.githubcopilot.com/mcp/. Docs: github/github-mcp-server remote-server.md and server-configuration.md. Read-only drops write tools. Header X-MCP-Readonly, or /readonly on the URL. Those headers choose tools, not OAuth scopes. Also X-MCP-Toolsets, X-MCP-Tools, X-MCP-Exclude-Tools. URL /x/{toolset} is one toolset. Hosted: the headers sit in the client config. You do not deploy that server. Do not say set when you deploy. Local server you run: --read-only or GITHUB_READ_ONLY, --toolsets, --tools, --exclude-tools. Flags at startup are a ceiling. Headers can only narrow. GitHub MCP PR 2208. Linear, linear.app/docs/mcp: https://mcp.linear.app/mcp/readonly only exposes read tools. Or request only the read OAuth scope on /mcp. The scope name is read. The token cannot reach write APIs. Or an API key with only the Read permission. Atlassian: maintainer reply on atlassian/atlassian-mcp-server issue 11. A capabilities query on https://mcp.atlassian.com/v1/mcp. That reply said the docs were not updated yet. The API-token guide does split read and write scopes, read:jira:agent-interface versus write. Don't quote the capability names as a docs page. Next: these are all deploy-time controls.

Title on screen is Governance. These are all deploy-time controls. Read-only, scopes, a fixed tool list. You set them when you deploy, and they don't change until the next deploy. Same tools for every caller, on every call. The list does not look at who is calling, and it does not change on this request. Hosted GitHub: that cut is the client config. You do not deploy GitHub's server. Say deploy for the client setup. Local server you run: the flags are there when the process starts. Next: gateways are proliferating because these controls aren't enough.

Title on screen is Governance. The line under it is: a lot of MCP servers still skip OAuth. OAuth is part of the governance story. Say it here. Do not say it isn't. No slide of its own. The person logs in at the identity provider. That part is authentication. The token's scopes are what the client can do. That part is authorization. OAuth 2 is an authorization framework. Zhou's abstract calls OAuth the dominant authorization mechanism for reaching remote services. The scopes slide is that half. Don't expand the letters. Don't teach grant types. Do not say coarse yet. Reference, APA: Zhou, H., Zhang, X., Zhang, H., Zhang, H., Zhang, M., & Yang, M. (2026, May). A first measurement study on authentication security in real-world remote MCP servers. arXiv. https://arxiv.org/abs/2605.22333 Authors: Huijun Zhou and Xiaohan Zhang, equal contribution. Then Haozhe Zhang, Haoyang Zhang, Mi Zhang, Min Yang. Fudan, except Haoyang Zhang at Central South University. Sample: 7,973 live remote MCP servers. Title of the paper says remote. Found on FOFA and Shodan, then checked with an MCP handshake. Public internet. Not GitHub repos. Not stdio. Stdio is a local subprocess, so it never shows up in that search. Stdio stays in the same trust boundary as the host. The spec puts OAuth on the HTTP transport, from the 2025-03-26 revision. A stdio server can still hold a token in the environment for an upstream API. That is not OAuth between the client and the server. Exact: 2,428 OAuth, 30.45%. 2,312 static token or API key, 29%. 3,233 no authentication, 40.55%. Slide rounds 40.55 to 41. 30 and 29 are already the nearest percent. About 70% are not OAuth. That is the 41 plus the 29. Speak the 41% as no authentication. That is the paper's category: no credential at all. Do not relabel it as no authorization. This is not a count of gateway policy, and it is not enterprise-internal MCP. If someone asks about source code instead of live servers: Astrix, 15 Oct 2025, 5,200+ public repos. 8.5% OAuth, 53% static API keys or personal access tokens. Different population. Don't mix the two numbers. Next: a shared static token. A gateway in the worst way.

Title on screen is Governance. A static token is often one person's key. It's a gateway in the worst way. One key, shared around, and you can't tell who did what. Zhou finding 1.3: often single-user or manually provisioned. No standard login, consent, or account linking. Not a percentage of the 29%. Say often. Do not say most of the 2,312. Share it, and the log can't name the teammate. GitHub's audit log actor is the account that authenticated. You can also search hashed_token. A shared PAT shows the token owner, not the coworker who pasted it into the MCP config. https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/identifying-audit-log-events-performed-by-an-access-token Everyone who holds that key has the same access. Say it if asked. Whatever you set on the key. Same on every call. A key per person can name the key holder, if the product logs the key. Then you issue, rotate, and revoke each one. That provisioning is the governance pain. His point. Not a study. Do not say the paper measured observability. Next: when a server does use OAuth, the scopes are coarse and static. GitHub repo.

Scopes. Coarse, and static. Say both here. Pomerium is not on screen yet. OAuth says what the token can do. Not whether this call should happen. Say this example out loud. GitHub's repo scope grants full access to public and private repositories, including read and write to code. The github-mcp-server README shows merge_pull_request challenges for repo. The point: merge_pull_request comes with repo, and you can't take it out. Remember this — the demo denies exactly that tool call. Narrower classic scopes exist. public_repo, repo:status. They don't subtract merge from a token that already has repo. If you own the catalog, you can split it. Linear shipped a read scope. Atlassian split read and write. Publishing that list is when you ship. Callers still choose a fixed set when the client is configured, or at consent. Step-up adds scopes. It does not drop merge for one call. Fine-grained PATs are a different token. Remote OAuth uses the classic scope challenge. Next: gateways are proliferating because none of this is enough.

Governance. His read. No count. MCP gateways are proliferating. The server's own controls aren't enough. Not a bad thing. Security always mattered. With agents it matters more. An individual might not care. Someone running MCP for an org does. Examples if asked. Do not name these on the slide. They validate the space. All of them are gateways. Octelium: self-hosted, CEL or OPA, method, tool, arguments, identity. octelium.com. Tyk: per-tool allow and block, tools/list filtered per consumer. They say that set is open source. Upstream OAuth client-credentials is Enterprise. tyk.io. Obot: open source, per-tool policy for users, groups, and agents. obot.ai. Permit: per-tool policy, risk level, consent. permit.io. agentgateway: CEL on tool name and JWT. Their docs say those MCP rules cannot see arguments. Arguments are a separate route policy. agentgateway.dev. Lunar MCPX: YAML ACLs read at startup. Caller is the x-lunar-consumer-tag header. lunar.dev. Veto: gateway judges tool name and arguments. Allow, deny, or approval. Also an SDK, so not only a gateway. veto.so. Not every gateway does this. A gateway can be a front door and have no per-tool policy. Leave comparison-blog names out. Next: IAP section.

That decision sits in front of the server. Including a server you don't own. An identity-aware proxy checks every request, not just login. No VPN. Policy at the proxy, not in each app. Audit logs come free because all traffic flows through it. A hosted agent can't join your VPN. From last year's GitNation talk. ChatGPT and Claude need a URL they can reach. Next: the IAP diagram, then Pomerium.

Generic identity-aware proxy. Walk left, middle, right, then down. Left: you, browser or client, arrow into the proxy. Top: identity provider points down at the proxy. Middle: the proxy checks identity and policy. Right: allow, into internal resources. Resource 1, Resource 2, Resource n. Down: deny, access denied.

Pomerium intro before the diagram. Open core. Always self-hosted, so all your data flows through your own infrastructure.

Pomerium intro, second half. Self-hosted and data ownership. Ask the room: who loves implementing OAuth in their apps or MCP servers? Good news. Pomerium handles it. Nearly there on the latest spec work like CIMD if asked, but don't claim full spec compliance. Upstream OAuth tokens (if your MCP server needs one) stay in the gateway. Cached per user, refreshed there. The client never sees it.

Same shape, Pomerium named in the middle. Left: you. Browser, MCP client, or terminal. Top: identity provider. Middle: Pomerium. Identity-aware proxy. Checks identity and policy. Right: allow, into protected resources. Web apps, MCP servers, SSH servers, databases, resource n. Down: deny, access denied. MCP here is still Model Context Protocol.

Already said this. Gateways are adding it. Here are the two parts. Don't re-list the other gateways. Who gets in: could be an email, an email domain, an RBAC group, or third-party data. The policy decides. Which tool: allow the server, and still deny one tool call. Centralized policy still holds, including servers you don't own. The route later is the concrete version. Demo blocks merge_pull_request. Don't claim it proves more than that.

Servers you run. Policy at the proxy. Allow or deny on the request. Who's calling, which tool, this request. You are not writing OAuth into the server. Same kind of policy in the YAML later. That one is GitHub, which you don't own.

GitHub's static knobs already happened. This is the other problem. Demo target is GitHub. The route later goes to api.githubcopilot.com. A token that can merge on github.com can merge from the agent. The demo blocks merge_pull_request. Don't claim it proves more than that.

MCP bridging. Pomerium docs: capabilities/mcp and the bridge guide. The mechanism: IdP signs in the person, Pomerium runs the upstream OAuth flow, and the upstream token is injected into proxied requests. The client never sees it. Q&A bonus: the docs name the two tokens. External Token (TE) is what the client holds, its Pomerium session. Internal Token (TI) is the cached upstream token Pomerium attaches to proxied requests. Policy still runs on the tool call. This demo is the pre-registered path: GitHub client id and secret on the next slide. Servers that publish their OAuth metadata can skip the manual app. The bridge guide uses Linear for that. Not this demo.

Route config. This is the bridge. MCP bridging. Pomerium docs: capabilities/mcp and the bridge guide. This demo is the pre-registered path: GitHub client id and secret on the slide. Servers that publish their OAuth metadata can skip the manual app. The bridge guide uses Linear for that. Not this demo. Read the from and to first. From the demo host github.maisonlab.dev, to api.githubcopilot.com, name GitHub. Then the MCP server block: upstream OAuth against GitHub. Scopes on the slide: read:user, user:email, read:org, repo. Client id and secret on the slide are placeholders, not live credentials.

Who gets through this route. Allow only if the email is one of the two on the slide: nick@nickyt.co or ntaylor@pomerium.com.

Same allow list, plus a deny. Deny when the MCP tool is merge_pull_request. That's the fine-grained part: the route can be allowed and a single tool still blocked.

Demo. Say what you're selecting. Don't point at "here."

After the demo, before the lesson. Pomerium MCP capabilities: pomerium.com/docs/capabilities/mcp Bridge guide: pomerium.com/docs/guides/mcp-bridge Open source repo: github.com/pomerium/pomerium Zhou et al., May 2026, arXiv:2605.22333. The remote MCP authentication study. MCP is still Model Context Protocol. Next three slides are the wrap-up. About a minute for all three.

Homework is the interactive lesson from the demo. URL on the slide: go.pomerium.com/ato26-lesson QR on the right encodes that same URL. Wrap-up is this slide and the next two. About a minute for all three.

Follow-up: the full course, after the lesson. URL on the slide: go.pomerium.com/ato26-mcp-course QR on the right encodes that same URL. MCP is still Model Context Protocol. Still inside the one-minute wrap-up.

Closer. URL on the slide: nickyt.co/socials QR on the right encodes that same URL. Last slide of the one-minute wrap-up.

Final slide. Thank the room. QR on the right encodes the deck URL: ato2026-talk.netlify.app