title: Copying JWT claims to header using istio ingress/gateway
body: <p>I'm trying to implement adding JWT claims as request headers, using the undocumented <code>DYNAMIC_METADATA</code> feature as mentioned in <a href="https://github.com/istio/istio/issues/35108#issuecomment-916547087" rel="nofollow noreferrer">this github issue comment</a> and explained in more detail as an 'existing solution' in <a href="https://docs.google.com/document/d/1eJ4sPt5-fbXytSwov7senndMsrq4Et6qNYQKicLGHDs/edit" rel="nofollow noreferrer">this google doc feature proposal</a>. By the way, is there a place where these feature proposals are tracked? I would like to get more information about the state of this proposal, is it anywhere on the roadmap?</p>
  <p>The specific part I'm talking about this this:</p>
  <blockquote>
  <p>Solution 3: Use an undocumented feature in VirtualService<br />
  Istio VirtualService supports setting headers from dynamic attributes using the DYNAMIC_METADATA key. This can be combined with the JWT metadata to copy claims to headers.<br />
  See example below</p>
  <pre><code>apiVersion: networking.istio.io/v1alpha3
  kind: VirtualService
  metadata:
    name: reviews-route
  spec:
    hosts:
    - reviews.prod.svc.cluster.local
    http:
    - headers:
        request:
          set:
            # Copy the group claim to the x-istio-jwt-group header
            x-istio-jwt-group:
              '%DYNAMIC_METADATA([&quot;istio_authn&quot;, &quot;request.auth.claims&quot;, &quot;group&quot;])%'
  
  </code></pre>
  </blockquote>
  <p>As a POC, I have deployed an app which listens on <code>/echo-header</code> and will echo back the value of the <code>whatever-id</code> header of any request it receives. The app is called <code>web</code> and has an accompanying <code>kind: Service</code> called <code>web-svc</code>. I have configured several <code>AuthorizationPolicy</code> resources and one <code>RequestAuthentication</code> resource to make it so that this service can only be accessed with a valid JWT. This part works as I get a 401 when I make a request without JWT or with expired JWT. A <code>VirtualService</code> and <code>Gateway</code> resource make the service available for external requests. So far so good.</p>
  <p>My from from here on out is to copy the value of the <code>whatever-id</code> claim which exists on my JWT (verified on <a href="http://jwt.io/" rel="nofollow noreferrer">jwt.io</a>) into the <code>whatever-id</code> header.<br />
  I Have then modified the <code>VirtualService</code> as mentioned in the feature proposal like so:</p>
  <pre><code>apiVersion: networking.istio.io/v1alpha3
  kind: VirtualService
  metadata:
    name: web
  spec:
    hosts:
      - example.com
    gateways:
      - web-gateway
    http:
      - match:
          - uri:
              prefix: /
        route:
          - destination:
              host: web-svc
              port:
                number: 80
        headers:
          request:
            set:
              whatever-id: '%DYNAMIC_METADATA([&quot;istio_authn&quot;, &quot;request.auth.claims&quot;, &quot;whatever-id&quot;])%'
              # whatever-id: &quot;testing&quot;
  
  </code></pre>
  <p>This however does not work. I verified the header transformation part, by commenting/uncommenting the last 2 lines, after which I did the <code>testing</code> string back from my POC application. But when I use the config with the line that should copy the claim value into the header, the header is not set on the request.</p>
  <p>When I set logging to <code>rbac:debug</code> I can see the following log when making a request:</p>
  <pre><code>2022-02-23T14:04:05.708454Z     debug   envoy rbac      checking request: requestedServerName: outbound_.80_._.web-svc.route-echo.svc.cluster.local, sourceIP: 172.16.36.22:50722, directRemoteIP: 172.16.36.22:50722, remoteIP: 172.16.36.35:0,localAddress: 172.16.36.57:80, ssl: uriSanPeerCertificate: spiffe://cluster.local/ns/istio-ingress/sa/istio-ingress, dnsSanPeerCertificate: , subjectPeerCertificate: , headers: ':authority', 'example.com'
  ':path', '/echo-header'
  ':method', 'GET'
  ':scheme', 'https'
  ... &lt;removed irrelevant headers for brevity&gt;
  , dynamicMetadata: filter_metadata {
    key: &quot;envoy.filters.http.jwt_authn&quot;
    value {
      fields {
        key: &quot;&lt;redacted issuer url&gt;&quot;
        value {
          struct_value {
            ... &lt;removed all irrelevant claims for brevity&gt;
            fields {
              key: &quot;whatever-id&quot;
              value {
                string_value: &quot;98a5b3ea-f5e7-4012-8cf5-9cfb5cf9e65f&quot;
              }
            }
          }
        }
      }
    }
  }
  filter_metadata {
    key: &quot;istio_authn&quot;
    value {
      fields {
        key: &quot;request.auth.audiences&quot;
        value {
          string_value: &quot;account&quot;
        }
      }
      fields {
        key: &quot;request.auth.claims&quot;
        value {
          struct_value {
            ... &lt;removed all irrelevant claims for brevity&gt;
            fields {
              key: &quot;whatever-id&quot;
              value {
                list_value {
                  values {
                    string_value: &quot;98a5b3ea-f5e7-4012-8cf5-9cfb5cf9e65f&quot;
                  }
                }
              }
            }
          }
        }
      }
      fields {
        key: &quot;request.auth.presenter&quot;
        value {
          string_value: &quot;poc&quot;
        }
      }
      fields {
        key: &quot;request.auth.principal&quot;
        value {
          string_value: &quot;&lt;redacted&gt;&quot;
        }
      }
      fields {
        key: &quot;request.auth.raw_claims&quot;
        value {
          string_value: &quot;&lt;redacted&gt;&quot;
        }
      }
    }
  }
  
  </code></pre>
  <p>First thing I noticed when looking at this, is that the <code>dynamicMetadata</code> (which I assumed can be accessed by using the <code>%DYNAMIC_METADATA()%</code> synxtax as mentioned in the solution) does not contain an <code>istio_authn</code> field, the only field it contains is the <code>envoy.filters.http.jwt_auth</code> field, which has the key value equal to the issuer URL of my JWT, and under the value belonging to that key, there is a <code>whatever-id</code> field, which has the value which I want to add to my header, so I tried changing the header value in my VirtualService to:</p>
  <pre><code>whatever-id: '%DYNAMIC_METADATA([&quot;envoy.filters.http.jwt_authn&quot;, &quot;&lt;issuer url&gt;&quot;, &quot;whatever-id&quot;])%'
  
  </code></pre>
  <p>Sadly this did not work either.</p>
  <p>What I also noticed, is that the keys used in the solution (<code>&quot;istio_authn&quot;, &quot;request.auth.claims&quot;</code>) are available under the <code>filter_metadata</code> property of the log (as opposed to the <code>dynamicMetadata</code> property).<br />
  So maybe it is possible to use another syntax to access those <code>filter_metadata</code> properties?</p>
  <p>Environment details:</p>
  <ul>
  <li>Kubernetes version 1.22.6</li>
  <li>Using Azure AKS</li>
  <li>Istio 1.13.0 deployed using helm chart</li>
  </ul>