Custom SSO
OpenUnison is configured to provide SSO for your API server, the Kubernetes Dashboard, and potentially an API server proxy. There are more components to your cluster then your API server and dashboard though. Git repos, other dashboards like for Kiali for Istio and Traefik, monitoring systems, and gitops platforms all have their own GUIs with their own identity systems. OpenUnison can provide you a single source for authentication that you control. Once OpenUnison has been onboarded to your central identity provider, it can be an identity broker for any and all of these applications. This section describes how to integrate applications that:
- Use the same group data and trust as your Kubernetes cluster (such as ArgoCD)
- Use OpenID Connect, but need their own identity information
- Use SAML2
- Need a reverse proxy to send identity data and perform authentication for them, such as the Kubernetes Dashboard
Extending the Kubernetes Trust
In this simplest example, you can extend the existing Kubernetes OpenID Connect identity provider for your cluster. This is useful when:
- Your application will re-use your token to communicate with the API server. Many dashboards do this so that they don't need to know your credentials.
- Your application needs the same groups as is used by your cluster
- Your application needs the same attributes, or claims as your cluster
Setting this up is very straight forward. There are two configuration points:
- OpenUnison - Create a
Trust
custom resource describing the application you wish to allow to authenticate via OpenUnison - Your Application - Give your application the configuration it needs to trust OpenUnison.
The id_token
generated by the Kubernetes identity provider will look like:
{
"iss": "https://k8sou.apps.domain.com/auth/idp/k8sIdp",
"aud": "kubernetes",
"exp": 1645623659,
"jti": "HDJaev3WKncv6Y4da21XNQ",
"iat": 1645623599,
"nbf": 1645623479,
"sub": "mlbadmin1",
"name": " Boorshtein",
"groups": [
"users",
"admins"
],
"preferred_username": "mlbadminx-49-x",
"email": "mlbadmin1@nodomain.io"
}
The main claims that you'll be concerned with are:
Claim | Description |
---|---|
sub | The user's unique id, this will map to the attribute cnfigured in your helm chart and will depend on your upstream identity provider. |
name | This is generally the user's full name |
groups | This is a list of groups the user is a member of |
preferred_username | This is the name of the User object in the openunison namespace that is used to represent this user. This can also be used to delete the user's session, forcing them to re-authenticate |
The user's email address |
Next, we'll cover both of these configuration points.
Configuring OpenUnison
Before configuring a Trust
for OpenUnison for an application, you'll need to know:
Data | Description | Example |
---|---|---|
Application redirect | This is a URL that OpenUnison will redirect the user's browser to after authentication. There can be multiple URLs specified. This is common when an application has both a web component and a CLI component, like ArgoCD. See your application's documentation. | https://app.domain.com/redirect/ |
Client Secret | If the application is a web application, there's generally a client secret that you'll need to generate. This secret should be long, random, and can't be easily guessed. NOTE for applications with a cli component, like ArgoCD, you'll generally skip the client secret | |
Will user_info be signed? |
Some applications require that the data returned from the user_info endpoing be a signed JWT. Others just want JSON. Plain JSON is more common |
Once you gather this information, the next step is create a Secret
for your client_secret
, if needed. The easiest way to do this is in the orchestra-secrets-source
Secret
you created when deploying OpenUnison. Once you created this Secret
, you can create the Trust
. The below is an example for GitLab :
apiVersion: openunison.tremolo.io/v1
kind: Trust
metadata:
labels:
app.kubernetes.io/name: openunison
app.kubernetes.io/instance: openunison-orchestra
app.kubernetes.io/component: gitlab-sso
app.kubernetes.io/part-of: openunison
name: gitlab
namespace: openunison
spec:
accessTokenSkewMillis: 120000
accessTokenTimeToLive: 60000
authChainName: login-service
clientId: gitlab
clientSecret:
keyName: gitlab
secretName: orchestra-secrets-source
codeLastMileKeyName: lastmile-oidc
codeTokenSkewMilis: 60000
publicEndpoint: false
redirectURI:
- https://gitlab.mydomain.com/users/auth/openid_connect/callback
signedUserInfo: false
verifyRedirect: true
The above YAML can be added to the openunison
namespace and OpenUnison will pick up the changes automatically. If no secret is needed, skip spec.clientSecret
. Below are the details for each configuration option.
Option | Desription |
---|---|
accessTokenSkewMillis | Milliseconds milliseconds added to account for clock skew |
accessTokenTimeToLive | Time an access token should live in milliseconds |
authChainName | The authentication chain to use for login, do not change |
clientId | The client id shared by your application |
clientSecret.scretName | If using a client secret, the name of the Secret storing the client secret |
clientSecret.keyName | The key in the data section of the Secret storing the client secret |
codeLastMileKeyName | The name of the key used to encrypt the code token, do not change |
codeTokenSkewMilis | Milliseconds to add to code token lifetime to account for clock skew |
publicEndpoint | If true , a client secret is required. If false , no client secret is needed |
redirectURI | List of URLs that are authorized for callback. If a URL is provided by your application that isn't in this list SSO will fail |
signedUserInfo | if true , the userinfo endpoint will return a signed JSON Web Token. If false it will return plain JSON |
verifyRedirect | If true , the redirect URL provided by the client MUST be listed in the redirectURI section. Should ALLWAYS be true if not in a development environment |
Once the Trust
has been created, the next step is to configure your application.
Configuring Your Application
Once OpenUnison has been configured, the next step is to configure your application. If your application is able to just accept an issuer, use the URL of your identity provider : https://host.domain.com/auth/idp/k8sIdp
where host.domain.com
is the host name for your OpenUnison instance. You'll also suplly your Trust
's clientId
, and if applicable the client secret you created.
If your application can't lookup this information from the OpenID Connect discovry URL provided by OpenUnison, here are the URLs you can supply:
URL Type | URL |
---|---|
issuer | https://host.domain.com/auth/idp/k8sIdp |
authorization endpoint | https://host.domain.com/auth/idp/k8sIdp/auth |
token endpoint | https://host.domain.com/auth/idp/k8sIdp/token |
user info endpoint | https://host.domain.com/auth/idp/k8sIdp/userinfo |
Again, replace host.domain.com
with the host of your OpenUnison deployment.
Finally you can also add a badge to the front page of your OpenUnison portal as well.
Creating an OpenID Connect Identity Provider
There are multiple scenarios where you can't build a trust off of the trust built for your cluster:
- You need different groups or want to limit groups
- You need specific claims
- You need the existing claims to have different names
- You need to perform a just-in-time provisioning action to a trusted application
In these situations, instead of creating a Trust
object, you'll create an Application
object that defines a brand new identity provider. In this example, we'll create an identity provider for ArgoCD that will limit the groups sent to ArgoCD to only groups that start with argocd-
. Also, ArgoCD doesn't like groups from LDAP in distinguished name form, so we'll take just the common name element from the group. Once we're done, a user with the groups:
cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com
cn=group2,ou=Groups,DC=domain,DC=com
cn=argocd-users,ou=Groups,DC=domain,DC=com
cn=argocd-admins,ou=Groups,DC=domain,DC=com
will only send
to ArgoCD. The first step is to create a custom JavaScriptMapping
object. Nearly every configuration in OpenUnison can be customized using some JavaScript. Here's the mapping for our groups:
Line Numbers | Description |
---|---|
1 - 8 | The YAML used to define this mapping |
9 | Function definition, every mapping must have a single function that takes two parameters: the user (com.tremolosecurity.provisioning.core.User ) and the name of the attribute to create (java.lang.String ). This function must resturn an object of com.tremolosecurity.saml.Attribute |
10 | Make the type com.tremolosecurity.saml.Attribute accessible as Attribute in the javascript. This can be done for any Java class |
13 - 31 | Perform our business logic |
35 | Returns our new attribute |
Once the mapping is created, the next step is to define the Application
object:
Here's the explination of each part of this configuration:
Lines | Description |
---|---|
1 - 12 | Standard kubernetes metadata |
13 | Determines how long authorization decisions should be cached. Should generally not be changes and should only be as long as it typically takes a single action (such as an API or page load) to complete |
14 - 23 | Determines how the user's session cookie should be configured. This section should generally be left unchanged when working with Kubernetes |
24 | This tells OpenUnison this applications is an identity provider, not a reverse proxy |
25 | Every Application in OpenUnison is made of multiple url entries. Identity providers, like this one, only have one URL definition. |
26 - 28 | Each URL can have its own authorization rules (azRules ). An authorization rule is made of a scope (one of dn ,group ,filter ,custom ) and a constraint , which defines what the rule is. The combination of dn and o=Tremolo tells OpenUnison to authorize any user in the internal LDAP virtual directory. If you wanted to only allow users in the group cn=argocd-users,ou=Groups,DC=domain,DC=com you would use filter as the scope and (groups=cn=argocd-users,ou=Groups,DC=domain,DC=com) as the constraint because OpenUnison internally represents groups in the Kubernetes integration as attributes instead of seperate objects. Multipls rules may be listed. If ANY rule passes, the url is authorized |
29 | Customizations can be done here to impect the request and response. For identity providers, this is generally not needed |
30 - 31 | URLs need specific hosts to be associated with. This is generally left unchanged |
32 - 77 | The idp section is where you define the details for your identity provider |
33 | The className should not change |
34 - 51 | The mappings section defines which claims, or attributes, will be included in the id_token provided to your application. These same claims are provided by the user_info endpoint. OpenUnison makes no distinction between the attributes in the id_token vs user_info endpoint. Each claim has three attributes. The sourceType can be a user , composite , static , or custom . When user , the claim is pulled straight for the user's object based on the targetAttributeSource attribute. If static , the targetAttributeSource is taken as is. If composite , the targetAttributeSource provides attributes and text from the user. Finally, custom lets you define a custom implmenetation. For groups , the targetAttributeSource is com.tremolosecurity.mapping.JavaScriptMapping|k8s,openunison,argocd-groups . The com.tremolosecurity.mapping.JavaScriptMapping| generally won't change. The k8s tells OpenUnison which cluster to pull the mapping from. openunison is the namespace and finally argocd-groups is the JavaScriptMapping defined earlier. Finally, strict tells OpenUnison to only include the attributes specifically defined here. In general this should be true |
52 - 56 | This section defines parameters for the identity provider and should generally be left unchanged. |
57 -73 | The trusts section is where you define which applications will trust this identity provdier. This has the same options as a Trust object. |
70 - 73 | Each trust can provide a list of secretParams that references external Secret objects instead of local configuration. |
74 - 76 | The results section defines what happens in response to certain events. Here, in response to the auFail and azFail events, the user is redirected to an invalid credentials page |
77 | The uri tells OpenUnison how to access this identity provider. It will always follow the pattern /auth/idp/appname . |
Once your Application
is deployed, you can test it without any kind of container restart. Once you're tested, you can also add a badge to the front page of your OpenUnison portal as well.
Creating a SAML2 Identity Provider
Using a Reverse Proxy Application
In addition to acting as an identity provider for applications that know how to authenticate via OpenID Connect and SAML2, OpenUnison can replace your oauth2 proxy for applications and integrate directly. When integrating an application using OpenUnison's reverse proxy, the first step is to determine if your application can use your existing OpenUnison host name, or if it needs its own. As an example, Prometheus can either run off the root of your host (/
), or you can configure it to have a prefix URL such as /prometheus
. This section will walk through both scenarios.
Using OpenUnison's Host
This is the easier of the two scenarios. Similar to identity providers, Prometheus will be integrated using an Application
object. First, configure your Prometheus to use the correct external URL and prefix. For instance, if your OpenUnison is configured to run on openunison.domain.com
make sure your Promtheus StatefulSet
has the following configureation parameters:
Next, create the below object in your cluster:
Here is the details of this configuration:
Lines | Description |
---|---|
1 - 7 | Standard kubernetes metadata |
8 | Determines how long authorization decisions should be cached. Should generally not be changes and should only be as long as it typically takes a single action (such as an API or page load) to complete |
10 | Every Application in OpenUnison is made of multiple url entries. Prometheus only requires a single url but this could be used for different urls in your application if needed |
11 - 12 | URLs need specific hosts to be associated with. This is left unchanged in this use case |
13 | Applications can have a chain of filters that an act on headers and requested cookies as well as influence which cookies and headers are returned |
14 | The XForward filter injects the original host name as the X-FORWARDED-FOR header. It will also set X-FORWARDED-PROTO |
17 | The 'HideCookies' filter will keep an application's own cookies in OpenUnison's internal session without ever sending them to the browser. |
19 | The uri tells OpenUnison how to access this application. In this instance, any URLs that start with /prometheus will be forewarded to this Application |
20 | This line tells OpenUnison where to forward requests to. Add ${fullURI} to get the originaly requested URI |
21 | The auth-chain is the name of an AuthChain object the URL will require for authentication. In this use-case, do not change |
22 - 24 | Each URL can have its own authorization rules (azRules ). An authorization rule is made of a scope (one of dn ,group ,filter ,custom ) and a constraint , which defines what the rule is. The combination of dn and o=Tremolo tells OpenUnison to authorize any user in the internal LDAP virtual directory. If you wanted to only allow users in the group cn=argocd-users,ou=Groups,DC=domain,DC=com you would use filter as the scope and (groups=cn=argocd-users,ou=Groups,DC=domain,DC=com) as the constraint because OpenUnison internally represents groups in the Kubernetes integration as attributes instead of seperate objects. Multipls rules may be listed. If ANY rule passes, the url is authorized |
25 - 26 | The results section defines what happens in response to certain events. Here, in response to the auFail and azFail events, the user is redirected to an invalid credentials page |
27 | Tells OpenUnison to send the host defined in the requested URL in the HOST header, usually set to true |
28 | Tells OpenUnison to update the host in redirects to what is the requested URL, usually set to true |
29 - 32 | Provide OpenUnison with custom timeouts for your application. Setting these timeouts will help if applications take too long to respond |
33 - 39 | Determines how the user's session cookie should be configured. This section should generally be left unchanged when working with Kubernetes |
Once the Application
object has been added to the openunison
namespace, you can access Prometheus by going to https://openunison.domain.com/prometheus
. There's no need to create any new Ingress
objects because you're re-using OpenUnison's own existing Ingress
.
Finallly, you'll want to add a badge to your portal so users can access Prometheus without having to memorize the URL.
Using a New Host
If your application can't use OpenUnison's current host name, you can still configure OpenUnison to provide authentication and authorization for your application.
Most apps can be integrated directly via the Helm charts without any additional customization. In the openunison
section of your values.yaml, create a list called apps and add your application's configuration. Here's an example for Grafana:
openunison:
.
.
.
apps:
- name: grafana
label: Grafana
org: b1bf4c92-7220-4ad2-91af-ee0fe0af7312
badgeUrl: https://grafana.apps.192-168-2-100.nip.io/
injectToken: false
azSuccessResponse: grafana
proxyTo: http://prometheus-grafana.monitoring.svc${fullURI}
az_groups:
- cn=k8s-cluster-admins,ou=Groups,DC=domain,DC=com
icon: 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Pk4fgCx1lb4BDg6B0RJlr/u8O9Ld279n2v39AC/icoySQsigAAANHGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDo3OTljYmNiOC04NzFiLTRhOTAtOTliMy1jNWEyNjQzMTMxZmUiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDEzNGU2ZDEtYzkyZC00MzlhLTgyODEtODUzNjcyNmZjNGYwIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6YTg2OWVkYWUtZjY1MC00NGZlLThhMzItOTkyNGMyMDI1NGZjIgogICBkYzpGb3JtYXQ9ImltYWdlL3BuZyIKICAgR0lNUDpBUEk9IjIuMCIKICAgR0lNUDpQbGF0Zm9ybT0iTWFjIE9TIgogICBHSU1QOlRpbWVTdGFtcD0iMTY4NDM1NTI0MzkwMTExNSIKICAgR0lNUDpWZXJzaW9uPSIyLjEwLjI4IgogICB0aWZmOk9yaWVudGF0aW9uPSIxIgogICB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InNhdmVkIgogICAgICBzdEV2dDpjaGFuZ2VkPSIvIgogICAgICBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjgxZTgzYmI5LTYxYjgtNDY1MS04ZDkwLWUwY2MzNzY1YWY3YyIKICAgICAgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChNYWMgT1MpIgogICAgICBzdEV2dDp3aGVuPSIyMDIzLTA1LTE3VDE2OjI3OjIzLTA0OjAwIi8+CiAgICA8L3JkZjpTZXE+CiAgIDwveG1wTU06SGlzdG9yeT4KICA8L3JkZjpEZXNjcmlwdGlvbj4KIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAKPD94cGFja2V0IGVuZD0idyI/PhgsBSQAAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfnBREUGxfWGnvhAAAgAElEQVR42u2dd3hc1dHGf3N3V83SSi64V9wkY9MhGAzGTgj1o8ZgS6EYcMEYQygOJZQQmqk2PQaMCbaMgRBCTYCA+SghGAIOYMnGDTDuTatqSXvn++MuhI9iS7J2b9nzPs95RJF2554z7505c+bMgIGBgYGBgYGBgYGBgYGBgYGBgYGBgYGB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
Each option will help generate the appropriate OpenUnison objects:
Option | Description |
---|---|
name | This is the name that is used for creating objects. Make sure it conforms to Kubernetes' naming standard |
label | The label for badge in OpenUnison's portal page |
org | The id of the Organization to include the badge in. If using the SSO portal, use b1bf4c92-7220-4ad2-91af-ee0fe0af7312 |
badgeUrl | The URL of the application as deployed in OpenUnison. This is what will be linked to by the badge. NOTE: The host name must point to the same LoadBalancer used by OpenUnison so it can pickup the Ingeress created by the helm chart. |
injectToken | If true , OpenUnison will inject the user's identity as Kubernetes sees them. Either as an id_token in the Authorization header or as Kubernetes impersonation headers |
azSuccessResponse | The name of a ResultGroup that can be used to inject additional data into requests, such as a user's identity |
az_groups | List of groups the user must be a member of to access this application. If none are specified, all users will have access |
proxyTo | The URL to proxy all requests to. In Kubernetes, this will usually be a host name for your application's Service |
icon | A base64 encoded PNG that is 240px high and 210px wide that will be displayed on the OpenUnison portal |
If your application is interacting with Kubernetes and interacts with Kubernetes on your behalf, OpenUnison will inject the user's identity for your user, whether the cluster is integrated by token or by impersonation headers.
For when your application doesn't need Kubernetes' identity you can generally inject the user's identity via a header. This is done in OpenUnison by creating a ResultGroup
object. For the Grafana example, we created the below object:
apiVersion: openunison.tremolo.io/v1
kind: ResultGroup
metadata:
name: grafana
namespace: openunison
spec:
- resultType: header
source: static
value: X-WEBAUTH-GROUPS=Admin
- resultType: header
source: user
value: X-WEBAUTH-USER=uid
The user's identity is set by creating the header X-WEBAUTH-USER
with the user's uid attribute.
Once OpenUnison is redeployed using ouctl, going to the URL specified by badgeUrl
or by logging into OpenUnison and clicking on your application badge will authenticate you and forward you into your application without building custom objects. If you need a more custom experience, the next section details the objects to create.
Manually Creating Objects
There are a couple of additional steps:
- Define an
AuthChain
that trusts OpenUnison's kubernetes identity provider - Create an
Application
that references this newAuthenticaionChain
- Create a
Trust
object that binds theAuthChain
and the Kubernetes identity provider - Create an
Ingress
object
As an example, we'll configure a Prometheus instance that's running on its own host name - prometheus.domain.com
. The Promtheus StatefulSet
has the following configureation parameters:
First, we'll define an AuthChain
. OpenUnison uses an AuthChain
to create a sequence of AuthMechs
to authenticate a user. You generally won't need to worry aobut defining your own AuthMech
objects. Create this object in the openunison
namespace:
Here are the details of the configuration:
Lines | Description |
---|---|
1 - 5 | Standard kubernetes metadata |
7 - 28 | Defines each step in the chain referencing an AuthMech |
8 | The name of the AuthMech to use, in this case oidc |
10 | The name of the resulting id_token in a filter's request object |
11 | The clientid that will be defined in our Trust |
12 | If a user can't be found, the default objectClass, should not be changed |
13 | Only used with Google as an identity provider |
14 | The idp authorization URL |
15 | The attribute with the id_token in the response, do not change |
16 | Determines if OpenUnison should link the account with an internal directory object, keep true |
17 | The IdP's token URL, do not change |
18 - 23 | How OpenUnison should find the user, do not change |
24 | If the AuthMech is optional, do not change |
25 - 28 | List of parameters that will be pulled from a Secret . Our configuration pulls the same Secret that was created for the dashboard trust |
29 | The authentication level, useful when you have multiple forms of authentication. Do not change |
30 | The search root inside of OpenUnison's internal LDAP virtual directory, do not change |
Next, create the Application
object:
Here is the details of this configuration:
Lines | Description |
---|---|
1 - 7 | Standard kubernetes metadata |
8 | Determines how long authorization decisions should be cached. Should generally not be changes and should only be as long as it typically takes a single action (such as an API or page load) to complete |
10 | Every Application in OpenUnison is made of multiple url entries. Prometheus only requires a single url but this could be used for different urls in your application if needed |
11 - 12 | URLs need specific hosts to be associated with. This needs to be the host name you want to use to access your application |
13 | Applications can have a chain of filters that an act on headers and requested cookies as well as influence which cookies and headers are returned |
14 | The XForward filter injects the original host name as the X-FORWARDED-FOR header. It will also set X-FORWARDED-PROTO |
17 | The 'HideCookies' filter will keep an application's own cookies in OpenUnison's internal session without ever sending them to the browser. |
19 | The uri tells OpenUnison how to access this application. In this instance, any URLs that start with / will be forewarded to this Application |
20 | This line tells OpenUnison where to forward requests to. Add ${fullURI} to get the originaly requested URI |
21 | The auth-chain is the name of an AuthChain object the URL will require for authentication. In this use-case, use prometheus-oidc to reference the chain created above |
22 - 24 | Each URL can have its own authorization rules (azRules ). An authorization rule is made of a scope (one of dn ,group ,filter ,custom ) and a constraint , which defines what the rule is. The combination of dn and o=Tremolo tells OpenUnison to authorize any user in the internal LDAP virtual directory. If you wanted to only allow users in the group cn=argocd-users,ou=Groups,DC=domain,DC=com you would use filter as the scope and (groups=cn=argocd-users,ou=Groups,DC=domain,DC=com) as the constraint because OpenUnison internally represents groups in the Kubernetes integration as attributes instead of seperate objects. Multipls rules may be listed. If ANY rule passes, the url is authorized |
25 - 26 | The results section defines what happens in response to certain events. Here, in response to the auFail and azFail events, the user is redirected to an invalid credentials page |
27 | Tells OpenUnison to send the host defined in the requested URL in the HOST header, usually set to true |
28 | Tells OpenUnison to update the host in redirects to what is the requested URL, usually set to true |
29 - 32 | Provide OpenUnison with custom timeouts for your application. Setting these timeouts will help if applications take too long to respond |
33 - 39 | Determines how the user's session cookie should be configured. This section should generally be left unchanged when working with Kubernetes |
Next, create a Trust
:
apiVersion: openunison.tremolo.io/v1
kind: Trust
metadata:
labels:
app.kubernetes.io/name: openunison
app.kubernetes.io/instance: openunison-orchestra
app.kubernetes.io/component: prometheus
app.kubernetes.io/part-of: openunison
name: prometheus
namespace: openunison
spec:
accessTokenSkewMillis: 120000
accessTokenTimeToLive: 60000
authChainName: login-service
clientId: prometheus
clientSecret:
keyName: K8S_DB_SECRET
secretName: orchestra-secrets-source
codeLastMileKeyName: lastmile-oidc
codeTokenSkewMilis: 60000
publicEndpoint: false
redirectURI:
- https://prometheus.domain.com/auth/oidc
signedUserInfo: true
verifyRedirect: true
Here are the details:
Option | Desription |
---|---|
accessTokenSkewMillis | Milliseconds milliseconds added to account for clock skew |
accessTokenTimeToLive | Time an access token should live in milliseconds |
authChainName | The authentication chain to use for login, do not change |
clientId | The client id shared by your application |
clientSecret.scretName | If using a client secret, the name of the Secret storing the client secret |
clientSecret.keyName | The key in the data section of the Secret storing the client secret |
codeLastMileKeyName | The name of the key used to encrypt the code token, do not change |
codeTokenSkewMilis | Milliseconds to add to code token lifetime to account for clock skew |
publicEndpoint | If true , a client secret is required. If false , no client secret is needed |
redirectURI | List of URLs that are authorized for callback. If a URL is provided by your application that isn't in this list SSO will fail |
signedUserInfo | if true , the userinfo endpoint will return a signed JSON Web Token. If false it will return plain JSON |
verifyRedirect | If true , the redirect URL provided by the client MUST be listed in the redirectURI section. Should ALLWAYS be true if not in a development environment |
Finally, create an Ingress
object for your new application that reference's OpenUnison's Service
. The best option is to get the source for the openunison-orchestra
object and customize it for your new application in a new Ingress
object. For instance, with an NGINX Ingress Controller:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/affinity: cookie
nginx.ingress.kubernetes.io/backend-protocol: https
nginx.ingress.kubernetes.io/secure-backends: "true"
nginx.ingress.kubernetes.io/session-cookie-hash: sha1
nginx.ingress.kubernetes.io/session-cookie-name: openunison-orchestra
nginx.org/ssl-services: openunison-orchestra
name: prometheus
namespace: openunison
spec:
rules:
- host: prometheus.domain.com
http:
paths:
- backend:
service:
name: openunison-orchestra
port:
number: 443
path: /
pathType: Prefix
tls:
- hosts:
- prometheus.domain.com
secretName: ou-tls-certificate
Once the Ingress
object has been added to the openunison
namespace, you can access Prometheus by going to https://prometheus.domain.com/
.
Finallly, you'll want to add a badge to your portal so users can access Prometheus without having to memorize the URL.
Adding a "Badge" to the Portal
When you login to the Orchestra portal, there are badges for your tokens and for the dashboard. You can dynamically add a badge for your application too. Here's an example PortalUrl
object for ArgoCD:
apiVersion: openunison.tremolo.io/v1
kind: PortalUrl
metadata:
name: argocd
namespace: openunison
spec:
label: ArgoCD
org: B158BD40-0C1B-11E3-8FFD-0800200C9A66
url: https://argocd.apps.192-168-2-140.nip.io
icon: iVBORw0KGgoAAAANSUhEUgAAANIAAADwCAYAAAB1/Tp/AAAfQ3pUWHRSYXcgcHJvZ...
azRules:
- constraint: o=Tremolo
scope: dn
Option | Descriptoin |
---|---|
label | The label shown on badge in the portal |
org | If using orgnaizations to organize badges, the uuid of the org. If not using organizations, leave as is |
url | The URL the badge should send the user to |
icon | A base64 encoded PNG with a width of 210 pixels and a height of 240 pixels |
azRules | Who is authorized to see this badge? See https://portal.apps.tremolo.io/docs/tremolosecurity-docs/1.0.19/openunison/openunison-manual.html#_applications_applications for an explination of the authorization rules |
Once created, the badge will appear in the Orchestra portal! No need to restart the containers.