Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/pages/enroll-resources/kubernetes-access/controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`.

<Admonition type="warning">
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.
</Admonition>

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.
Expand Down
2 changes: 1 addition & 1 deletion lib/kube/proxy/ephemeral_containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
40 changes: 23 additions & 17 deletions lib/kube/proxy/forwarder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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:
Expand Down Expand Up @@ -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")
}
}

Expand Down
41 changes: 41 additions & 0 deletions lib/kube/proxy/forwarder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,7 @@ func TestSetupImpersonationHeaders(t *testing.T) {
desc string
kubeUsers []string
kubeGroups []string
username string
remoteCluster bool
isProxy bool
inHeaders http.Header
Expand Down Expand Up @@ -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) {
Expand All @@ -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},
Expand Down Expand Up @@ -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
Expand All @@ -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": {}},
Expand All @@ -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": {}},
Expand All @@ -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": {}},
Expand Down
1 change: 1 addition & 0 deletions lib/kube/proxy/resource_deletecollection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading