Skip to content
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

[Feature]: JWT validation only mode #2634

Open
jkroepke opened this issue May 6, 2024 · 8 comments
Open

[Feature]: JWT validation only mode #2634

jkroepke opened this issue May 6, 2024 · 8 comments

Comments

@jkroepke
Copy link

jkroepke commented May 6, 2024

Motivation

I would like to use oauth2-proxy to validate OIDC compatible tokens only. (JWT auth)

Use-Case: Validate Service Account Tokens from Kubernetes

Currently, this is not possible, because client ID/client secret is mandatory.

Possible solution

No response

Provider

None

@matzegebbe
Copy link

Sorry for the advertising, for this challenge I have published this tool. You might want to fork it and rebuild it locally
https://github.com/matzegebbe/web-jwks-validator

@CesarStef
Copy link

Hi @jkroepke, can you put an example of token that you want to validate?
I'm asking this because if you want to validate a token that follow the standard described in the page ServiceAccount token volume projection, I think that if and only if k8s sign the token (it's not visible in the example and I don't have any cluster to try it now) this is theorically already working with some tricks in the configs.

The idea is that you set the client-id value as the same of the audience of the token ("aud" claim), disable the login page, activate the function to check bearer token and put a random value as client-secret(this will never be used).

Let me know if this was helpful or not 😄

@jkroepke
Copy link
Author

Hey @CesarStef

thanks for your detailed answers.

Yes, I want to valid JWT issued by Service Account and secondly I also want to validate tokens issued by Entra ID that are coming trough Managed Identity (like AWS Instance Roles, but the token is an OIDC compatible JWT), Ref: https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http

In case you are not familiar with Azure Managed Identity, you can assume its the same concept as Kubernetes Service Accounts, but only for Virtual Machines and the token is available via http.

The only difference here is that the aud claim is not fully free to choice. In the case, the audience claim must be pre-registered on the Entra ID first.

Additionally, I also would like to authenticate JWT tokens from different sources. While I'm fully flexible at Kubernetes side, for Entra ID I have to use the same audience claim at Entra ID side.

I'm also missing the client ID verification.

The idea is that you set the client-id value as the same of the audience of the token ("aud" claim)

But not secure! The audience value at the ServiceAccount token volume projection is not validated. Any user could create a service account token with the "correct" audience value to successfully validate against oauth2-proxy.

@CesarStef
Copy link

CesarStef commented May 14, 2024

Hi @jkroepke,

Sorry for the late response!

But not secure! The audience value at the ServiceAccount token volume projection is not validated. Any user could create a service account token with the "correct" audience value to successfully validate against oauth2-proxy.

The audience check is only a part of all the process to validate a token.

In my previous message I putted a condition as necessary to make the validation work:

I think that if and only if k8s sign the token (it's not visible in the example and I don't have any cluster to try it now) this is theorically already working with some tricks in the configs.

Attackers can't simply put a valid clientId in the token because oauth2-proxy need to verify that the signature of the token is really made by the correct issuer.
Every IdP use their private key to sign their access token and give you an endpoint to find their public key to verify that a token is really issued by them.

If somebody manipulate the token, the signature just change and the token is invaid.

oauth2-proxy (at least for the oidc standard provider) follow the standard and checks three things to validate the token:

  1. The iss claim (issuer) must be the correct one
  2. The aud claim must contain the clientID (you can change the claim to check)
  3. The signature is really made by the issuer

Additionally, I also would like to authenticate JWT tokens from different sources.

As I know this can't be done now but there are some PR in working to make this possible (maybe some of maintainers can help more then me on this point).

@jkroepke
Copy link
Author

jkroepke commented May 14, 2024

Attackers can't simply put a valid clientId in the token because oauth2-proxy need to verify that the signature of the token is really made by the correct issuer.
Every IdP use their private key to sign their access token and give you an endpoint to find their public key to verify that a token is really issued by them.

This is true, but audience is a free-text value field at kubernetes. On kubernetes, you get signed a token with any aud value of your choice.

For example:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/tokens
      name: vault-token
  serviceAccountName: build-robot
  volumes:
  - name: vault-token
    projected:
      sources:
      - serviceAccountToken:
          path: vault-token
          expirationSeconds: 7200
          audience: vault

In that case, the aud value is vault here. As I know, I can put any value of my choice here and it gets signed.

oidc standard provider

But Kubernetes isn't OIDC standard provider and doesn't want to be, maybe doesn't have to be.

In terms of OAuth2, the aud claim has a different purpose (https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3)

The "aud" (audience) claim identifies the recipients that the JWT is intended for.

That the reason, why the aud claim in Kubernetes and Entra ID is free to choice (Note: Not talking about end-user token, more token about identity (machine) tokens). For example in Entra ID, the aud claim must be https://management.azure.com/, if I want to use the token to authenticated against https://management.azure.com/ and has no link against the clientID

From OAuth2 point of view, there is link to the clientID.

Based on your comment, extra-jwt-issuers is designed for OIDC compatible provider and should not used for OAuth2 Providers, like Kubernetes.

A flag like extra-jwt-issuers, but using the sub claim instead the aud claim would be feasible solution.

@CesarStef
Copy link

CesarStef commented May 16, 2024

Hi @jkroepke,

I'm not a maintainer of this app but just an user so everything I will say can be completly wrong and/or not in line with their ideas on this topic :)

Said so,

