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

backend: oidc: impersonate instead of forwarding oidc token to k8s api #2814

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

iSE-stefan-may
Copy link

@iSE-stefan-may iSE-stefan-may commented Jan 30, 2025

By default, the OIDC JWT is forwarded to the Kubernetes API server. In some cases it might not be possible to have the Kubernetes API server configured with OIDC, in these cases the OIDC JWT should be evaluated by Headlamp and the request should use the service account of the pod together with impersonating the user in the token.

Related issue:

Testing

To test this PR, do the following:

  1. build the container image
  2. follow the documentation at https://headlamp.dev/docs/latest/installation/in-cluster/keycloak/ for setting up a keycloak client and configuring a clusterrole binding (do NOT follow the steps for setting up minikube)
  3. deploy the helm chart with the oidc values and oidc.impersonate=true and oidc.impersonateClaim=preferred_username using the container image from step 1
  4. go to headlamp and login with oidc

Copy link

@zivcex zivcex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lg2m

Copy link
Collaborator

@illume illume left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello. Thanks for this!

I left a few questions/notes.

Can you please add some steps in the PR description on how we can test this?

Would it be possible for you to add some test cases?

}

idToken, err := c.getValidToken(r.Context(), token)
if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add some logging in the error conditions?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging added, but I'm not really sure, whether it is really the best case to log always when the token validation fails. It may also be, that OIDC impersonation is active, but the user still used a serviceaccount token, in those cases a warning in the log would be irritating.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. That sounds like it makes sense to remove.

Is something like this possible? Otherwise remove that logging?

