Skip to content
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

Adds support for kube_users, extend interpolation #3404

Merged
merged 1 commit into from
Mar 7, 2020
Merged

Conversation

klizhentas
Copy link
Contributor

@klizhentas klizhentas commented Mar 4, 2020

This commit fixes #3369, refs #3374

Corresponding teleport.e commit:

https://github.com/gravitational/teleport.e/pull/128

It adds support for kubernetes_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:

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:

  • If kubernetes_users part of the resulting role set will have one entry,
    forwarding proxy will add this user to impersonate headers of the request, because
    there is no ambiguity.

  • If kubernetes_users part of the resulting role set will have multiple entries,
    forwarding proxy will not add any impersonating user headers by default, users
    will have to set impersonate user headers.

  • 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.

@benarent
Copy link
Contributor

benarent commented Mar 4, 2020

👍 Looks like this solves a couple of customer issues. Have you tested it with Trusted Cluster? We've had a couple related bugs come up recently in regards to mapping. #3402 & #3376 It'll be good to fix this issue as many customer will deploy K8s as Trusted Clusters.

@klizhentas
Copy link
Contributor Author

@benarent not sure I follow, are you asking if I can add two more issue fixes to this PR?

@klizhentas
Copy link
Contributor Author

retest this please

Comment on lines 704 to 710
if len(ctx.kubeUsers) == 1 {
for user := range ctx.kubeUsers {
impersonateUser = user
log.Debugf("Setting impersonate user to %v: defaulting single value from roles", user)
break
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is holding a single user given special treatment? This seems like it could create confusing behavior.

E.G. alice has role dev. Alice impersonates herself by default. Bob adds admin user to the role dev. Now alice impersonates admin. Bob then adds testing user to dev. Alice is now back to impersonating herself. From alice's point of view, this is very confusing.

Copy link
Contributor Author

@klizhentas klizhentas Mar 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to allow a default: it does not matter how many roles Alice holds, as long as the resulting role set only specifies one user to impersonate, the system will default to it. In your example, if Bob adds admin user to the role dev, the system will not be able to choose a default, because there are two users in the set, and will ask Alice to pick one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the system will not be able to choose a default, because there are two users in the set, and will ask Alice to pick one.

Seems that impersonateUser = ctx.User.GetName() always runs unless len(ctx.kubeUsers) == 1... maybe I'm misunderstanding, but that looks to me like alice will initially impersonate herself, start impersonating the first user added to any role she holds, then go back to impersonating herself as soon as any role adds an additional user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you are right, that would be the effect, I'm wondering what would be a better default then:

  • Alice would not be impersonating any user by default unless specified explicitly
  • Alice would be impersonating herself by default at all times, unless specified otherwise and impersonating herself explicitly will be allowed
  • Alice would be impersonating only users set in the kubernetes_users from now on (if there is only one user specified) and no other impersonation will be allowed

Comment on lines +687 to +694
if _, ok := ctx.kubeUsers[impersonateUser]; !ok {
return trace.AccessDenied("%v, user header %q is not allowed in roles", ImpersonationRequestDeniedMessage, impersonateUser)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this reject a user attempting to impersonate their own name? If so, this is potentially confusing since users are allowed to impersonate themselves as a default behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Their own name could mean something else in the context of the Kubernetes cluster, e.g. IAM role with some privileges. That's why impersonation is fully controlled by roles and there are no default allowed values.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why impersonation is fully controlled by roles and there are no default allowed values.

This line looks like it causes users to impersonate their own usernames by default, regardless of what the roles say.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for pointing it out, that was a behavior before kuberenetes_users, so I tried to preserve the default behavior of the system, but now I see how it can be confusing. I wonder if we should always allow users to impersonate themselves then 🤔

Copy link
Contributor

@fspmarshall fspmarshall Mar 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the ramifications of kubernetes user impersonation well enough to comment on whether self-impersonation is a desirable default... I will say that I prefer it over breaking existing deployments (though I think it should either always or never be the default).

If we don't want to keep self-impersonation as the default, perhaps we could provide a migration which allowed existing deployments to keep their behavior? E.G.:

options:
  k8s_user_passthrough: yes

The above could resolve to no if no roles specify it, resolve to yes if one or more roles specify yes (which could be set by a migration), and resolve to no if any role explicitly specifies no.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fspmarshall I think this will be reasonable but a bit confusing behavior to explain to users and admins:

  • By default, if no kubernetes_users is set (which will be a majority), user will impersonate themselves
  • As long as at least one kubernetes_users is set, it will start controlling the overall list of users allowed by the client to impersonate. If it does not include actual user name, well, will be rejected (otherwise there will be no way to exclude the user from the list)
  • If the kuberentes_users set includes only one user (many times that's the real intent), teleport will default to it, otherwise it will refuse to select. This will enable IAM#{{external.email}} use case that is the driver for this feature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@klizhentas sounds good, that does feel easier to explain.

lib/tlsca/ca.go Outdated
Comment on lines 132 to 136
// DELETE IN (4.4.0)
// Groups are marshaled to both ASN1 extension
// and old Province section, for backwards-compatibility,
// however begin migration to ASN1 extensions in the future
// for this and other properties
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to delete this on a minor version change, we should definitely add checks in tsh to warn about outdated cert format when loading identity files.

Maybe we should start explicitly versioning the teleport certificate format and producing readable errors on version incompatibility. Certificate incompatibility has the potential to fail silently in very confusing ways.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can delete this in major, overall it should not have any impact if we just skip one version, because the new cert fields are not used on the tsh client, are only in memory in proxy and are not stored anywhere, so it will be safe to delete with no backwards compatibility problems.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, this comment was more directed at the parsing side of things, see below.

@benarent benarent linked an issue Mar 4, 2020 that may be closed by this pull request
@klizhentas
Copy link
Contributor Author

retest this please

1 similar comment
@klizhentas
Copy link
Contributor Author

retest this please

lib/tlsca/ca.go Outdated
Comment on lines 196 to 201
// DELETE IN(4.5.0): This logic is using Province field
// from subject in case if Kubernetes groups were not populated
// from ASN1 extension
if len(id.KubernetesGroups) == 0 {
id.KubernetesGroups = subject.Province
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should either keep this around longer or make a non-empty Province become an error. Silently pruning kubernetes groups on older certs (such as those exported with tctl auth sign) would be very confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok to keep this longer let's say to 5.0 release

@klizhentas
Copy link
Contributor Author

retest this please


switch n := node.(type) {
case *ast.CallExpr:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Parse call expression. At the moment only selector expressions are supported to allow
// transformation of data. For example, email.local(foo@example.com) -> foo@example.com.

case *ast.Ident:
return nil, trace.BadParameter("function %v is not supported", call.Name)
case *ast.SelectorExpr:
namespace, ok := call.X.(*ast.Ident)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Extract name of transformation function.

return nil, trace.BadParameter("expected 1 argument for email.local got %v", len(n.Args))
}
result.transform = EmailLocal
ret, err := walk(n.Args[0])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Extract email address.

subject.Province = append([]string{}, id.KubernetesGroups...)
subject.StreetAddress = []string{id.RouteToCluster}
subject.PostalCode = []string{string(rawTraits)}

for i := range id.KubernetesUsers {
kubeUser := id.KubernetesUsers[i]
subject.ExtraNames = append(subject.ExtraNames,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're moving data out of the subject, why not move this information to the extensions? That where the SAN, key usage, and other metadata now lives.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the benefit of doing that vs keeping it in subject?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with keeping it in subject, there is a benefit of locality as we plan on keeping other things like common name to be used for groups, unless we want to completely stop using the subject.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that is what was being suggested in one of the comments, to get everything off of subject.{Organization, OrganizationUnit, Locality} in the future?

I don't have a technical argument for moving data out of the subject, but more for debugging. With our current approach of packing everything into the subject, when running a command like openssl x509 -in <cert> -text -noout I have to mentally map fields in the subject to Teleport metadata like OU=groups.

Ideally we'd see something like x-teleport-groups: admin, dev instead. It appears that's possible with extensions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest we keep the metadata in the subject for now, but instead use name extensions instead of common fields. The same command you have outlined works with subject as well, no problem

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

for _, group := range ctx.kubeGroups {
req.Header.Add("Impersonate-Group", group)
f.Debugf("Impersonate Group: %v", group)
headers.Add("Impersonate-User", impersonateUser)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that impersonateUser may be the empty string ("") if len(ctx.kubeUsers) != 1. Is that intended behavior?

Copy link
Contributor Author

@klizhentas klizhentas Mar 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fspmarshall thanks for outstanding detailed review. I think this is undefined behavior, however I restricted our approach to rejecting the empty impersonating header and in case of multiple choice we will refuse to make a selection, asking user to select a user, see my changes above.

@klizhentas
Copy link
Contributor Author

retest this please

1 similar comment
@klizhentas
Copy link
Contributor Author

retest this please

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.
@klizhentas
Copy link
Contributor Author

retest this please

@klizhentas klizhentas merged commit 31fd5b1 into branch/4.2 Mar 7, 2020
klizhentas added a commit that referenced this pull request Mar 7, 2020
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.
klizhentas added a commit that referenced this pull request Mar 8, 2020
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.
@klizhentas klizhentas deleted the sasha/ku branch March 15, 2021 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Teleport to access IBM Kubernetes Service using JWT auth scheme
4 participants