I think that we are missing the point, because "aud" is not really so important as much the signature:

  1. You are pointing out the RFC 7519 but this is for JWTs, while the OAuth2.0 RFC is the number 6749.
    So, to be OAuth2 complaint, Kubernetes must follow some other rules.
    In particular, the RFC 6749 Section 10.3 says:

    The authorization server MUST ensure that access tokens cannot be
    generated, modified, or guessed to produce valid access tokens by
    unauthorized parties.

  2. The JWTs RFC 7519 Section 4.1.3 also say this:

The following Claim Names are registered in the IANA "JSON Web Token
Claims" registry established by Section 10.1.
None of the claims defined below are intended to be mandatory to use or implement in all
cases, but rather they provide a starting point for a set of useful,
interoperable claims. Applications using JWTs should define which
specific claims they use and when they are required or optional. All
the names are short because a core goal of JWTs is for the
representation to be compact.

(This part is the reason that allow some OAuth2 Authorization Server to not use "aud" at all in their access token, for example: AWS Cognito)
So this:

From OAuth2 point of view, there is link to the clientID.

It's just a common practice, but there a OAuth2 authorization Server like AWS Cognito that don't have "aud" at all in their access tokens (but have a custom claim called "client_id"...)

  1. And in the [ JWT RFC Section 7] (https://www.rfc-editor.org/rfc/rfc7519#section-7) the explicit say that you need to validate the signature of the token

Based on your comment, extra-jwt-issuers is designed for OIDC compatible provider and should not used for OAuth2 Providers, like Kubernetes.

Sorry I misspoke, ".well-known" is part of the OAuth2 specification(s) (https://datatracker.ietf.org/doc/html/rfc8414#section-3.2)

So in the end, if Kubernetes don't have a ".well-known" endpoint is theorically not OAuth2 complaint I think.

But the meantime I found out that Kubernetes sign the token and theorically you can have the key:
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#service-account-tokens

So theorically, the problem now is to make them accessible by URL from your oauh2-proxy istance.

A flag like extra-jwt-issuers, but using the sub claim instead the aud claim would be feasible solution.

No, because you still have the signature problem and to that you must add the problem that you can't know the "sub" value before that your authorization server generate at least one token for that "user".

The idea behind having "aud" == "client_id" is to bind the audience to a client and tipically, a client is an application.

@jkroepke
Copy link
Author

Hey @CesarStef

In particular, the RFC 6749 Section 10.3 says:

I think, Kubernetes is compliant here. Only users with edit permissions are able to generate such tokens.

It's just a common practice, but there a OAuth2 authorization Server

Common practice isn't a spec.

The JWTs RFC 7519 Section 4.1.3 also say this:

I can't find the quote you mention in that section, however the section clearly describes what the ident of the aud claim is. A URL is common for the aud name.

In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value.

In that scenario, multiple clients from multiple issue would have the same aud claim value for my endpoint.

I agree, that aud claim is optional. But nothing is mention here about a client ID. Kubernetes service accounts doesn't have a client ID.

So in the end, if Kubernetes don't have a ".well-known" endpoint is theorically not OAuth2 complaint I think.
So theorically, the problem now is to make them accessible by URL from your oauh2-proxy istance.

Kubernetes has a .well-known endpoint + JWKS endpoints.

https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-issuer-discovery

to that you must add the problem that you can't know the "sub" value before that your authorization server generate at least one token for that "user".

For Kubernetes, the subject value is the service account name + namespace.
For Microsoft Entra ID, the subject value is the client id of the managed identity.
For GitHub Actions, the subject value has the format repo:<orgName/repoName>:environment:<environmentName>.
For Gitlab CI, the subject value has the format project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}.

The idea behind having "aud" == "client_id" is to bind the audience to a client and tipically, a client is an application.

That a good idea, but not mention in the spec. Other scenarios should be supported as well.


The OIDC spec describes scenarios, where a end user needs to be authenticated.

Mention that for machine based authentication, where non-human identities gets an issued token by default or can request one on demand.

For example, in Github Actions having access to an token API to get an signed JWT token where is aud claim is free to choice.

There is no client credential or code authorization flow to get an token with an registered client on behalf. Instead a non standardized/proprietary flow is used. It's just curl (in case of Github/Entra ID), cat (Kubernetes) or env variable (Gitlab CI) to get an signed token.

In case of Github Action, there isn't a client id. because there is not client. sub claim is commonly used to get an stable identifier.

@CesarStef
Copy link

CesarStef commented May 18, 2024

Hi @jkroepke,

Kubernetes has a .well-known endpoint + JWKS endpoints.

Perfect, so you can validate Kubernetes without problem.

That a good idea, but not mention in the spec. Other scenarios should be supported as well.

I completly agree with you and if you look at alpha configs you will see that the maintainers tried to allow other scenarios.
There is the possibility to change the claim to which oauth2-proxy will try to match the clientId with the optionaudienceClaims and then, you can add more "audiences" with extraAudiences but for now, to let the system use extraAudiences you need the old option --skip-jwt-bearer-tokens set to true ( I expect that you already set this option to true ).
So in these way you can can validate the sub claim if you want.

But there are one big problems with alpha configs now:
You can't have more than one provider active at the same time, and so there is no way to validate tokens from two or more issuers at the same time.
(I hope that some PR will be merged about this soon, because by my opinion this is a big usability issue)

You will still have the "clientID" problem, but if you want to use oauth2-proxy only as resource-server + reverse-proxy it's just a naming problem.

I know that the "clientID" parameter name is missleading, but I can see why the maintainers had decided to not create another config parameter with the exactly same scope just for the resource-server only scenario.
Maybe, if there were an option to completely disable the client part, then this would be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants