From bec3e6d1e1e831d96fde301c7f84b5db679e20ee Mon Sep 17 00:00:00 2001 From: Dan Upton Date: Thu, 28 Aug 2025 20:38:22 +0100 Subject: [PATCH] Allow `"*"` in `kubernetes_users` Backport #58282 to branch/v17 --- .../kubernetes-access/controls.mdx | 14 ++++++- lib/kube/proxy/ephemeral_containers.go | 2 +- lib/kube/proxy/forwarder.go | 40 ++++++++++-------- lib/kube/proxy/forwarder_test.go | 41 +++++++++++++++++++ lib/kube/proxy/resource_deletecollection.go | 1 + 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/docs/pages/enroll-resources/kubernetes-access/controls.mdx b/docs/pages/enroll-resources/kubernetes-access/controls.mdx index 05b64e32a883b..454962d41d012 100644 --- a/docs/pages/enroll-resources/kubernetes-access/controls.mdx +++ b/docs/pages/enroll-resources/kubernetes-access/controls.mdx @@ -313,8 +313,8 @@ determines this from the `kubernetes_users` and `kubernetes_groups` fields in a user's roles. If a user has exactly one value in `kubernetes_users`, the Teleport Kubernetes -Service impersonates that user. If there are no values in `kubernetes_users`, -the Kubernetes Service uses the user's Teleport username. +Service impersonates that user. If there are no values or a wildcard (`*`) in +`kubernetes_users`, the Kubernetes Service uses the user's Teleport username. The Kubernetes Service will deny a request if a user has multiple `kubernetes_users` and has not specified one when authenticating to a cluster @@ -323,6 +323,16 @@ The Kubernetes Service will deny a request if a user has multiple If the user has not specified a Kubernetes group to impersonate, the Kubernetes Service uses all values within `kubernetes_groups`. + + When impersonating a less privileged user, remember that unless you're + also manually impersonating specific groups (e.g. using `--as-groups` flag), + the Kubernetes Service will automatically impersonate any groups within + `kubernetes_groups`. + + This can be confusing because you will have the combined permissions of both + the user and any automatically-impersonated groups. + + With the `kube-access` role above, after you authenticate to Teleport, the Kubernetes Service uses impersonation headers to forward requests to the API server with the `developers` group and the `myuser` Kubernetes user. diff --git a/lib/kube/proxy/ephemeral_containers.go b/lib/kube/proxy/ephemeral_containers.go index 1c9ae08e417a4..15bec03d2b4f4 100644 --- a/lib/kube/proxy/ephemeral_containers.go +++ b/lib/kube/proxy/ephemeral_containers.go @@ -248,7 +248,7 @@ func (f *Forwarder) impersonatedKubeClient(authCtx *authContext, headers http.He return nil, nil, trace.NotFound("kubernetes cluster %q not found", authCtx.kubeClusterName) } restConfig := details.getKubeRestConfig() - kubeUser, kubeGroups, err := computeImpersonatedPrincipals(authCtx.kubeUsers, authCtx.kubeGroups, headers) + kubeUser, kubeGroups, err := computeImpersonatedPrincipals(authCtx.kubeUsers, authCtx.kubeGroups, authCtx.User.GetName(), headers) if err != nil { return nil, nil, trace.Wrap(err) } diff --git a/lib/kube/proxy/forwarder.go b/lib/kube/proxy/forwarder.go index 8586dace4e997..fb633733ef79e 100644 --- a/lib/kube/proxy/forwarder.go +++ b/lib/kube/proxy/forwarder.go @@ -479,7 +479,7 @@ func (c *authContext) key() string { func (c *authContext) eventClusterMeta(req *http.Request) apievents.KubernetesClusterMetadata { var kubeUsers, kubeGroups []string - if impersonateUser, impersonateGroups, err := computeImpersonatedPrincipals(c.kubeUsers, c.kubeGroups, req.Header); err == nil { + if impersonateUser, impersonateGroups, err := computeImpersonatedPrincipals(c.kubeUsers, c.kubeGroups, c.User.GetName(), req.Header); err == nil { kubeUsers = []string{impersonateUser} kubeGroups = impersonateGroups } else { @@ -1939,7 +1939,7 @@ func setupImpersonationHeaders(sess *clusterSession, headers http.Header) error return nil } - impersonateUser, impersonateGroups, err := computeImpersonatedPrincipals(sess.kubeUsers, sess.kubeGroups, headers) + impersonateUser, impersonateGroups, err := computeImpersonatedPrincipals(sess.kubeUsers, sess.kubeGroups, sess.User.GetName(), headers) if err != nil { return trace.Wrap(err) } @@ -1978,7 +1978,9 @@ func copyImpersonationHeaders(dst, src http.Header) { // received in the `Impersonate-User` and `Impersonate-Groups` headers and the // allowed values. If the user didn't specify any user and groups to impersonate, // Teleport will use every group the user is allowed to impersonate. -func computeImpersonatedPrincipals(kubeUsers, kubeGroups map[string]struct{}, headers http.Header) (string, []string, error) { +func computeImpersonatedPrincipals(kubeUsers, kubeGroups map[string]struct{}, username string, headers http.Header) (string, []string, error) { + _, hasUserWildcard := kubeUsers[types.Wildcard] + var impersonateUser string var impersonateGroups []string for header, values := range headers { @@ -2003,7 +2005,7 @@ func computeImpersonatedPrincipals(kubeUsers, kubeGroups map[string]struct{}, he } impersonateUser = values[0] - if _, ok := kubeUsers[impersonateUser]; !ok { + if _, ok := kubeUsers[impersonateUser]; !ok && !hasUserWildcard { return "", nil, trace.AccessDenied("%v, user header %q is not allowed in roles", ImpersonationRequestDeniedMessage, impersonateUser) } case ImpersonateGroupHeader: @@ -2037,20 +2039,24 @@ func computeImpersonatedPrincipals(kubeUsers, kubeGroups map[string]struct{}, he // link the user identity with the IAM role, for example `IAM#{{external.email}}` // if impersonateUser == "" { - switch len(kubeUsers) { - // this is currently not possible as kube users have at least one - // user (user name), but in case if someone breaks it, catch here - case 0: - return "", nil, trace.AccessDenied("assumed at least one user to be present") - // if there is deterministic choice, make it to improve user experience - case 1: - for user := range kubeUsers { - impersonateUser = user - break + if hasUserWildcard { + impersonateUser = username + } else { + switch len(kubeUsers) { + // this is currently not possible as kube users have at least one + // user (user name), but in case if someone breaks it, catch here + case 0: + return "", nil, trace.AccessDenied("assumed at least one user to be present") + // if there is deterministic choice, make it to improve user experience + case 1: + for user := range kubeUsers { + impersonateUser = user + break + } + default: + return "", nil, trace.AccessDenied( + "please select a user to impersonate, refusing to select a user due to several kubernetes_users set up for this user") } - default: - return "", nil, trace.AccessDenied( - "please select a user to impersonate, refusing to select a user due to several kubernetes_users set up for this user") } } diff --git a/lib/kube/proxy/forwarder_test.go b/lib/kube/proxy/forwarder_test.go index d23c966a3f061..3aaee41eb2242 100644 --- a/lib/kube/proxy/forwarder_test.go +++ b/lib/kube/proxy/forwarder_test.go @@ -787,6 +787,7 @@ func TestSetupImpersonationHeaders(t *testing.T) { desc string kubeUsers []string kubeGroups []string + username string remoteCluster bool isProxy bool inHeaders http.Header @@ -931,6 +932,33 @@ func TestSetupImpersonationHeaders(t *testing.T) { }, errAssertion: require.NoError, }, + { + desc: "kubernetes_users wildcard, no impersonation headers", + username: "ted-lasso", + kubeUsers: []string{types.Wildcard}, + kubeGroups: []string{"kube-group-a"}, + inHeaders: http.Header{}, + wantHeaders: http.Header{ + ImpersonateUserHeader: []string{"ted-lasso"}, + ImpersonateGroupHeader: []string{"kube-group-a"}, + }, + errAssertion: require.NoError, + }, + { + desc: "kubernetes_users wildcard, impersonation headers given", + username: "ted-lasso", + kubeUsers: []string{types.Wildcard}, + kubeGroups: []string{"kube-group-a"}, + inHeaders: http.Header{ + ImpersonateUserHeader: []string{"kube-user-a"}, + ImpersonateGroupHeader: []string{"kube-group-a"}, + }, + wantHeaders: http.Header{ + ImpersonateUserHeader: []string{"kube-user-a"}, + ImpersonateGroupHeader: []string{"kube-group-a"}, + }, + errAssertion: require.NoError, + }, } for _, tt := range tests { tt := tt @@ -943,6 +971,11 @@ func TestSetupImpersonationHeaders(t *testing.T) { &clusterSession{ kubeAPICreds: kubeCreds, authContext: authContext{ + Context: authz.Context{ + User: &types.UserV2{ + Metadata: types.Metadata{Name: tt.username}, + }, + }, kubeUsers: utils.StringsSet(tt.kubeUsers), kubeGroups: utils.StringsSet(tt.kubeGroups), teleportCluster: teleportClusterClient{isRemote: tt.remoteCluster}, @@ -1561,6 +1594,11 @@ func Test_authContext_eventClusterMeta(t *testing.T) { kubeClusterLabels := map[string]string{ "label": "value", } + baseAuthCtx := authz.Context{ + User: &types.UserV2{ + Metadata: types.Metadata{Name: "ted-lasso"}, + }, + } type args struct { req *http.Request ctx *authContext @@ -1577,6 +1615,7 @@ func Test_authContext_eventClusterMeta(t *testing.T) { Header: http.Header{}, }, ctx: &authContext{ + Context: baseAuthCtx, kubeClusterName: "clusterName", kubeClusterLabels: kubeClusterLabels, kubeGroups: map[string]struct{}{"kube-group-a": {}, "kube-group-b": {}}, @@ -1600,6 +1639,7 @@ func Test_authContext_eventClusterMeta(t *testing.T) { }, }, ctx: &authContext{ + Context: baseAuthCtx, kubeClusterName: "clusterName", kubeClusterLabels: kubeClusterLabels, kubeGroups: map[string]struct{}{"kube-group-a": {}, "kube-group-b": {}, "kube-group-c": {}}, @@ -1620,6 +1660,7 @@ func Test_authContext_eventClusterMeta(t *testing.T) { Header: http.Header{}, }, ctx: &authContext{ + Context: baseAuthCtx, kubeClusterName: "clusterName", kubeClusterLabels: kubeClusterLabels, kubeGroups: map[string]struct{}{"kube-group-a": {}, "kube-group-b": {}, "kube-group-c": {}}, diff --git a/lib/kube/proxy/resource_deletecollection.go b/lib/kube/proxy/resource_deletecollection.go index b7f5f9434b58a..5fe7927512af7 100644 --- a/lib/kube/proxy/resource_deletecollection.go +++ b/lib/kube/proxy/resource_deletecollection.go @@ -617,6 +617,7 @@ func deleteResources[T kubeObjectInterface]( impersonatedUsers, impersonatedGroups, err := computeImpersonatedPrincipals( utils.StringsSet(allowedKubeUsers), utils.StringsSet(allowedKubeGroups), + params.authCtx.User.GetName(), params.header, ) if err != nil {