docs/REVERSE_PROXY.md

ORDS MCP derrière un reverse proxy

Cette page ne concerne que le proxy devant ORDS. L’IdP d’entreprise (authorize, token, JWKS, découverte OIDC) est un hôte différent — ne pas mettre ces URL IdP sur le proxy ORDS.

Topologie

Qui appelle quelle URL

 OpenCode / client MCP
        │
        │  1. POST https://ords-mcp.company.com/mcp
        ▼
 public reverse proxy  ──►  ORDS  :8080/mcp
        │                     │
        │                     │  3. ORDS va chercher la JWKS (côté serveur)
        │                     ▼
        │              https://login.company.com/.well-known/jwks.json
        │
        │  2. OAuth navigateur (authorize / token) — PAS via le proxy ORDS
        ▼
 https://login.company.com/authorize
URLHôteQui l’appelle
https://ords-mcp.company.com/mcpORDS (ce proxy)Le client MCP
https://ords-mcp.company.com/.well-known/oauth-protected-resource/mcpORDS (ce proxy)Le client MCP (métadonnées de ressource, RFC 9728)
https://login.company.com/authorize (token, etc.)IdP d’entrepriseNavigateur / OpenCode
https://login.company.com/.well-known/jwks.jsonIdP d’entrepriseORDS (sortant ; doit atteindre l’IdP depuis le processus ORDS)
https://login.company.com/.well-known/openid-configurationIdP d’entrepriseClient MCP / OpenCode
⚠️ Deux règles qui évitent 90 % des problèmes 1) ORDS MCP est une ressource à la racine de l’hôte (/mcp), pas sous le contexte REST /ords. standalone.context.path (par défaut /ords) ne concerne que REST/APEX. 2) Il n’existe aucun réglage ORDS de préfixe de chemin pour un proxy — l’audience n’est pas ce réglage.

L’audience

Audience = URL publique du MCP

mcp.security.jwt.profile.audience est l’URL publique du MCP : la chaîne exacte que les clients envoient en OAuth resource=, et l’API identifier / aud JWT de l’IdP.

Même valeur aux quatre endroits :

EndroitExemple (recommandé)Exemple (préfixe chemin — à éviter)
URL MCP du clienthttps://ords-mcp.company.com/mcphttps://my-proxy/ords/mcp
API identifier IdPidemidem
JWT audidemidem
ORDS mcp.security.jwt.profile.audienceidemidem

Faire correspondre schéma, hôte, port, chemin. Pas de slash final, sauf si tous les côtés en ont un. https://my-proxy/mcp et https://my-proxy/ords/mcp sont des audiences différentes.

💡 C’est de la validation JWT uniquement Mettre l’audience à https://my-proxy/ords/mcp ne fait pas générer à ORDS un /ords/mcp dans WWW-Authenticate ni dans les métadonnées de ressource protégée.

Pourquoi OpenCode impose l’exactitude

Dans opencode.json, mcp.<name>.url est envoyée comme OAuth resource= à l’authorize :

https://<idp>/authorize
  ?…
  &resource=https://ords-mcp.company.com/mcp

Auth0/Entra fait correspondre resource= à l’API identifier ; cela devient le JWT aud ; ORDS vérifie aud contre mcp.security.jwt.profile.audience. Utiliser l’URL publique joignable depuis le poste, pas l’interne http://ords:8080/mcp.

Config ORDS liée au proxy

Ce qu’ORDS sait configurer

ords --config "$ORDS_CONFIG" config set --global \
  mcp.security.jwt.profile.audience "https://ords-mcp.company.com/mcp"

ords --config "$ORDS_CONFIG" config set --global \
  security.httpsHeaderCheck "X-Forwarded-Proto: https"
RéglageEffet
mcp.security.jwt.profile.audienceaud JWT attendu = URL publique du MCP
security.httpsHeaderCheckX-Forwarded-Proto: https pour qu’ORDS sache que le TLS s’arrête au proxy
security.externalSessionTrustedOriginsAllowlist CORS si un Origin navigateur est l’hôte public
standalone.context.pathREST/APEX uniquement — ne déplace pas /mcp

Helm : config.jwtAudience doit être cette même URL publique (deploy/k8s/ords-mcp/values.yaml).

Topologie recommandée

Hôte dédié, sans suppression de préfixe

Donner un nom d’hôte à ORDS. Ne pas retirer /ords. Ne pas placer l’IdP sur cet hôte.

https://ords-mcp.company.com/mcp
  → http://ords:8080/mcp

https://ords-mcp.company.com/.well-known/oauth-protected-resource/mcp
  → http://ords:8080/.well-known/oauth-protected-resource/mcp

Proxyer ces deux chemins ORDS sans les réécrire. Audience : https://ords-mcp.company.com/mcp. C’est ce que supposent les docs Oracle MCP, ce lab et le Helm chart (ingress.hosts[].paths par défaut /). Un catch-all / convient uniquement parce que cet hôte est ORDS, pas l’IdP.

Transmettre : Host (ou X-Forwarded-Host), X-Forwarded-Proto: https, X-Forwarded-For. Désactiver le buffering et utiliser de longs timeouts sur /mcp (HTTP streamé).

ORDS doit toujours pouvoir sortir vers l’URL JWKS de l’IdP (NetworkPolicy, proxy, DNS). Ce trafic ne passe pas par ce reverse proxy.

Sketch nginx (hôte ORDS dédié)

# ORDS MCP uniquement. IdP / JWKS restent sur login.company.com — ne pas les ajouter ici.
location /mcp {
    proxy_pass http://ords:8080;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

# Métadonnées de *ressource* RFC 9728 sur l'hôte ORDS — pas JWKS, pas OIDC
location /.well-known/oauth-protected-resource/mcp {
    proxy_pass http://ords:8080;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Le piège

Préfixe de chemin (https://my-proxy/ords/ → http://ords)

C’est la disposition qui casse la découverte :

https://my-proxy/ords/mcp  →  http://ords/mcp

ORDS ne voit que l’hôte my-proxy et le chemin /mcp. Il annonce donc :

ÉlémentReconstruit par ORDSUtilisé par le client
Ressource MCPhttps://my-proxy/mcphttps://my-proxy/ords/mcp
WWW-Authenticate resource_metadatahttps://my-proxy/.well-known/oauth-protected-resource/mcpsous /ords/ si c’est tout ce qui est proxyé

C’est attendu : il n’y a pas de bouton de préfixe. Ne pas forwarder https://my-proxy/ords/mcp vers http://ords/ords/mcp sans retirer le préfixe : MCP n’est pas à /ords/mcp côté backend (404).

Si la politique impose le préfixe /ords

  1. Retirer /ords pour que les POST atteignent /mcp.
  2. Publier aussi la découverte là où les clients la cherchent. Mappages minimum :
https://my-proxy/ords/mcp
  → http://ords/mcp

https://my-proxy/ords/.well-known/oauth-protected-resource/mcp
  → http://ords/.well-known/oauth-protected-resource/mcp

L’insertion de chemin RFC 9728 tente aussi la racine de l’hôte :

https://my-proxy/.well-known/oauth-protected-resource/ords/mcp

et ORDS lui-même annonce https://my-proxy/mcp et https://my-proxy/.well-known/oauth-protected-resource/mcp — ces deux-là ne sont pas sous /ords/. Une location qui ne proxye que /ords/ ne les verra jamais.

Ensuite, soit :

⚠️ Ne pas « réparer » en proxyant la JWKS ou l’OIDC /.well-known/ de l’IdP sur my-proxy Ils restent sur l’hôte de l’IdP. Plus propre : exposer MCP à la racine (https://my-proxy/mcp) et laisser /ords au REST/APEX classique s’il faut partager l’hôte.

Récapitulatif

Les endpoints que le proxy doit atteindre

Chemin public (hôte recommandé)Chemin ORDSRôle
/mcp/mcpMCP HTTP streamé + défi 401 Bearer
/.well-known/oauth-protected-resource/mcpidemMétadonnées de ressource protégée RFC 9728 (liste authorization_servers si configuré)

Pas sur ce proxy

URLConfigRôle
JWKS IdPmcp.security.jwt.profile.jwk.urlORDS valide les signatures JWT
Issuer IdPmcp.security.jwt.profile.issuerJWT iss
authorize/token IdPmcp.security.jwt.profile.authorization.server.urlLogin navigateur OpenCode ; copié dans les métadonnées comme authorization_servers

Un GET/POST non authentifié sur /mcp doit renvoyer 401 avec un WWW-Authenticate pointant vers l’URL well-known ORDS ci-dessus. Ce document peut nommer l’IdP ; le client parle ensuite à l’IdP sur son propre hôte.

Vérification

Comment valider la config

Comparer les URL annoncées à l’URL réellement utilisée par le client :

curl -sI https://ords-mcp.company.com/mcp
curl -s  https://ords-mcp.company.com/.well-known/oauth-protected-resource/mcp

Si ces corps disent https://my-proxy/mcp pendant qu’OpenCode utilise https://my-proxy/ords/mcp, c’est la reconstruction Host + /mcp. Changer l’audience ne corrige que la validation JWT, pas ces URL annoncées. Après OAuth, le aud du jeton doit égaler cette même URL publique du MCP (voir la page OAuth — contrôle du jeton).

Reproduire au travail (K8s / IdP d’entreprise) →