I’m trying to implement adding JWT claims as request headers, using the undocumented DYNAMIC_METADATA
feature as mentioned in this github issue comment and explained in more detail as an ‘existing solution’ in this google doc feature proposal. 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?
The specific part I’m talking about this this:
Solution 3: Use an undocumented feature in VirtualService
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.
See example belowapiVersion: 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(["istio_authn", "request.auth.claims", "group"])%'
As a POC, I have deployed an app which listens on /echo-header
and will echo back the value of the whatever-id
header of any request it receives. The app is called web
and has an accompanying kind: Service
called web-svc
. I have configured several AuthorizationPolicy
resources and one RequestAuthentication
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 VirtualService
and Gateway
resource make the service available for external requests. So far so good.
My from from here on out is to copy the value of the whatever-id
claim which exists on my JWT (verified on jwt.io) into the whatever-id
header.
I Have then modified the VirtualService
as mentioned in the feature proposal like so:
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(["istio_authn", "request.auth.claims", "whatever-id"])%'
# whatever-id: "testing"
This however does not work. I verified the header transformation part, by commenting/uncommenting the last 2 lines, after which I did the testing
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.
When I set logging to rbac:debug
I can see the following log when making a request:
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'
... <removed irrelevant headers for brevity>
, dynamicMetadata: filter_metadata {
key: "envoy.filters.http.jwt_authn"
value {
fields {
key: "<redacted issuer url>"
value {
struct_value {
... <removed all irrelevant claims for brevity>
fields {
key: "whatever-id"
value {
string_value: "98a5b3ea-f5e7-4012-8cf5-9cfb5cf9e65f"
}
}
}
}
}
}
}
filter_metadata {
key: "istio_authn"
value {
fields {
key: "request.auth.audiences"
value {
string_value: "account"
}
}
fields {
key: "request.auth.claims"
value {
struct_value {
... <removed all irrelevant claims for brevity>
fields {
key: "whatever-id"
value {
list_value {
values {
string_value: "98a5b3ea-f5e7-4012-8cf5-9cfb5cf9e65f"
}
}
}
}
}
}
}
fields {
key: "request.auth.presenter"
value {
string_value: "poc"
}
}
fields {
key: "request.auth.principal"
value {
string_value: "<redacted>"
}
}
fields {
key: "request.auth.raw_claims"
value {
string_value: "<redacted>"
}
}
}
}
First thing I noticed when looking at this, is that the dynamicMetadata
(which I assumed can be accessed by using the %DYNAMIC_METADATA()%
synxtax as mentioned in the solution) does not contain an istio_authn
field, the only field it contains is the envoy.filters.http.jwt_auth
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 whatever-id
field, which has the value which I want to add to my header, so I tried changing the header value in my VirtualService to:
whatever-id: '%DYNAMIC_METADATA(["envoy.filters.http.jwt_authn", "<issuer url>", "whatever-id"])%'
Sadly this did not work either.
What I also noticed, is that the keys used in the solution ("istio_authn", "request.auth.claims"
) are available under the filter_metadata
property of the log (as opposed to the dynamicMetadata
property).
So maybe it is possible to use another syntax to access those filter_metadata
properties?