diff --git a/docs/pages/enroll-resources/kubernetes-access/controls.mdx b/docs/pages/enroll-resources/kubernetes-access/controls.mdx index 44989dca2b5e4..b1b006fc5519c 100644 --- a/docs/pages/enroll-resources/kubernetes-access/controls.mdx +++ b/docs/pages/enroll-resources/kubernetes-access/controls.mdx @@ -319,8 +319,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 @@ -329,6 +329,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 baccb97026342..9de25cb83f811 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 d22806b0a88fd..bc3559579d887 100644 --- a/lib/kube/proxy/forwarder.go +++ b/lib/kube/proxy/forwarder.go @@ -478,7 +478,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 { @@ -1968,7 +1968,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) } @@ -2007,7 +2007,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 { @@ -2032,7 +2034,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: @@ -2066,20 +2068,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 3d14ed4d4742a..ed331ff3c28dd 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 { t.Run(tt.desc, func(t *testing.T) { @@ -942,6 +970,11 @@ func TestSetupImpersonationHeaders(t *testing.T) { &clusterSession{ kubeAPICreds: kubeCreds, authContext: authContext{ + Context: authz.Context{ + User: &types.UserV2{ + Metadata: types.Metadata{Name: tt.username}, + }, + }, kubeUsers: set.New(tt.kubeUsers...), kubeGroups: set.New(tt.kubeGroups...), teleportCluster: teleportClusterClient{isRemote: tt.remoteCluster}, @@ -1519,6 +1552,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 @@ -1535,6 +1573,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": {}}, @@ -1558,6 +1597,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": {}}, @@ -1578,6 +1618,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 a30ecadb8ec60..c50cbd5d5b370 100644 --- a/lib/kube/proxy/resource_deletecollection.go +++ b/lib/kube/proxy/resource_deletecollection.go @@ -358,6 +358,7 @@ func deleteResources[T kubeObjectInterface]( impersonatedUsers, impersonatedGroups, err := computeImpersonatedPrincipals( set.New(allowedKubeUsers...), set.New(allowedKubeGroups...), + params.authCtx.User.GetName(), params.header, ) if err != nil {