Skip to content

Commit 2e16eca

Browse files
committed
Sync open source content 🐝 (from 11f2fd6a0c6824b0a85737221577b0c6c01965d2)
1 parent b237312 commit 2e16eca

1 file changed

Lines changed: 7 additions & 3 deletions

File tree

‎docs/ai-control-plane/mcp-gateway/tunneled-servers/internal-mcp.mdx‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,15 @@ The header contains the JWT without a `Bearer` prefix. Your server can ignore th
126126

127127
### JWT contract
128128

129-
The protected JWT header contains `alg=RS256`, `typ=speakeasy-authz+jwt`, and a `kid` identifying the signing key. The `kid` is the public key's RFC 7638 SHA-256 thumbprint.
129+
The protected JWT header contains `alg=RS256`, `typ=speakeasy-identity+jwt`, and a `kid` identifying the signing key. The `kid` is the public key's RFC 7638 SHA-256 thumbprint.
130130

131131
| Claim | Value |
132132
| --- | --- |
133133
| `version` | `1`. |
134134
| `iss` | `https://tunnel.speakeasy.com`. |
135135
| `aud` | The destination's saved resource identifier, or `tunneled-mcp-server:<TUNNELED_MCP_SERVER_ID>` if it is unset. |
136136
| `sub` | A stable, typed principal ID: `user:<USER_ID>`, `api_key:<API_KEY_ID>`, or `agent:<AGENT_ID>`. The prefix identifies the principal type. |
137+
| `organization_id` | The destination owner's Speakeasy organization ID. |
137138
| `email` | The human caller's profile email. Absent for API keys and agents. |
138139
| `iat`, `exp` | Issuance and expiry in Unix seconds. Valid for at most 60 seconds, and capped by the source credential's expiry where available. |
139140
| `jti` | A unique identifier for this assertion. Each forwarded request or retry gets a fresh assertion. |
@@ -154,11 +155,14 @@ https://tunnel.speakeasy.com/.well-known/jwks.json
154155
If your server uses these claims to identify a caller:
155156

156157
1. Use a JWT library to select the RSA signing key by `kid` from this fixed JWKS URL. Do not follow key URLs or issuers supplied by a token.
157-
2. Verify the signature with an explicit RS256 allowlist. Require `typ=speakeasy-authz+jwt`, `version=1`, `iss=https://tunnel.speakeasy.com`, and the exact audience you expect for your server.
158+
2. Verify the signature with an explicit RS256 allowlist. Require `typ=speakeasy-identity+jwt`, `version=1`, `iss=https://tunnel.speakeasy.com`, and the exact audience you expect for your server. If your server is reachable other than through the tunnel agent, also require `organization_id` equal to your organization's ID.
158159
3. Require `iat` and `exp`. Reject expired or future-dated tokens, and enforce a maximum 60-second lifetime with at most five seconds of clock tolerance.
159160
4. Apply your own access policy to the verified identity.
160161

161-
The default `tunneled-mcp-server:<ID>` audience is unique to your tunneled server. A saved resource identifier is not: another organization can save the same identifier, and Speakeasy then issues assertions with that audience to its own callers. When you use a custom audience, do not accept every valid assertion; allowlist the `sub` values your policy admits.
162+
The tunnel connection is the primary binding: only Speakeasy can deliver requests through your tunnel, and it forwards only assertions minted for this server. The assertion identifies the caller for your policy and audit, and proves the request came from Speakeasy. The default `tunneled-mcp-server:<ID>` audience is unique to your server. A saved resource identifier is not: another organization can save the same identifier and receive assertions with that audience. Do one of the following:
163+
164+
- Accept requests only from the tunnel agent, for example with a network policy, so every request arrives through your tunnel.
165+
- If your server is reachable any other way, such as from the internet, require `organization_id` to match your organization's ID. This rejects assertions minted for another organization that saved the same identifier.
162166

163167
The JWKS endpoint supports GET, HEAD, ETag, and conditional GET. It advertises a five-minute cache lifetime (`Cache-Control: public, max-age=300, must-revalidate`). On an unknown `kid`, refresh from that fixed URL once before rejecting the assertion. The JWKS always includes the current signing key and the next one, and keeps the previous key while it may still be in use. The signing key changes about every two months. Each key is published about a day before it signs and stays published about a day after it stops, so verifiers that follow this refresh rule see no gap during rotation.
164168

0 commit comments

Comments
 (0)