Project: Pinniped (Provisioning / Key Management)
File: configure-supervisor-federationdomain-idps.md

title: Configure Identity Providers (IDPs) on a FederationDomain description: Learn how to use one or more identity providers, and identity transformations and policies, on a FederationDomain. cascade: layout: docs menu: docs: name: IDPs on FederationDomains weight: 20 parent: howto-configure-supervisor This guide explains how to associate one or more external identity providers (IDPs) with a FederationDomain. It also details how to configure identity transformations and identity policies for those identity providers. This how-to guide assumes that you have already and have already read the guide about how to . This guide focuses on the use of the `spec.identityProviders` setting on the resource. Note that the `spec.identityProviders` setting on the FederationDomain resource was added in v0.26.0 of Pinniped. This guide assumes that you are using at least that version. External identity providers may be configured in the Supervisor by creating OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resources in the same namespace as the Supervisor. There are two ways to configure which of these external identity providers shall be used by a FederationDomain. When there is no `spec.identityProviders` configured on a FederationDomain, then the FederationDomain will use the one and only identity provider that is configured in the same namespace. This provides backwards compatibility with older configurations of Supervisors from before the `spec.identityProviders` setting was added to the FederationDomain resource. There must be exactly one OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resource in the same namespace as the Supervisor. If there are no identity provider resources, or if there are more than one, then the FederationDomain will not allow any users to authenticate, and a error message will be shown in its `status`. When `spec.identityProviders` is explicitly configured on a FederationDomain, then the FederationDomain will allow clients to use any of those identity providers to authenticate. In this case, you may optionally also configure identity transformations and policies that the FederationDomain should apply to each of these identity providers (see below for details). When using the `pinniped get kubeconfig` CLI command, you will need to choose for which identity provider you would like to generate a kubeconfig. A cluster may have multiple kubeconfigs, e.g. one for each identity provider. The remainder of this guide focuses on the second case, and describes the settings that may be used to explicitly configure which identity providers are used, along with optional identity transformations and policies. A user may authenticate to a FederationDomain using any of the IDPs configured in the FederationDomain's `spec.identityProviders`. To add IDPs to this list, simply configure each as a reference to the type and name of the resource. The identity provider resources must be in the same namespace where the Supervisor was . Here is an example FederationDomain with two IDPs configured. ```yaml apiVersion: config.supervisor.pinniped.dev/v1alpha1 kind: FederationDomain metadata: name: my-provider namespace: pinniped-supervisor spec: issuer: https://my-issuer.example.com/any/path tls: secretName: my-tls-cert-secret identityProviders: displayName: ActiveDirectory for Admins objectRef: apiGroup: idp.supervisor.pinniped.dev kind: ActiveDirectoryIdentityProvider name: ad-for-admins displayName: Okta for Developers objectRef: apiGroup: idp.supervisor.pinniped.dev kind: OIDCIdentityProvider name: okta-for-developers ``` Now users may use either of the above identity providers to authenticate. You can create kubeconfigs for both IDPs for each cluster by using `pinniped get kubeconfig` twice for each cluster. When multiple identity providers are configured onto a FederationDomain, then a user may use any of those providers to authenticate to that FederationDomain. Since an identity in Kubernetes is simply a username string and a list of group name strings, it is very important to consider what will happen if two users each authenticate to a FederationDomain using different identity

