TT Lab
Get started
Learn Learning paths Courses

Sessions and Tokens — From the Browser to the Mesh

Why verify JWTs when you already have mTLS

Continue in TT Lab

In one line

In a service mesh, "who made the request" has two answers. An mTLS certificate proves which workload opened the connection, and a JWT proves on whose behalf a person is making the request. The sidecar proxy can verify both, but neither can stand in for the other.

Why this was needed

Until the earlier module, token verification was the application code's job. With twenty services there is the same verification code in twenty places, and one of them leaves out the aud check or a library update lags a beat. A mesh moves this verification into a proxy that stands in front of every service. The verification rules become a setting in one place, and a wrong token is cut off before it even reaches the application code.

But once you bring in a mesh, a misconception often arises. "mTLS is already on between the services, so authentication is done." What mTLS proves is the workload that opened the connection — the fact that what attached to the order service was the frontend Pod. Whether that request is Alice's or Bob's is not in the certificate. This is why the Istio security concepts doc explains authentication as split into peer authentication (service to service) and request authentication (the end user).

How it works

Envoy's jwt_authn filter verifies the signature, issuer, and audience. It receives the public keys as a JWK Set, and instead of a remote URL you can also give a file or an inline string (local_jwks). The token header's kid picks which key to use — thanks to this, you can keep the old and new keys together in one JWKS when you rotate keys.

The fields this module uses from the configuration reference are these.

forward                 기본값 false. 검증에 성공하면 원래 토큰을 요청에서 지운다.
forward_payload_header  검증된 페이로드를 base64url(JSON) 으로 이 헤더에 실어 보낸다.
payload_in_metadata     헤더 대신 동적 메타데이터에 넣는다(RBAC 같은 다음 필터용).
rules                   경로별 요구. 처음 맞는 규칙이 이기고, requires 가 비면 검증하지 않는다.
allow_missing_or_failed 없든 틀리든 통과시킨다 — 잘못 쓰면 필터가 장식이 된다.

The reason forward is off by default matters. Upstream only needs to receive the claims that have already been verified, and an upstream that receives the original token could reuse that token on another service. The shorter the distance the token travels, the fewer places it can leak from.

The rejection codes are also distinguished. If the token is missing, the signature is wrong, or it has expired, it is 401, and if the signature is right but the aud is not this service, it is 403. RFC 7519 says that if the processing party is not in the aud, it must reject that JWT (MUST).

On the mTLS side, it is the listener's TLS setting. If you turn on require_client_certificate, it rejects a connection without a valid client certificate. The information of the verified certificate goes to upstream in the x-forwarded-client-cert (XFCC) header. The URI key carries the certificate's URI SAN, and in a mesh a SPIFFE ID in the shape spiffe://신뢰도메인/... (the placeholder is the trust domain). The HCM's forward_client_cert_details defaults to SANITIZE, which discards the XFCC it received, and SANITIZE_SET, on an mTLS connection, discards what it received and refills it fresh with the values it verified itself.

What it looks like in the field

The most common accident is an upstream that "trusts the header". The upstream trusts x-jwt-payload as the identity, but if a client sends that header directly over a public path where the proxy skips verification, the upstream believes the fake identity as is. On a verified path, Envoy overwrites it with the verified value, but on a public path no one touches it. So on a public path you explicitly remove that header.

The second is a request with no token. The Istio docs say that if you put only a RequestAuthentication, a request with no token passes by default, and that to reject it you should put an authorization rule separately. "If there is a token, check it" and "a token must be present" are different settings. When you write it directly in Envoy, a rule with requires is that difference.

What you will do in the next lab

You make an RS256 key and a JWKS with cryptography and sign tokens by hand. You attach jwt_authn to Envoy and confirm that only a valid token reaches upstream, that a missing, forged, or expired one is cut off in front of upstream with a 401, and that a different audience is cut off with a 403. You pass the verified claims as a header and remove an imitated header on the public path. Finally you add an mTLS chain and see the workload identity and the user identity arrive separately.