-
Notifications
You must be signed in to change notification settings - Fork 248
A97: xDS JWT Call Credentials #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
3fed664
A97: xDS JWT Call Credentials
markdroth f7c8491
minor cleanups
markdroth 8d452ce
add mailing list link
markdroth 7e7160b
rename call creds class for clarity
markdroth 060ae03
add note about call creds config validation
markdroth 2901bd5
update type string in registry
markdroth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
A97: xDS JWT Call Credentials | ||
---- | ||
* Author(s): @markdroth | ||
* Approver: @ejona86, @dfawley | ||
* Status: {Draft, In Review, Ready for Implementation, Implemented} | ||
* Implemented in: <language, ...> | ||
* Last updated: 2025-06-05 | ||
* Discussion at: https://groups.google.com/g/grpc-io/c/D2S6cQvD6Tg | ||
|
||
## Abstract | ||
|
||
This document proposes a new call credential type for JWT tokens and the | ||
ability to configure that call credential for use in calls to the xDS | ||
server via the xDS bootstrap config. | ||
|
||
## Background | ||
|
||
As part of xDS fallback support ([A71]), gRPC switched from a single, | ||
globally shared XdsClient instance shared across all channels to | ||
a separate XdsClient for each data plane target. This means that | ||
if the application creates channels to multiple targets, the client | ||
will now create multiple concurrent connections to the xDS server. | ||
Unfortunately, this change has broken the ability to use proxyless gRPC | ||
in Istio due to a limitation in Istio's xDS proxy of supporting only one | ||
concurrent xDS client connection at a time, as per istio/istio#53532. | ||
|
||
Istio does not want to fix this limitation in their proxy, so we need a | ||
way for gRPC to work without the proxy. The only reason that gRPC uses | ||
the Istio proxy is that it allows gRPC to connect to the proxy locally | ||
using plaintext, and let the proxy figure out the authentication details | ||
for talking to the Istio xDS server remotely. Therefore, we need gRPC | ||
to handle those authentication details directly. | ||
|
||
Talking to the Istio xDS server requires two things. First, it requires | ||
configuring TLS credentials, as introduced in [A65]. And second, it | ||
requires use of a JWT token as a call credential, which is described in | ||
this document. | ||
|
||
### Related Proposals: | ||
* [A27: xDS-Based Global Load Balancing][A27] | ||
* [A65: mTLS Credentials in xDS Bootstrap File][A65] | ||
* [A71: xDS Fallback][A71] | ||
* [A83: xDS GCP Authentication Filter][A83] | ||
|
||
[A27]: A27-xds-global-load-balancing.md | ||
[A65]: A65-xds-mtls-creds-in-bootstrap.md | ||
[A71]: A71-xds-fallback.md | ||
[A83]: A83-xds-gcp-authn-filter.md | ||
|
||
## Proposal | ||
|
||
We will add a new type of call credentials called | ||
JwtTokenFileCallCredentials that loads a JWT token from a local file, | ||
with the ability to periodically reload the token. We will also provide | ||
hooks to configure the use of this new call credentials type in the xDS | ||
bootstrap file, which was originally described in [A27]. | ||
|
||
### JwtTokenFileCallCredentials | ||
|
||
Note: This section is intended for gRPC implementations that need to | ||
implement a new call credential type for loading JWT tokens from a file. | ||
Implementations that already support this functionality may continue to | ||
use their existing functionality, even if the behavior differs in small | ||
ways from what is described in this section. | ||
|
||
gRPC will support a JwtTokenFileCallCredentials call credentials type, | ||
which is not xDS-specific. The design for this call credential type | ||
is modeled after GcpServiceAccountIdentityCallCredentials, described in | ||
[A83]. | ||
|
||
JwtTokenFileCallCredentials will be instantiated with one parameter, | ||
the path to the file containing the JWT token. The credential object | ||
will handle loading the token on-demand and caching it based on the | ||
token's expiration time. | ||
|
||
To handle potential clock skew issues and to account for processing time | ||
on the server, the credential will set the cache expiration time to be | ||
30 seconds before the expiration time encoded in the token. All logic in | ||
the call credential code will use this modified expiration time instead | ||
of the expiration time encoded in the token. | ||
|
||
When the credential is asked for a token for a data plane RPC, if the | ||
token is not yet cached or the cached token will expire within some | ||
fixed refresh interval (typically 1 minute), the credential will start | ||
re-reading it from the file. | ||
|
||
When a data plane RPC starts, if the token is cached and is not expired, | ||
the token will immediately be added to the RPC, and the RPC will | ||
continue. Otherwise (i.e., before the token is initially obtained or | ||
after the cached token has expired), the data plane RPC will be queued | ||
until the file read completes. When the file read completes, the | ||
result (either success or failure, as described below) will be applied | ||
to all queued data plane RPCs. | ||
|
||
Note that when the token's expiration time is less than the refresh | ||
interval in the future, a new data plane RPC being started will trigger | ||
a new file read, but the cached token value will still be used for | ||
that data plane RPC. This pre-emptive re-fetching is intended to avoid | ||
periodic latency spikes when refreshing the token. | ||
|
||
If reading the file fails, all queued data plane RPCs will be failed | ||
with UNAVAILABLE status. | ||
|
||
If reading the file succeeds, the content of the file will contain the JWT | ||
token, which the client will cache. The client does not need to do full | ||
[RFC-7519](https://datatracker.ietf.org/doc/html/rfc7519) validation | ||
of the token (that is the responsibility of the server side), but it | ||
does need to extract the `exp` field for caching purposes. If the `exp` | ||
field cannot be extracted (i.e., the JWT token is invalid), all queued | ||
data plane RPCs will be failed with status UNAUTHENTICATED. Otherwise, | ||
the cache is updated, and the returned token is added to all queued data | ||
plane RPCs, which may then continue. | ||
|
||
If reading the file does not result in the cache being updated (i.e., | ||
if reading the file fails or if it returns an invalid JWT token), | ||
[backoff](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md) | ||
must be applied before the next attempt may be started. If a data plane | ||
RPC is started when there is no cached token available and while in | ||
backoff delay, it will be failed with the status from the last attempt | ||
to read the file. When the backoff delay expires, the next data plane | ||
RPC will trigger a new attempt. Note that no attempt should be started | ||
ejona86 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
until and unless a data plane RPC is started, since we do not want to | ||
unnecessarily retry if the channel is idle. The backoff state will be | ||
reset once the file has been read successfully. | ||
|
||
To add the token to a data plane RPC, the call credential will add | ||
a header named `authorization`. The header value will be the string | ||
`Bearer ` (note trailing space) followed by the token value. | ||
|
||
### Configuring Call Credentials in xDS Bootstrap File | ||
|
||
We will add a new field to the xDS bootstrap representation of an xDS | ||
server: | ||
|
||
```json5 | ||
{ | ||
"server_uri": <string containing URI of xds server>, | ||
|
||
// List of channel creds; client will stop at the first type it | ||
// supports. This field is required and must contain at least one | ||
// channel creds type that the client supports. | ||
"channel_creds": [ | ||
{ | ||
"type": <string containing channel cred type>, | ||
// The "config" field is optional; it may be missing if the | ||
// credential type does not require config parameters. | ||
"config": <JSON object containing config for the type> | ||
} | ||
], | ||
|
||
"server_features": [ ... ], | ||
|
||
// NEW FIELD! | ||
// List of call creds. Optional. Client will apply all call creds | ||
// types that it supports but will ignore any types that it does not | ||
// support. | ||
"call_creds": [ | ||
{ | ||
"type": <string containing call cred type>, | ||
// The "config" field is optional; it may be missing if the | ||
// credential type does not require config parameters. | ||
"config": <JSON object containing config for the type> | ||
markdroth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
], | ||
} | ||
``` | ||
|
||
The new "call_creds" field uses essentially the same format as the | ||
existing "channel_creds" field, but with the following key differences: | ||
- The supported types for call creds use a different namespace than the | ||
supported types for channel creds. | ||
- Unlike channel creds, where we must configure exactly one type, we can | ||
use multiple call creds instances together. Therefore, instead of | ||
stopping at the first supported type, the client will look through | ||
the entire list and use all types that it supports. | ||
- Unlike channel creds, call creds are optional, so if the call_creds | ||
field is not specified or does not contain any supported call creds | ||
type, the client will proceed without configuring any call creds. | ||
|
||
Just like in the "channel_creds" field, the "config" field is optional | ||
and can be omitted for a channel creds type that does not require any | ||
configuration. However, if present, the config must pass whatever | ||
validation is appropriate for the call creds type; if it does not, the | ||
bootstrap config will be considered invalid. | ||
|
||
For now, we will support only one type of call credentials, | ||
"jwt_token_file", whose configuration will look like this: | ||
|
||
```json5 | ||
{ | ||
// Path to JWT token file. Required. | ||
"jwt_token_file": <string>, | ||
} | ||
``` | ||
|
||
Implementations may implement a general-purpose registry of call | ||
credentials types, or they may simply hard-code the single supported type. | ||
|
||
### Temporary environment variable protection | ||
|
||
The new bootstrap fields will be guarded by the | ||
`GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS` environment variable. We | ||
do not plan to do interop testing for this feature, but we can remove | ||
the env var protection once we hear from OSS users that they have | ||
successfully used this functionality in Istio. | ||
|
||
## Rationale | ||
|
||
The alternative to this would have been for Istio to fix their xDS | ||
proxy, but they did not want to do so due to concerns about complexity. | ||
|
||
## Implementation | ||
|
||
Will be implemented in C-core, Java, Go, and Node. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Java implementation note: We can use JsonWebSignature.parse().getPayload().getExpirationTimeSeconds() to create an AccessToken with appropriate expiration. We could extend OAuth2Credentials and implement refreshAccessToken(). It takes care of stale vs expiration checking. Using JsonWebSignature is essentially re-implementing IdToken, which would probably work today, but technically this is not necessarily an IdToken so we should probably avoid the existing class.