if ! OIDCImpersonationEnaled and serviceAccount {
    log()
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wether the user provided token is a kubernetes serviceaccount token can only be evaluated from the Kubernetes API, therefore this check won't be possible. Only option where this would be possible would be, if we disable the option to login in the ui via a serviceaccount token altogether. In this case we could give a log warning here, but as far as I can see, that's currently not possible.
I'll cleanup the logging for the cases, where it may not be an error case.

func StartHeadlampServer(config *HeadlampConfig) {
handler := createHeadlampHandler(config)

handler = config.OIDCTokenRefreshMiddleware(handler)
if config.oidcImpersonate {
handler = config.OIDCImpersonateMiddleware(handler)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain why this middleware is after the OIDCTokenRefreshMiddleware?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the most fitting position I thought of, but not a particular reason. Do you have better recommendations?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering how it would interact with the other middleware? Wondering if it would make sense to go before OIDCTokenRefreshMiddleware or not?

Because would the token refresh need to use the impersonated stuff? If that's the case I was wondering if the OIDCImpersonateMiddleware should go first?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, the OIDCTokenRefreshMiddleware needs the original token, therefore it must be evaluated before OIDCImpersonateMiddleware. I'll change the order of those two and add a comment.

@@ -166,6 +168,10 @@ func flagset() *flag.FlagSet {
f.String("oidc-idp-issuer-url", "", "Identity provider issuer URL for OIDC")
f.String("oidc-scopes", "profile,email",
"A comma separated list of scopes needed from the OIDC provider")
f.Bool("oidc-impersonate", false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@knrt10 would this need to be added to the helm chart as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the helm chart, however I'm not convinced that I did it in a good way, since the chart is a little bit confusing for me. So please recheck.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our main helm chart expert is on holidays at the moment. So this will take a bit longer to review. Bear with me :)

@illume
Copy link
Collaborator

illume commented Feb 5, 2025

Signed-off-by: Stefan May <stefan.may@ise-online.com>
@iSE-stefan-may
Copy link
Author

Have updated some code regarding the comments, but not fully tested yet and not added a test description. Will work on that tomorrow, but perhaps you can already review the answers to the comments.

@audunsolemdal
Copy link

Hi, not an expert here. I am wondering if this implementation will support impersonation based on groups listed in the groups claim found in the token returned from the OIDC provider?

@illume
Copy link
Collaborator

illume commented Feb 7, 2025

Have updated some code regarding the comments, but not fully tested yet and not added a test description. Will work on that tomorrow, but perhaps you can already review the answers to the comments.

Thanks. Reading through it now :)

@@ -147,11 +177,21 @@ spec:
# Check if scopes are non empty either from env or oidc.config
- "-oidc-scopes=$(OIDC_SCOPES)"
{{- end }}
{{- if or ($oidc.impersonate) ($impersonate) }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey great PR!!

I'm wondering would it be too heavy if you can provide the groups as well here within the chart and deployment?

more details are here:
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation:~:text=Impersonate%2DGroup%3A%20A%20group%20name%20to%20act%20as.%20Can%20be%20provided%20multiple%20times%20to%20set%20multiple%20groups.%20Optional.%20Requires%20%22Impersonate%2DUser%22.

Convention here is that groups claim in JWT hold the user group coming in from IDP. It would be good if it was configurable as some IDPs send the groups membership under other claim name.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added functionality to use groups too and changed the parameters therefore to -oidc-user-claim and -oidc-groups-claim. Also added a filter for additional impersonate-* headers to prevent any authentication attacks, since the service account token is now used.

@illume
Copy link
Collaborator

illume commented Feb 8, 2025

It looks like the backend/lint CI job is failing.

You can run make backend-lint locally.

@illume
Copy link
Collaborator

illume commented Feb 8, 2025

The helm template job is having an issue: https://github.com/headlamp-k8s/headlamp/actions/runs/13197046279/job/36885634312?pr=2814

Template test failed for ./charts/headlamp/tests/test_cases/oidc-create-secret.yaml against ./charts/headlamp/tests/expected_templates/oidc-create-secret.yaml:
--- ./charts/headlamp/tests/oidc-create-secret.yaml_output/rendered_templates.yaml	2025-02-08 02:48:20.528327104 +0000
+++ ./charts/headlamp/tests/expected_templates/oidc-create-secret.yaml	2025-02-08 02:48:20.536327[13](https://github.com/headlamp-k8s/headlamp/actions/runs/13197046279/job/36885634312?pr=2814#step:5:14)6 +0000
@@ -126,11 +126,6 @@
                 secretKeyRef:
                   name: oidc
                   key: scopes
-            - name: OIDC_IMPERSONATE_CLAIM
-              valueFrom:
-                secretKeyRef:
-                  name: oidc
-                  key: impersonateClaim
make: *** [Makefile:[18](https://github.com/headlamp-k8s/headlamp/actions/runs/13197046279/job/36885634312?pr=2814#step:5:19)5: helm-template-test] Error 1
           args:
             - "-in-cluster"
             - "-plugins-dir=/headlamp/plugins"
Error: Process completed with exit code 2.

To test the charts locally

make helm-template-test

Signed-off-by: Stefan May <stefan.may@ise-online.com>
@mlbiam
Copy link

mlbiam commented Feb 11, 2025

By default, the OIDC JWT is forwarded to the Kubernetes API server. In some cases it might not be possible to have the Kubernetes API server configured with OIDC, in these cases the OIDC JWT should be evaluated by Headlamp and the request should use the service account of the pod together with impersonating the user in the token.

This is not a good idea. Dashboards should never be configured with privileged token. it's too easy to expose a dashbaord by accident. Even when impersonating the user, headlamp should rely on the token from the proxy, not its own. This way someone accidentally exposes the dashbaord outside of the proxy, they haven't created a vector to access the API server with a privleged account.

@illume
Copy link
Collaborator

illume commented Feb 11, 2025

This is not a good idea. Dashboards should never be configured with privileged token. it's too easy to expose a dashbaord by accident. Even when impersonating the user, headlamp should rely on the token from the proxy, not its own. This way someone accidentally exposes the dashbaord outside of the proxy, they haven't created a vector to access the API server with a privleged account.

It is a good idea to try and help people prevent making mistakes with configuration accidentally opening things up.

Also, when hearing about other peoples use cases it is often the situation that they are best placed to evaluate and decide why they want to do things in a particular way.

@iSE-stefan-may maybe you could give us some ideas about this? Could you elaborate on your use case a bit? Also, what do you think about this possibility of opening things up accidentally and how we could address this?

@joaquimrocha
Copy link
Collaborator

By default, the OIDC JWT is forwarded to the Kubernetes API server. In some cases it might not be possible to have the Kubernetes API server configured with OIDC, in these cases the OIDC JWT should be evaluated by Headlamp and the request should use the service account of the pod together with impersonating the user in the token.

This is not a good idea. Dashboards should never be configured with privileged token. it's too easy to expose a dashbaord by accident. Even when impersonating the user, headlamp should rely on the token from the proxy, not its own. This way someone accidentally exposes the dashbaord outside of the proxy, they haven't created a vector to access the API server with a privleged account.

So far we have avoided requiring or encouraging running Headlamp with its own token/role. We did have some users asking for this and we resorted to advising to use a reverse proxy. We even have thought of maybe having a side project to facilitate this for Headlamp; would this bring any advantage here?

@mlbiam I'd like to ask your opinion whether this is still a security concern if we add the feature as optional and somehow make it clear, via CLI options' names, extra options, and documentation what the implications of this are. What would that look like?

@mlbiam
Copy link

mlbiam commented Feb 14, 2025

We did have some users asking for this and we resorted to advising to use a reverse proxy.

Just because users want it doesn't mean its a good idea. Users in general tend to rely on what they can easily control, and a port-forward to a dashboard that is privileged is something they control. Unfortunately, it's also something anyone else can control too! This was how Tesla was breached (https://www.trendmicro.com/vinfo/us/security/news/cybercrime-and-digital-threats/tesla-and-jenkins-servers-fall-victim-to-cryptominers) and has led to the assumption by security teams that the Kubernetes dashboard is "insecure". It's also the reason why the Kubernetes Dashboard moved away from being able to use its own ServiceAccount token and requiring a reverse proxy to inject a token in the 7.x version.

Headlamp already has a "local" mode. If users don't want to do it "correctly" in a centralized way, they should use the local version.

We even have thought of maybe having a side project to facilitate this for Headlamp; would this bring any advantage here?

easier with good security is always better. That said, i don't know you need a new project. OpenUnison does this today. You can also use oauth2proxy+kube-oidc-proxy too if you don't want to use openunison: https://www.tremolo.io/post/kubernetes-authentication-comparing-solutions

That said, you're essentially talking about rolling your own authentication code, which i'd recommend against. Best to invest time in good docs, better helm charts, etc, for existing projects to make this easier. The hard part isn't the proxies, it's setting up DNS, TLS, integration with a authentication team that is in a different silo in your org, etc. Creating another project won't fix those issues, just create one more thing to support.

I'd like to ask your opinion whether this is still a security concern if we add the feature as optional and somehow make it clear, via CLI options' names, extra options, and documentation what the implications of this are

I'd still say this is an anti-pattern. All the red flags and warnings won't stop users from doing the easy thing. ie: clicking through a security warning for an invalid certificate. It will only take one major breach for Headlamp to earn an "insecure" badge.

The Kubernetes Dashbaord moved awat from this pattern for 7.x and has received considerable pushback. Their answer (which I think is correct), is setup a reverse proxy that hard codes an SA in it. It's terribly insecure, but so's running the dashboard with a privileged sa. It at least makes it harder to do the insecure thing and protects the project's reputation.

@zivcex
Copy link

zivcex commented Feb 19, 2025

Hi @iSE-stefan-may will you close this one given the new circumstances? Or leave this setting optional?

}
r.Header.Set("Impersonate-User", user)
for _, group := range groups {
r.Header.Set("Impersonate-Group", group)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should use Add instead of Set.

Suggested change
r.Header.Set("Impersonate-Group", group)
r.Header.Add("Impersonate-Group", group)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size:L This PR changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants