Skip to content

Commit

Permalink
Adds support for kubernetes_users, extend interpolation (#3404) (#3418)
Browse files Browse the repository at this point in the history
This commit fixes #3369, refs #3374

It adds support for kuberenetes_users section in roles,
allowing Teleport proxy to impersonate user identities.

It also extends variable interpolation syntax by adding
suffix and prefix to variables and function `email.local`:

Example:

```yaml
kind: role
version: v3
metadata:
  name: admin
spec:
  allow:
    # extract email local part from the email claim
    logins: ['{{email.local(external.email)}}']

    # impersonate a kubernetes user with IAM prefix
    kubernetes_users: ['IAM#{{external.email}}']

  # the deny section uses the identical format as the 'allow' section.
  # the deny rules always override allow rules.
  deny: {}
```

Some notes on email.local behavior:

* This is the only function supported in the template variables for now
* In case if the email.local will encounter invalid email address,
it will interpolate to empty value, will be removed from resulting
output.

Changes in impersonation behavior:

* By default, if no kubernetes_users is set, which is a majority of cases,
  user will impersonate themselves, which is the backwards-compatible behavior.

* As long as at least one `kubernetes_users` is set, the forwarder will start
  limiting the list of users allowed by the client to impersonate.

* If the users' role set does not include actual user name, it will be rejected,
  otherwise there will be no way to exclude the user from the list).

* If the `kuberentes_users` role set includes only one user
  (quite frequently that's the real intent), teleport will default to it,
  otherwise it will refuse to select.

  This will enable the use case when `kubernetes_users` has just one field to
  link the user identity with the IAM role, for example `IAM#{{external.email}}`

* Previous versions of the forwarding proxy were denying all external
impersonation headers, this commit allows 'Impesrsonate-User' and
'Impersonate-Group' header values that are allowed by role set.

* Previous versions of the forwarding proxy ignored 'Deny' section of the roles
when applied to impersonation, this commit fixes that - roles with deny
kubernetes_users and kubernetes_groups section will not allow
impersonation of those users and groups.
  • Loading branch information
klizhentas authored Mar 8, 2020
1 parent 382628f commit 73ecb48
Show file tree
Hide file tree
Showing 25 changed files with 1,172 additions and 544 deletions.
4 changes: 4 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,10 @@ const (
// allowed kubernetes groups
TraitKubeGroups = "kubernetes_groups"

// TraitKubeUsers is the name the role variable used to store
// allowed kubernetes users
TraitKubeUsers = "kubernetes_users"

// TraitInternalLoginsVariable is the variable used to store allowed
// logins for local accounts.
TraitInternalLoginsVariable = "{{internal.logins}}"
Expand Down
2 changes: 1 addition & 1 deletion e
Submodule e updated from 1e8763 to 0b124e
85 changes: 83 additions & 2 deletions integration/kube_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2016-2019 Gravitational, Inc.
Copyright 2016-2020 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -155,6 +155,7 @@ func (s *KubeSuite) TestKubeExec(c *check.C) {
Allow: services.RoleConditions{
Logins: []string{username},
KubeGroups: []string{teleport.KubeSystemMasters},
KubeUsers: []string{"alice@example.com"},
},
})
t.AddUserWithRole(username, role)
Expand All @@ -166,7 +167,8 @@ func (s *KubeSuite) TestKubeExec(c *check.C) {
c.Assert(err, check.IsNil)
defer t.Stop(true)

// impersonating client requests will be denied
// impersonating client requests will be denied if the headers
// are referencing users or groups not allowed by the existing roles
impersonatingProxyClient, impersonatingProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{
t: t,
username: username,
Expand All @@ -180,6 +182,23 @@ func (s *KubeSuite) TestKubeExec(c *check.C) {
})
c.Assert(err, check.NotNil)

// scoped client requests will be allowed, as long as the impersonation headers
// are referencing users and groups allowed by existing roles
scopedProxyClient, scopedProxyClientConfig, err := kubeProxyClient(kubeProxyConfig{
t: t,
username: username,
impersonation: &rest.ImpersonationConfig{
UserName: role.GetKubeUsers(services.Allow)[0],
Groups: role.GetKubeGroups(services.Allow),
},
})
c.Assert(err, check.IsNil)

_, err = scopedProxyClient.CoreV1().Pods(kubeSystemNamespace).List(metav1.ListOptions{
LabelSelector: kubeDNSLabels.AsSelector().String(),
})
c.Assert(err, check.IsNil)

// set up kube configuration using proxy
proxyClient, proxyClientConfig, err := kubeProxyClient(kubeProxyConfig{t: t, username: username})
c.Assert(err, check.IsNil)
Expand Down Expand Up @@ -266,6 +285,68 @@ loop:
})
c.Assert(err, check.NotNil)
c.Assert(err.Error(), check.Matches, ".*impersonation request has been denied.*")

// scoped kube exec is allowed, impersonation headers
// are allowed by the role
term = NewTerminal(250)
term.Type("\aecho hi\n\r\aexit\n\r\a")
out = &bytes.Buffer{}
err = kubeExec(scopedProxyClientConfig, kubeExecArgs{
podName: pod.Name,
podNamespace: pod.Namespace,
container: kubeDNSContainer,
command: []string{"/bin/sh"},
stdout: out,
tty: true,
stdin: &term,
})
c.Assert(err, check.IsNil)
}

// TestKubeDeny makes sure that deny rule conflicting with allow
// rule takes precendence
func (s *KubeSuite) TestKubeDeny(c *check.C) {
tconf := s.teleKubeConfig(Host)

t := NewInstance(InstanceConfig{
ClusterName: Site,
HostID: HostID,
NodeName: Host,
Ports: s.ports.PopIntSlice(5),
Priv: s.priv,
Pub: s.pub,
})

username := s.me.Username
role, err := services.NewRole("kubemaster", services.RoleSpecV3{
Allow: services.RoleConditions{
Logins: []string{username},
KubeGroups: []string{teleport.KubeSystemMasters},
KubeUsers: []string{"alice@example.com"},
},
Deny: services.RoleConditions{
KubeGroups: []string{teleport.KubeSystemMasters},
KubeUsers: []string{"alice@example.com"},
},
})
t.AddUserWithRole(username, role)

err = t.CreateEx(nil, tconf)
c.Assert(err, check.IsNil)

err = t.Start()
c.Assert(err, check.IsNil)
defer t.Stop(true)

// set up kube configuration using proxy
proxyClient, _, err := kubeProxyClient(kubeProxyConfig{t: t, username: username})
c.Assert(err, check.IsNil)

// try get request to fetch available pods
_, err = proxyClient.CoreV1().Pods(kubeSystemNamespace).List(metav1.ListOptions{
LabelSelector: kubeDNSLabels.AsSelector().String(),
})
c.Assert(err, check.NotNil)
}

// TestKubePortForward tests kubernetes port forwarding
Expand Down
11 changes: 7 additions & 4 deletions lib/auth/github.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2017 Gravitational, Inc.
Copyright 2017-2020 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -289,9 +289,12 @@ type createUserParams struct {
// logins is the list of *nix logins.
logins []string

// kubeGroups is the list of Kubernetes this user belongs to.
// kubeGroups is the list of Kubernetes groups this user belongs to.
kubeGroups []string

// kubeUsers is the list of Kubernetes users this user belongs to.
kubeUsers []string

// roles is the list of roles this user is assigned to.
roles []string

Expand All @@ -309,14 +312,14 @@ func (s *AuthServer) calculateGithubUser(connector services.GithubConnector, cla
}

// Calculate logins, kubegroups, roles, and traits.
p.logins, p.kubeGroups = connector.MapClaims(*claims)
p.logins, p.kubeGroups, p.kubeUsers = connector.MapClaims(*claims)
if len(p.logins) == 0 {
return nil, trace.BadParameter(
"user %q does not belong to any teams configured in %q connector",
claims.Username, connector.GetName())
}
p.roles = modules.GetModules().RolesFromLogins(p.logins)
p.traits = modules.GetModules().TraitsFromLogins(p.logins, p.kubeGroups)
p.traits = modules.GetModules().TraitsFromLogins(p.logins, p.kubeGroups, p.kubeUsers)

// Pick smaller for role: session TTL from role or requested TTL.
roles, err := services.FetchRoles(p.roles, s.Access, p.traits)
Expand Down
3 changes: 2 additions & 1 deletion lib/auth/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (s *AuthServer) ProcessKubeCSR(req KubeCSR) (*KubeCSRResponse, error) {

// extract and encode the kubernetes groups of the authenticated
// user in the newly issued certificate
kubernetesGroups, err := roles.CheckKubeGroups(0)
kubernetesGroups, kubernetesUsers, err := roles.CheckKubeGroupsAndUsers(0)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -129,6 +129,7 @@ func (s *AuthServer) ProcessKubeCSR(req KubeCSR) (*KubeCSRResponse, error) {
// otherwise proxies can generate certs for any user.
Usage: []string{teleport.UsageKubeOnly},
KubernetesGroups: kubernetesGroups,
KubernetesUsers: kubernetesUsers,
}
subject, err := identity.Subject()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions lib/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ func (a *AuthMiddleware) GetUser(r *http.Request) (IdentityGetter, error) {
Username: identity.Username,
Principals: identity.Principals,
KubernetesGroups: identity.KubernetesGroups,
KubernetesUsers: identity.KubernetesUsers,
RemoteRoles: identity.Groups,
Identity: *identity,
}, nil
Expand Down
9 changes: 7 additions & 2 deletions lib/auth/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,17 @@ func (a *authorizer) authorizeRemoteUser(u RemoteUser) (*AuthContext, error) {
return nil, trace.AccessDenied("no roles mapped for remote user %q from cluster %q", u.Username, u.ClusterName)
}
// Set "logins" trait and "kubernetes_groups" for the remote user. This allows Teleport to work by
// passing exact logins and kubernetes groups to the remote cluster. Note that claims (OIDC/SAML)
// passing exact logins, kubernetes groups and users to the remote cluster. Note that claims (OIDC/SAML)
// are not passed, but rather the exact logins, this is done to prevent
// leaking too much of identity to the remote cluster, and instead of focus
// on main cluster's interpretation of this identity
traits := map[string][]string{
teleport.TraitLogins: u.Principals,
teleport.TraitKubeGroups: u.KubernetesGroups,
teleport.TraitKubeUsers: u.KubernetesUsers,
}
log.Debugf("Mapped roles %v of remote user %q to local roles %v and traits %v.", u.RemoteRoles, u.Username, roleNames, traits)
log.Debugf("Mapped roles %v of remote user %q to local roles %v and traits %v.",
u.RemoteRoles, u.Username, roleNames, traits)
checker, err := services.FetchRoles(roleNames, a.access, traits)
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -589,6 +591,9 @@ type RemoteUser struct {
// KubernetesGroups is a list of Kubernetes groups
KubernetesGroups []string `json:"kubernetes_groups"`

// KubernetesUsers is a list of Kubernetes users
KubernetesUsers []string `json:"kubernetes_users"`

// Identity is source x509 used to build this role
Identity tlsca.Identity
}
Expand Down
Loading

0 comments on commit 73ecb48

Please sign in to comment.