<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sécurité on Rémi Tech Notes</title><link>https://www.vrchr.fr/tags/s%C3%A9curit%C3%A9/</link><description>Recent content in Sécurité on Rémi Tech Notes</description><generator>Hugo</generator><language>fr-fr</language><lastBuildDate>Mon, 18 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.vrchr.fr/tags/s%C3%A9curit%C3%A9/index.xml" rel="self" type="application/rss+xml"/><item><title>Envoy Gateway : OIDC, JWT &amp; Authorization</title><link>https://www.vrchr.fr/posts/2026/05/18/envoy-gateway-oidc-jwt-authz/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/05/18/envoy-gateway-oidc-jwt-authz/</guid><description>Protéger une application avec une SecurityPolicy OIDC via Envoy Gateway, et restreindre l&amp;#39;accès à un groupe spécifique avec JWT et authorization.</description><content:encoded><![CDATA[<p>Dans la continuité de l'article sur la <a href="../gateway-api-backendtls-trust-manager/">Gateway API</a>, une des ressources <a href="https://gateway.envoyproxy.io/">Envoy Gateway</a> avec laquelle j'ai joué est la <code>SecurityPolicy</code>. Depuis 1 seule ressource YAML, on peut brancher du vrai OIDC sur n'importe quelle <code>HTTPRoute</code> — sans déployer un <code>oauth2-proxy</code> à côté. Voici un exemple d'authentification avec Keycloak, et comment ajouter une notion d'authorization via filtrage par groupe.</p>
<p>Pour l'exemple, on va imaginer une app <code>mysecureapp</code> dans le namespace <code>mysecurens</code>, accessible sur <code>mysecureapp.gravitek.io</code>, et un Keycloak sur <code>sso.gravitek.io</code>.</p>
<p>💡 J'ai profité de cet article pour tester ma démo sur l'environnement Clever Cloud avec un cluster Kubernetes <a href="https://www.clever.cloud/developers/doc/kubernetes/">CKE</a> et un add-on <a href="https://www.clever.cloud/developers/doc/addons/keycloak/">Keycloak</a>. Je ne détaille pas le setup ici, peut-être dans un autre article 😉.</p>
<p>⚠️ Je n'explique pas non plus comment configurer Keycloak, ni comment exposer l'app via Gateway API, on suppose que vous avez déjà à disposition ces éléments.</p>
<h2 id="contexte--exposer-lapp-via-gateway-api">Contexte : exposer l'app via Gateway API</h2>
<h3 id="la-route-applicative-">La route applicative <code>/</code></h3>
<p>La <code>HTTPRoute</code> de base est classique : on route tout le trafic de <code>mysecureapp.gravitek.io</code> vers le service applicatif.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-https</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">hostnames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span>- <span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">      </span><span class="nt">backendRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">          </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><h3 id="la-route-oauth2-pour-le-callback-oidc">La route <code>/oauth2</code> pour le callback OIDC</h3>
<p>Pour que l'authentification OIDC fonctionne, la <code>redirectURL</code> et le <code>logoutPath</code> de la <code>SecurityPolicy</code> (qu'on verra juste après) doivent matcher une règle de la <code>HTTPRoute</code>. C'est mentionné dans la <a href="https://gateway.envoyproxy.io/docs/tasks/security/oidc/">doc Envoy Gateway</a>, en particulier la section <em>&quot;OIDC Authentication for a HTTPRoute&quot;</em> → <em>&quot;Create a SecurityPolicy&quot;</em>. C'est aussi valable pour une policy attachée à une Gateway.</p>
<p>Ici, la route <code>/</code> ci-dessus couvre déjà <code>/oauth2/callback</code> — donc en théorie une règle dédiée n'est pas obligatoire. Mais dès qu'on remplace ce <code>/</code> par des préfixes spécifiques (ex: <code>/myapp</code>, <code>/metrics</code>), aucun ne couvre <code>/oauth2</code> et la règle devient <strong>indispensable</strong>. Pour ne pas se mélanger les pinceaux, autant la déclarer explicitement dès le départ :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="c"># Endpoint interne du filtre OAuth2</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span>- <span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/oauth2</span><span class="w">
</span></span></span></code></pre></div><p>Pas besoin de <code>backendRefs</code> ici : ce path est entièrement géré en interne par le filtre OIDC d'Envoy (callback, échange de token, logout). Il faut juste que la <code>HTTPRoute</code> accepte de le router.</p>
<h2 id="oidc-avec-securitypolicy">OIDC avec SecurityPolicy</h2>
<p>Avant, avec ingress-nginx, ce type d'app tournait avec un <code>oauth2-proxy</code> déployé à côté, avec ses propres <code>Ingress</code>, sa conf, et son <code>Deployment</code> à maintenir. Avec Envoy Gateway, la <code>SecurityPolicy</code> fait exactement le même travail, sans déploiement supplémentaire :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-oidc</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">targetRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">oidc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">provider</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="l">https://sso.gravitek.io/realms/myrealm</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">clientID</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">clientSecret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-client-secret  </span><span class="w"> </span><span class="c"># Secret K8s contenant le client_secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">redirectURL</span><span class="p">:</span><span class="w"> </span><span class="l">https://mysecureapp.gravitek.io/oauth2/callback</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">logoutPath</span><span class="p">:</span><span class="w"> </span><span class="l">/oauth2/logout</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">scopes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span>- <span class="l">openid</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span>- <span class="l">groups </span><span class="w"> </span><span class="c"># On se basera plus tard la-dessus pour l&#39;authz</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span><span class="nt">cookieNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span><span class="nt">idToken</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;IdTokenMySecureApp&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Le <code>targetRefs</code> pointe sur la <code>HTTPRoute</code>. Envoy intercepte toutes les requêtes, lance le flow OAuth2/OIDC si l'utilisateur n'est pas authentifié, et pose le cookie <code>IdTokenMySecureApp</code> avec l'id_token JWT une fois connecté.</p>
<p>➡️ Résultat : toute personne avec un compte valide dans le realm peut accéder à l'app. C'est déjà pas mal... mais on peut mieux faire !</p>
<blockquote>
<p>💡 <strong>Pourquoi fixer <code>cookieNames.idToken</code> ?</strong> Ce champ est optionnel : sans lui, Envoy nomme le cookie <code>IdToken</code> mais y ajoute un suffixe de hash dérivé du domaine (ex: <code>IdToken-5671b67c</code>), donc imprévisible. Or le filtre JWT relira ce cookie plus tard via <code>extractFrom.cookies</code> et a besoin d'un nom stable. On l'épingle donc explicitement.</p>
</blockquote>
<h2 id="authn--authz">Authn ≠ Authz</h2>
<p>L'OIDC, c'est de l'<strong>authentification</strong> : il vérifie <em>qui vous êtes</em>. Il ne dit rien sur <em>ce que vous avez le droit de faire</em>. Avec la SecurityPolicy ci-dessus, n'importe quel compte du realm peut se connecter 🙈.</p>
<p>Il faut alors de l'<strong>autorisation</strong> : restreindre ici l'accès aux membres d'un groupe spécifique. Envoy Gateway résout ça avec deux blocs supplémentaires dans la même <code>SecurityPolicy</code> :</p>
<ul>
<li><strong><code>jwt</code></strong> : pour extraire et valider le token depuis le cookie posé par le filtre OIDC</li>
<li><strong><code>authorization</code></strong> : pour définir les règles d'accès à partir des claims du token</li>
</ul>
<p>L'ordre des filtres générés par Envoy Gateway est <code>oauth2</code> (OIDC) → <code>jwt</code> → <code>authorization</code>, le filtre OIDC s'exécute en premier. Une requête non authentifiée est donc interceptée et redirigée vers Keycloak <em>avant</em> que le filtre JWT ne soit évalué : ce dernier ne voit que des requêtes déjà authentifiées, qui portent l'<em>id_token</em> dans le cookie.</p>
<p><img alt="Ordre des filtres Envoy : OIDC → JWT → Authorization" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-filter-order.svg"></p>
<h2 id="filtrage-par-groupe-avec-jwt-et-authorization">Filtrage par groupe avec JWT et authorization</h2>
<p>Pour pouvoir vérifier les groupes d'un utilisateur, on va d'abord configurer le filtre JWT pour lire le cookie <code>IdTokenMySecureApp</code> et en extraire le claim <code>groups</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">providers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">remoteJWKS</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span><span class="nt">cacheDuration</span><span class="p">:</span><span class="w"> </span><span class="l">300s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">          </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm/protocol/openid-connect/certs&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">extractFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">          </span><span class="nt">cookies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">            </span>- <span class="l">IdTokenMySecureApp</span><span class="w">
</span></span></span></code></pre></div><p>Ensuite, on configure le bloc <code>authorization</code> où par défaut tout est <strong>refusé</strong>, et on autorise uniquement les membres du groupe <code>/admins</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="w">  </span><span class="nt">authorization</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="nt">defaultAction</span><span class="p">:</span><span class="w"> </span><span class="l">Deny</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">allow-admin-group</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Allow</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">principal</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">          </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">            </span><span class="nt">provider</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">            </span><span class="nt">claims</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">              </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">                </span><span class="nt">valueType</span><span class="p">:</span><span class="w"> </span><span class="l">StringArray</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">                </span><span class="nt">values</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/admins&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Le claim <code>groups</code> est un tableau dans Keycloak, d'où le <code>valueType: StringArray</code>. Envoy vérifie que <code>/admins</code> est présent dans ce tableau — si oui, la requête passe. Sinon, <code>403</code>.</p>
<h3 id="groupes-keycloak">Groupes Keycloak</h3>
<p>Pour que le claim <code>groups</code> reflète l'appartenance aux groupes, il faut un mapper de type <strong>Group Membership</strong> sur le client scope.</p>
<p><img alt="Group Membership mapper" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-keycloak-group-membership-mapper.webp"></p>
<p>Avec l'option <em>Full group path</em> <strong>activée</strong> sur le mapper Group Membership, le claim vaut <code>/admins</code> (avec le slash initial) ; désactivée, il vaut <code>admins</code>. Envoy fait une comparaison <strong>exacte</strong> : <code>values: [&quot;/admins&quot;]</code> ne matchera <em>jamais</em> un claim <code>admins</code>, et inversement. Un simple <code>/</code> de différence = <code>403</code>. Choisissez une convention et gardez la même des deux côtés (le mapper Keycloak et le bloc <code>authorization</code>).</p>
<p><img alt="Full group path" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-keycloak-full-group-path.webp"></p>
<h2 id="securitypolicy-complète">SecurityPolicy complète</h2>
<p>En assemblant le tout, un seul objet qui fait l'authn et l'authz :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">SecurityPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-oidc</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">targetRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span>- <span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">oidc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">provider</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="l">https://sso.gravitek.io/realms/myrealm</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">clientID</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">clientSecret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-client-secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">redirectURL</span><span class="p">:</span><span class="w"> </span><span class="l">https://mysecureapp.gravitek.io/oauth2/callback</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">logoutPath</span><span class="p">:</span><span class="w"> </span><span class="l">/oauth2/logout</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">scopes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span>- <span class="l">openid</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span>- <span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span><span class="nt">cookieNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span><span class="nt">idToken</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;IdTokenMySecureApp&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">  </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">    </span><span class="nt">providers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">        </span><span class="nt">issuer</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">        </span><span class="nt">remoteJWKS</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">          </span><span class="nt">cacheDuration</span><span class="p">:</span><span class="w"> </span><span class="l">300s</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">          </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://sso.gravitek.io/realms/myrealm/protocol/openid-connect/certs&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">        </span><span class="nt">extractFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">          </span><span class="nt">cookies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">            </span>- <span class="l">IdTokenMySecureApp</span><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">  </span><span class="nt">authorization</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">    </span><span class="nt">defaultAction</span><span class="p">:</span><span class="w"> </span><span class="l">Deny</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">allow-admin-group</span><span class="w">
</span></span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Allow</span><span class="w">
</span></span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="w">        </span><span class="nt">principal</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="w">          </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="w">            </span><span class="nt">provider</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln">45</span><span class="cl"><span class="w">            </span><span class="nt">claims</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="w">              </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">47</span><span class="cl"><span class="w">                </span><span class="nt">valueType</span><span class="p">:</span><span class="w"> </span><span class="l">StringArray</span><span class="w">
</span></span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="w">                </span><span class="nt">values</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;/admins&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Le diagramme ci-dessous résume le flux complet :</p>
<p><img alt="Flux d'authentification OIDC + JWT + Authorization" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-18-oidc-auth-flow.svg"></p>
<p>Et donc, après tout ce paramétrage, vous aurez un beau message si vous n'avez pas les droits nécessaires 😕 :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">RBAC: access denied
</span></span></code></pre></div><p>Sinon, si vous êtes dans le bon groupe, vous pourrez accéder à votre application 🥳 !</p>
<h2 id="allons-plus-loin--passer-lidentité-à-lapplication">Allons plus loin : passer l'identité à l'application</h2>
<p>Tout ce qu'on a vu jusqu'ici se passe <strong>dans la gateway</strong> : la décision authn/authz est prise par Envoy, et le backend <code>mysecureapp</code> ne voit jamais le token ni les groupes. C'est suffisant pour protéger l'accès, mais parfois l'app a <em>elle-même</em> besoin de savoir qui est connecté (afficher le nom de l'utilisateur, adapter l'UI selon les groupes, appeler une autre API en son nom...).</p>
<p>Deux champs optionnels permettent de transmettre cette info au backend.</p>
<h3 id="forwardaccesstoken--laccess-token-vers-le-backend"><code>forwardAccessToken</code> — l'access token vers le backend</h3>
<p>Dans le bloc <code>oidc</code>, <code>forwardAccessToken: true</code> demande à Envoy de transmettre l'<strong>access token</strong> OIDC au backend (dans le header <code>Authorization</code>) :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="nt">oidc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="c"># ...</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">forwardAccessToken</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h3 id="claimtoheaders--un-claim-dans-un-header"><code>claimToHeaders</code> — un claim dans un header</h3>
<p>Dans le bloc <code>jwt</code>, <code>claimToHeaders</code> injecte la valeur d'un claim du token dans un header HTTP transmis au backend :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="nt">jwt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">providers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">keycloak</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span><span class="c"># ...</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span><span class="nt">claimToHeaders</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">          </span>- <span class="nt">claim</span><span class="p">:</span><span class="w"> </span><span class="l">groups</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">            </span><span class="nt">header</span><span class="p">:</span><span class="w"> </span><span class="l">x-user-groups</span><span class="w">
</span></span></span></code></pre></div><p>Le nom du header (<code>x-user-groups</code>) est ici libre : ce n'est ni un standard ni une convention Keycloak. L'app lira alors les groupes dans ce header sans avoir à décoder le JWT elle-même.</p>
<h3 id="valider-le-flux-avec-un-backend-echo">Valider le flux avec un backend &quot;echo&quot;</h3>
<p>Pour vérifier concrètement ce qu'Envoy injecte (cookie, <code>Authorization</code>, headers custom), on peut s'appuyer sur l'application <a href="https://github.com/mendhak/docker-http-https-echo"><code>mendhak/http-https-echo</code></a>. Avec la variable <code>JWT_HEADER</code>, il décode le <a href="https://hub.docker.com/r/mendhak/http-https-echo#decode-jwt-header">JWT</a> d'un header donné et l'ajoute, claims lisibles, dans sa réponse JSON.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">metadata</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">      </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">        </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">echo</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mendhak/http-https-echo</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">JWT_HEADER</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Authorization&#34;</span><span class="w">   </span><span class="c"># décode l&#39;access token</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">          </span><span class="nt">ports</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span>- <span class="nt">containerPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">  </span><span class="nt">selector</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">    </span><span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">  </span><span class="nt">ports</span><span class="p">:</span><span class="w"> 
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">    </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">      </span><span class="nt">targetPort</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><ul>
<li>L'<strong>id_token</strong> est dans le cookie <code>IdTokenMySecureApp</code> → l'echo l'affiche <strong>brut, non décodé</strong> (à passer dans <a href="https://jwt.io">jwt.io</a> à la main, ou via <code>jwt decode</code>).</li>
<li>L'<strong>access token</strong> n'arrive dans <code>Authorization: Bearer …</code> <em>que si</em> <code>forwardAccessToken: true</code> → là <code>JWT_HEADER=Authorization</code> le décode automatiquement. C'est le cas d'usage idéal pour valider <code>forwardAccessToken</code>.</li>
</ul>
<p>Une fois authentifié en tant que membre du groupe autorisé — et avec les deux options activées (<code>forwardAccessToken: true</code>, <code>claimToHeaders</code>) — l'echo renvoie ceci :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonc" data-lang="jsonc"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;/&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;headers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nt">&#34;host&#34;</span><span class="p">:</span> <span class="s2">&#34;mysecureapp.gravitek.io&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nt">&#34;x-forwarded-proto&#34;</span><span class="p">:</span> <span class="s2">&#34;https&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nt">&#34;x-request-id&#34;</span><span class="p">:</span> <span class="s2">&#34;02d9c605-3391-48d7-ba70-9b7f95f0c0c2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="c1">// forwardAccessToken: true → l&#39;access token en Bearer
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&#34;authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer eyJhbGciOiJSUzI1NiIsInR5cCI…&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1">// posés par le filtre OIDC : access token + id_token
</span></span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nt">&#34;cookie&#34;</span><span class="p">:</span> <span class="s2">&#34;AccessToken-10c7e844=eyJhbGc…; IdTokenMySecureApp=eyJhbGc…&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="c1">// claimToHeaders : valeur du claim `groups`, encodée en base64
</span></span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nt">&#34;x-user-groups&#34;</span><span class="p">:</span> <span class="s2">&#34;WyIvYWRtaW5zIl0=&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="nt">&#34;method&#34;</span><span class="p">:</span> <span class="s2">&#34;GET&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">  <span class="c1">// JWT_HEADER=Authorization → l&#39;access token décodé par l&#39;echo
</span></span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="nt">&#34;jwt&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nt">&#34;payload&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nt">&#34;iss&#34;</span><span class="p">:</span> <span class="s2">&#34;https://sso.gravitek.io/realms/myrealm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">      <span class="nt">&#34;aud&#34;</span><span class="p">:</span> <span class="s2">&#34;account&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nt">&#34;azp&#34;</span><span class="p">:</span> <span class="s2">&#34;mysecureapp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">      <span class="nt">&#34;scope&#34;</span><span class="p">:</span> <span class="s2">&#34;openid groups&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">      <span class="nt">&#34;groups&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="s2">&#34;/admins&#34;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">      <span class="p">]</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Les trois mécanismes vus plus haut sont visibles d'un coup d'œil :</p>
<ul>
<li><strong><code>authorization: Bearer …</code></strong> → c'est <code>forwardAccessToken: true</code> qui l'a ajouté ; sans lui, ce header est absent.</li>
<li><strong><code>x-user-groups: WyIvYWRtaW5zIl0=</code></strong> → c'est <code>claimToHeaders</code> ; la valeur est le base64 du tableau JSON du claim (<code>echo WyIvYWRtaW5zIl0= | base64 -d</code> → <code>[&quot;/admins&quot;]</code>). Envoy n'envoie pas la valeur brute mais la représentation JSON encodée.</li>
<li><strong><code>jwt.payload.groups</code></strong> → l'echo a décodé l'access token (grâce à <code>JWT_HEADER=Authorization</code>) ; on y retrouve <code>groups</code>, c'est exactement le claim sur lequel la règle <code>authorization</code> a statué pour laisser passer la requête.</li>
</ul>
<p>Le cookie <code>IdTokenMySecureApp</code>, lui, contient l'id_token : c'est <em>lui</em> que le filtre JWT relit (via <code>extractFrom.cookies</code>) pour appliquer l'<code>authorization</code>, indépendamment de ce qui est transmis au backend.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Maintenant, avec une seule <code>SecurityPolicy</code>, on peut gérer l'<strong>authn OIDC, l'extraction JWT, l'authorization par claim</strong>, colocalisé avec la <code>HTTPRoute</code>. Plus de déploiement <code>oauth2-proxy</code>, plus de conf éparpillée. Plus simple, plus clair, j'aime bien !</p>
<p>Une fois le pattern en place, protéger une nouvelle app par groupe devient simple = un bloc <code>oidc</code>, un bloc <code>jwt</code>, une règle <code>authorization</code>, et go ! 🎉</p>
<p>Rendez-vous bientôt pour un prochain article autour d'Envoy Gateway et les Gateway API !</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Envoy Gateway - OIDC</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/security/oidc/">gateway.envoyproxy.io/docs/tasks/security/oidc</a> — inclut le warning sur le matching <code>redirectURL</code> / <code>HTTPRoute</code></li>
<li><strong>Envoy Gateway - JWT Authentication</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/security/jwt-authentication/">gateway.envoyproxy.io/docs/tasks/security/jwt-authentication</a></li>
<li><strong>Envoy Gateway - JWT Claim-Based Authorization</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/security/jwt-claim-authorization/">gateway.envoyproxy.io/docs/tasks/security/jwt-claim-authorization</a> — exemples avec <code>StringArray</code> et filtrage par claim</li>
</ul>]]></content:encoded></item></channel></rss>