If two users are assigned the same username string as a result of authenticating, then those two users will be considered the same user by Kubernetes, regardless of which IDP they came from. If two users are assigned the same group name string as a result of authenticating, then those two users will be considered to both belong to same group by Kubernetes, regardless of which IDP they came from. This may or may not be desirable. If the two identity providers are intended to represent distinct, non-overlapping sets of users, then it is not desirable for their usernames and group names to conflict by being identical. On the other hand, if two identity providers contain the same sets of users, and when those users authenticate then your intention is that they are the same identity, then it may be desirable. Let's consider several example use cases. Imagine two IDPs in which usernames and group names are assigned to users by their administrators separately, with no coordination between them. In this case, a user named "ryan" from one IDP is probably not the same human as the user named "ryan" from the other IDP. The group named "admins" from one IDP may have a totally different intended meaning then the group named "admins" from the other IDP. In this case, it is not desirable for usernames and group names from one IDP to conflict with the usernames and group names from the other IDP. Imagine two IDPs which both use corporate email addresses as usernames. These email addresses are assigned by IT and are not adjustable by the individual users. In this case, it may be desirable for a human user to be able to authenticate using either IDP and end up being assigned the same username in Kubernetes clusters either way. However, it may or may not be the case that the user's group names from one IDP are meant to represent the same Kubernetes groups as the same group names from the other IDP. In this case, it is not a concern for usernames from one IDP to conflict with the usernames from the other IDP, however it still might be a concern for group names from one IDP to conflict with the group names from the other IDP. You can easily add configuration to the FederationDomain to handle username and group name conflicts. When it is not desirable for usernames to conflict, then a simple solution is to use identity transformations to change all usernames to have a prefix which is unique to each IDP within that FederationDomain. For example, usernames `ldap:ryan` and `gitlab:ryan` will be considered two different users by Kubernetes. Similarly, when it is not desirable for group names to conflict, then a simple solution is to use identity transformations to change all group names to have a prefix which is unique to each IDP within that FederationDomain. Refer to the next section to learn about how to configure identity transformations to add prefixes to usernames and group names. When a user authenticates, the configuration of the OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resource determines how the user's username and group names will be extracted from the external identity provider in a protocol-specific way (e.g. via OIDC ID token claims or LDAP record attributes). Then, operating on the username and group names extracted from the external IDP: Identity transformations can change either the user's username or group names. Identity policies can reject the user's authentication based on their username and/or

Identity transformations and policies are configured on the FederationDomain, so they are specific to that specific FederationDomain's usage of the external identity provider. Transformations and policies are configured using the Common Expression Language (CEL) programming language. They are configured as a list, and they will be executed in the order specified. The output of each transformation or policy expression may impact the input values for the next expression from the list. Pinniped's implementation of CEL expressions includes the as well as . There are three types of transformation expressions: `username/v1` are expressions which may change the user's username. These expressions must return a string, and the value of the string will be the user's username. Returning an empty string or a string that contains only whitespace characters will cause an authentication error. Returning the value of the `username` variable unmodified will leave the username unchanged. `groups/v1` are expressions which may change the group names of the groups to which the user belongs. These expressions must return a list of strings. The returned list may be empty. The returned list will be the names of the groups to which the user belongs. Returning the value of the `groups` variable unmodified will leave the groups unchanged. `policy/v1` are expressions which may reject the user's authentication based on their username and/or groups. These expressions must return a boolean. Returning true has no impact on the user's authentication and will therefore allow the user's authentication to continue. Returning true also has no impact on the username or group names. Returning false will the reject user's authentication and the user will see the error message configured for that policy expression (or a default error message). Rejecting a user's authentication prevents the user from authenticating into every cluster which uses this FederationDomain for identity services. This happens before Kubernetes RBAC policies are considered by the individual clusters. Therefore, this is a authentication-level rejection, not an authorization check. All three transformation expression types are written using CEL expressions. They are declared as a list of transformations and policies. Each time a user attempts to authenticate, and each time a user's session is automatically refreshed periodically, the list is evaluated in the order that it was declared. `username/v1` expressions may change the username that is passed to the next expressions. `groups/v1` expressions may change the group names that are passed to the next expressions. `policy/v1` expressions may halt the processing of further expressions when they reject the authentication. Because each expression in the list can pass information to the following expressions via its return values, the list of expressions acts like a "pipeline". Any unexpected runtime evaluation errors (e.g. division by zero) cause the authentication to fail. The following variables are available to each expression, regardless of type: `username` is a string. The value will be the username of the user who is attempting to authenticate. Its value will never be the empty string. The value of `username` may have been modified by the previous `username/v1` transformations in the pipeline. `groups` is a list of strings. The value will be the list of group names to which the user attempting to authenticate belongs. The list may be empty, meaning that the user does not belong to any groups. The value of `groups` may have been modified by the previous `groups/v1` transformations in the pipeline. `strConst` contains the string constants declared for those transformations, and each string constant can be referenced using its name e.g. a string constant called `x` can be referenced as `strConst.x` `strListConst` contains the list constants declared for those transformations, and each list constant can be referenced using its name

a list constant called `x` can be referenced as `strListConst.x` Each identity provider selected for use in a FederationDomain may declare its own list of expressions. The expressions will only be applied when that FederationDomain uses that identity provider. Rather than repeating the same special strings across multiple expressions, you may optionally configure string constants and string list constants for your transformation pipeline. For example, if there is a special username or group name which will be used for comparisons in your expressions, then you might like to declare it as a string constant. If there is a special list of usernames or group names which will be used for comparisons then you might like to declare the list as a constant. Constants are available in every expression of the pipeline. Because the pipelines of expressions may behave differently based on their inputs, you may also optionally configure `examples` to demonstrate how a pipeline is expected to behave for various possible input scenarios. These examples act as living documentation for your fellow administrators, and also act as unit tests for your CEL expression code. Each example declares inputs for the whole pipeline of expressions, and also declares the expected results of the entire pipeline running on those inputs. The inputs are examples of the username and list of group names that might be determined by the related OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resource. The expected outputs are the username and list of group names, or the authentication rejection, for which your pipeline should result upon the given inputs. If any example does not behave as expected, Pinniped will mark the whole FederationDomain with an error in its `status` and users will not be allowed to use the FederationDomain to authenticate until the error is corrected. The following example is contrived to demonstrate every feature of the `transforms` configuration (constants, expressions, and examples). It is likely more complex than a typical configuration. Documentation for each of the fields shown below can be found in the API docs for the resource. ```yaml kind: FederationDomain apiVersion: config.supervisor.pinniped.dev/v1alpha1 metadata: name: demo-federation-domain namespace: pinniped-supervisor spec: issuer: https://issuer.example.com/demo-issuer tls: secretName: my-federation-domain-tls identityProviders: displayName: ActiveDirectory for Admins objectRef: apiGroup: idp.supervisor.pinniped.dev kind: ActiveDirectoryIdentityProvider name: ad-for-admins transforms: constants: name: prefix type: string stringValue: "ad:" name: onlyIncludeGroupsWithThisPrefix type: string stringValue: "kube/" name: mustBelongToOneOfThese type: stringList stringListValue: "kube/admins" "kube/developers" "kube/auditors" name: additionalAdmins type: stringList stringListValue: "ryan@example.com" "ben@example.com" "josh@example.com" expressions: type: policy/v1 expression: 'groups.exists(g, g in strListConst.mustBelongToOneOfThese)' message: "Only users in kube groups are allowed to authenticate" type: groups/v1 expression: 'username in strListConst.additionalAdmins ? groups + ["kube/admins"] : groups' type: groups/v1 expression: 'groups.filter(group, group.startsWith(strConst.onlyIncludeGroupsWithThisPrefix))' type: username/v1 expression: 'strConst.prefix + username' type: groups/v1 expression: 'groups.map(group, strConst.prefix + group)' examples: username: "ryan@example.com" groups: [ "kube/developers", "kube/auditors", "non-kube-group" ] expects: username: "ad:ryan@example.com" groups: [ "ad:kube/developers", "ad:kube/auditors", "ad:kube/admins" ] username: "someone_else@example.com" groups: [ "kube/developers", "kube/other", "non-kube-group" ] expects: username: "ad:someone_else@example.com" groups: [ "ad:kube/developers", "ad:kube/other" ] username: "paul@example.com" groups: [ "kube/other", "non-kube-group" ] expects: rejected: true message: "Only users in kube groups are allowed to authenticate" ``` Pinniped uses the cel-go library to implement CEL expressions. It includes the CEL as well as the . This section will attempt to highlight some of the useful features of CEL, but is not intended to be a comprehensive overview of everything that you can use in CEL expressions. CEL has several which may be called on strings: `contains`, `startsWith`, `endsWith`, and `matches` (for regex matching), e.g.

