-
Notifications
You must be signed in to change notification settings - Fork 380
backend: oidc: impersonate instead of forwarding oidc token to k8s api #2814
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
base: main
Are you sure you want to change the base?
Changes from all commits
a1bbd62
bd747b2
53fadbe
039dde1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -54,6 +54,10 @@ | |||||
oidcClientID string | ||||||
oidcClientSecret string | ||||||
oidcIdpIssuerURL string | ||||||
oidcImpersonate bool | ||||||
oidcUserClaim string | ||||||
oidcGroupsClaim string | ||||||
oidcProvider *oidc.Provider | ||||||
baseURL string | ||||||
oidcScopes []string | ||||||
proxyURLs []string | ||||||
|
@@ -366,7 +370,7 @@ | |||||
if config.useInCluster { | ||||||
context, err := kubeconfig.GetInClusterContext(config.oidcIdpIssuerURL, | ||||||
config.oidcClientID, config.oidcClientSecret, | ||||||
strings.Join(config.oidcScopes, ",")) | ||||||
strings.Join(config.oidcScopes, ","), config.oidcImpersonate) | ||||||
if err != nil { | ||||||
logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context") | ||||||
} | ||||||
|
@@ -896,9 +900,112 @@ | |||||
}) | ||||||
} | ||||||
|
||||||
// OIDCImpersonateMiddleware will validate and exchange the authorization JWT from the header with | ||||||
// the kubernetes service account JWT and instead impersonate the user using the `Impersonate-User` | ||||||
// header. | ||||||
// The headlamp service account must have the corresponding roles, see | ||||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation | ||||||
// If the token cannot be validated, the request will be forwarded to the next handler. | ||||||
func (c *HeadlampConfig) OIDCImpersonateMiddleware(next http.Handler) http.Handler { | ||||||
oidcProvider, err := oidc.NewProvider(context.Background(), c.oidcIdpIssuerURL) | ||||||
if err != nil { | ||||||
logger.Log(logger.LevelError, map[string]string{"idpIssuerURL": c.oidcIdpIssuerURL}, | ||||||
err, "failed to get oidc provider") | ||||||
return next | ||||||
} | ||||||
|
||||||
c.oidcProvider = oidcProvider | ||||||
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
// skip if not cluster request | ||||||
if !strings.HasPrefix(r.URL.String(), "/clusters/") { | ||||||
next.ServeHTTP(w, r) | ||||||
return | ||||||
} | ||||||
|
||||||
// parse cluster and token | ||||||
cluster, token := parseClusterAndToken(r) | ||||||
if cluster == "" || token == "" { | ||||||
next.ServeHTTP(w, r) | ||||||
return | ||||||
} | ||||||
|
||||||
idToken, err := c.getValidToken(r.Context(), token) | ||||||
if err != nil { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please add some logging in the error conditions? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
next.ServeHTTP(w, r) | ||||||
return | ||||||
} | ||||||
|
||||||
var claims map[string]interface{} | ||||||
if err := idToken.Claims(&claims); err != nil { | ||||||
logger.Log(logger.LevelWarn, nil, err, "token claims could not be extracted") | ||||||
next.ServeHTTP(w, r) | ||||||
return | ||||||
} | ||||||
user, ok := claims[c.oidcUserClaim].(string) | ||||||
if !ok || user == "" { | ||||||
logger.Log(logger.LevelWarn, nil, err, "no user found in token") | ||||||
next.ServeHTTP(w, r) | ||||||
return | ||||||
} | ||||||
var groups []string | ||||||
if c.oidcGroupsClaim != "" { | ||||||
switch v := claims[c.oidcGroupsClaim].(type) { | ||||||
case string: | ||||||
groups = []string{v} | ||||||
case []string: | ||||||
groups = v | ||||||
} | ||||||
} | ||||||
|
||||||
context, err := c.kubeConfigStore.GetContext(cluster) | ||||||
if err != nil { | ||||||
logger.Log(logger.LevelError, map[string]string{"cluster": cluster}, err, | ||||||
"no serviceaccount token found") | ||||||
next.ServeHTTP(w, r) | ||||||
return | ||||||
} | ||||||
|
||||||
// User was successfully authenticated, execute request as this user | ||||||
r.Header.Set("Authorization", "Bearer "+context.BearerToken) | ||||||
// Since we are doing requests with the serviceaccount, make sure there are no | ||||||
// other Impersonate-* headers added | ||||||
for key, _ := range r.Header { | ||||||
if strings.HasPrefix(strings.ToLower(key), "impersonate-") { | ||||||
r.Header.Set(key, "") | ||||||
} | ||||||
} | ||||||
r.Header.Set("Impersonate-User", user) | ||||||
for _, group := range groups { | ||||||
r.Header.Set("Impersonate-Group", group) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this should use Add instead of Set.
Suggested change
|
||||||
} | ||||||
next.ServeHTTP(w, r) | ||||||
}) | ||||||
} | ||||||
|
||||||
func (c *HeadlampConfig) getValidToken(ctx context.Context, token string) (*oidc.IDToken, error) { | ||||||
oidcConfig := &oidc.Config{ | ||||||
ClientID: c.oidcClientID, | ||||||
} | ||||||
oauth2Token, err := c.oidcProvider.Verifier(oidcConfig).Verify(ctx, token) | ||||||
|
||||||
if err != nil { | ||||||
return nil, fmt.Errorf("token is not valid: %w", err) | ||||||
} | ||||||
|
||||||
return oauth2Token, nil | ||||||
} | ||||||
|
||||||
func StartHeadlampServer(config *HeadlampConfig) { | ||||||
handler := createHeadlampHandler(config) | ||||||
|
||||||
if config.oidcImpersonate { | ||||||
handler = config.OIDCImpersonateMiddleware(handler) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please explain why this middleware is after the OIDCTokenRefreshMiddleware? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
|
||||||
logger.Log(logger.LevelInfo, nil, nil, "OIDC impersonate active") | ||||||
} | ||||||
// OIDCTokenRefreshMiddleware must always be first evaluated, since it needs the original | ||||||
// OIDC token and other middlewares might exchange it | ||||||
handler = config.OIDCTokenRefreshMiddleware(handler) | ||||||
|
||||||
// Start server | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,9 @@ type Config struct { | |
OidcClientSecret string `koanf:"oidc-client-secret"` | ||
OidcIdpIssuerURL string `koanf:"oidc-idp-issuer-url"` | ||
OidcScopes string `koanf:"oidc-scopes"` | ||
OidcImpersonate bool `koanf:"oidc-impersonate"` | ||
OidcUserClaim string `koanf:"oidc-user-claim"` | ||
OidcGroupsClaim string `koanf:"oidc-groups-claim"` | ||
} | ||
|
||
func (c *Config) Validate() error { | ||
|
@@ -166,6 +169,11 @@ 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) |
||
"Kubernetes API calls are done with the service account and impersonated as the user in the OIDC token") | ||
f.String("oidc-user-claim", "preferred_username", | ||
"The username claim of the OIDC token to be used when -oidc-impersonate=true") | ||
f.String("oidc-groups-claim", "", "The groups claim of the OIDC token to be used when -oidc-impersonate=true") | ||
|
||
return f | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.