for a string `x` The include several additional functions which may be called on strings: `charAt`, `indexOf`, `join`, `lastIndexOf`, `lowerAscii`, `quote`, `replace`, `split`, `substring`, `trim`, `upperAscii`, and `reverse` CEL has : `map` and `filter` can be used to return a modified copy of a list `exists`, `exists_one`, and `all` can be used to perform boolean checks on the contents of a list Equality of strings and lists can be compared with the `==` and `!=` operators Lexicographic ordering of strings can be compared with `<`, `<=`, `>`, and `>=` operators Concatenation of two strings or two lists can be performed with the `+` operator List membership may be tested using the `in` operator, e.g. `"foo" in x` for a list `x` CEL does not have an `if` statement, but it does include a ternary operator to achieve the same result: `booleanexpression ? whentrueexpression : whenfalse_expression`. These may be nested, e.g. the `whentrueexpression` may itself be another ternary expression. Boolean operators include `&&` (and), `||` (or), `!` (not), `in` (inclusion in a list), and the ternary `?:` String literals can be quoted using single quotes `''` or double quotes `""` and List literals can be written as a comma-seperated list of elements within enclosing `[]` `[]` may be used to index into a list, e.g. `x[4]` for a list `x` `size(x)` returns the length of a string `x` or the length of a list `x` Below are some examples of using expressions for identity transformations and policies. Note that any of the string literals in these examples could be replaced by string constants, i.e. `"prefix"` could instead refer to a constant like `strConst.prefix`. Any literal list of strings could be replaced by a string list constant, e.g. `["allowed1", "allowed2"]` could instead refer to a constant like `strListConst.allowedGroups`. Prefix the username: `"prefix" + username` Suffix the username: `username + "suffix"` Down-case the username (be careful that this will cause "Ryan" and "ryan" to become the same user in Kubernetes): `username.lowerAscii()` Prefix all group names: `groups.map(g, "prefix" + g)` Suffix all group names: `groups.map(g, g + "suffix")` Filter groups to remove any group names that start with `system:` which is a prefix that has a special meaning to Kubernetes: `groups.filter(group, !group.startsWith("system:"))` Filter groups to keep only groups with a certain prefix: `groups.filter(group, group.startsWith("kube/"))` Down-case all group names (be careful that this will cause "Admins" and "admins" to become the same group in Kubernetes): `groups.map(g, g.lowerAscii())` Filter groups based on an allow list, dropping any group names except those included in the allow list: `groups.filter(g, g in ["allowed1", "allowed2"])` Filter groups based on a disallow list, dropping any group names included in the disallow list: `groups.filter(g, !(g in ["dropped1", "dropped2"]))` Filter groups based on a list of disallowed prefixes, dropping any groups which have one of the disallowed prefixes: `groups.filter(group, !(["disallowed-prefix1:", "disallowed-prefix2:"].exists(prefix, group.startsWith(prefix))))` Unconditionally add a group: `groups + ["new-group"]` Add a group, but only if the user already belongs to another specific group: `"other" in groups ? groups + ["new-group"] : groups` Rename a particular group if the user belongs to that group: `groups.map(g, g == "other" ? "other-renamed" : g)` Unconditionally drop all groups: `[]` User must belong to a particular group: `"required-group" in groups` User must belong to at least one of the groups in a list: `groups.exists(g, g in ["foobar", "foobaz", "foobat"])` User must belong to all the groups in a list: `["foobar", "foobaz", "foobat"].all(g, g in groups)` User must not belong to any of the groups in a list: `!groups.exists(g, g in ["foobar", "foobaz"])` Certain users are allowed to authenticate and everyone else is rejected: `username in ["foobar", "foobaz"]`