Skip to content

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:

  1. 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.
  2. Your application needs the same groups as is used by your cluster
  3. Your application needs the same attributes, or claims as your cluster

Setting this up is very straight forward. There are two configuration points:

  1. OpenUnison - Create a Trust custom resource describing the application you wish to allow to authenticate via OpenUnison
  2. 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
email 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

argocd-user
argocd-admins

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:

---
apiVersion: openunison.tremolo.io/v1
kind: JavaScriptMapping
metadata:
  namespace: openunison
  name: argocd-groups
spec:
  javascript: |-
    function doMapping(user,name) {
      Attribute = Java.type("com.tremolosecurity.saml.Attribute");

      // get the current groups from the user
      var groups = user.getAttribs().get("groups").getValues()

      // list of simple group names
      simpleGroups = new Attribute(name);


      // convert groups from LDAP DNs from Active Directory to standard group names
      for (var i=0;i<groups.length;i++) {
        var group = groups.get(i);

        // remove everything before the first '=' and after the first comma ','
        // ex cn=my-group,dc=domain,dc=com --> my-group
        var simpleGroupName = group.substring(group.indexOf('=') + 1, group.indexOf(','));

        // we only want to send argocd groups
        if (simpleGroupName.startsWith('argocd-')) {
          simpleGroups.getValues().add(simpleGroupName);
        }
      }



      return simpleGroups;
    }
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:

---
apiVersion: openunison.tremolo.io/v2
kind: Application
metadata:
  labels:
    app.kubernetes.io/component: openunison-applications
    app.kubernetes.io/instance: openunison-orchestra-login-portal
    app.kubernetes.io/name: openunison
    app.kubernetes.io/part-of: openunison
  name: argocd
  namespace: openunison
spec:
  azTimeoutMillis: 3000
  cookieConfig:
    cookiesEnabled: true
    domain: '#[OU_HOST]'
    httpOnly: true
    keyAlias: session-unison
    logoutURI: /logout
    scope: -1
    secure: true
    sessionCookieName: tremolosession
    timeout: 900
  isApp: false
  urls:
  - azRules:
    - constraint: o=Tremolo
      scope: dn
    filterChain: []
    hosts:
    - '#[OU_HOST]'
    idp:
      className: com.tremolosecurity.idp.providers.OpenIDConnectIdP
      mappings:
        map:
        - sourceType: user
          targetAttributeName: sub
          targetAttributeSource: sub
        - sourceType: composite
          targetAttributeName: name
          targetAttributeSource: ${givename} ${sn}
        - sourceType: user
          targetAttributeName: preferred_username
          targetAttributeSource: uid
        - sourceType: user
          targetAttributeName: email
          targetAttributeSource: mail
        - sourceType: custom
          targetAttributeName: groups
          targetAttributeSource: com.tremolosecurity.mapping.JavaScriptMapping|k8s,openunison,argocd-groups
        strict: true
      params:
        jwtSigningKey: unison-saml2-rp-sig
        k8sNameSpace: 'openunison'
        k8sTarget: k8s
        sessionStoreClassName: com.tremolosecurity.oidc.k8s.K8sSessionStore
      trusts:
      - name: 'https://argocd.apps.192-168-2-104.nip.io/'
        params:
          accessTokenSkewMillis: "120000"
          accessTokenTimeToLive: '60000'
          authChainName: login-service
          clientID: argocd-argocdweb
          codeLastMileKeyName: lastmile-oidc
          codeTokenSkewMilis: '60000'
          publicEndpoint: "true"
          redirectURI: 
          - https://argocd.apps.192-168-2-104.nip.io/auth/callback
          - http://localhost:8085/auth/callback
        secretParams:
        - name: clientSecret
          secretName: orchestra-secrets-source
          secretKey: K8S_DB_SECRET
    results:
      auFail: default-login-failure
      azFail: default-login-failure
    uri: /auth/idp/argocd

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:

- '--web.external-url=https://openunison.domain.com/prometheus'
- '--web.route-prefix=/prometheus/'

Next, create the below object in your cluster:

---
apiVersion: openunison.tremolo.io/v2
kind: Application
metadata:
  name: prometheus
  namespace: openunison
spec:
  azTimeoutMillis: 3000
  isApp: true
  urls:
  - hosts:
    - "#[OU_HOST]"
    filterChain:
    - className: com.tremolosecurity.proxy.filters.XForward
      params:
        createHeaders: "true"
    - className: com.tremolosecurity.proxy.filters.HideCookie
      params: {}
    uri: "/prometheus"
    proxyTo: "http://prom-stack-kube-prometheus-prometheus.monitoring.svc:9090${fullURI}"
    authChain: login-service
    azRules:
    - scope: dn
      constraint: o=Tremolo
    results:
      azFail: default-login-failure
    overrideHost: true
    overrideReferer: true
    proxyConfiguration:
      connectionTimeoutMillis: 5000
      requestTimeoutMillis: 5000
      socketTimeoutMillis: 5000
  cookieConfig:
    sessionCookieName: tremolosession
    domain: "#[OU_HOST]"
    secure: true
    httpOnly: true
    logoutURI: "/logout"
    keyAlias: session-unison

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:

  1. Define an AuthChain that trusts OpenUnison's kubernetes identity provider
  2. Create an Application that references this new AuthenticaionChain
  3. Create a Trust object that binds the AuthChain and the Kubernetes identity provider
  4. 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:

- '--web.external-url=https://prometheus.domain.com/'
- '--web.route-prefix=/'

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:

apiVersion: openunison.tremolo.io/v1
kind: AuthenticationChain
metadata:
  name: prometheus-oidc
  namespace: openunison
spec:
  authMechs:
  - name: oidc
    params:
      bearerTokenName: prometheus-ou
      clientid: prometheus
      defaultObjectClass: inetOrgPerson
      hd: ""
      idpURL: https://#[OU_HOST]/auth/idp/k8sIdp/auth
      jwtTokenAttributeName: id_token
      linkToDirectory: "true"
      loadTokenURL: https://#[OU_HOST]/auth/idp/k8sIdp/token
      lookupFilter: (sub=${sub})
      noMatchOU: oidc
      responseType: code
      scope: openid name offline
      uidAttr: sub
      userLookupClassName: com.tremolosecurity.unison.proxy.auth.openidconnect.loadUser.LoadJWTFromAccessToken
    required: required
    secretParams:
    - name: secretid
      secretKey: K8S_DB_SECRET
      secretName: orchestra-secrets-source
  level: 20
  root: o=Tremolo

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:

---
apiVersion: openunison.tremolo.io/v2
kind: Application
metadata:
  name: prometheus
  namespace: openunison
spec:
  azTimeoutMillis: 3000
  isApp: true
  urls:
  - hosts:
    - prometheus.domain.com
    filterChain:  
    - className: com.tremolosecurity.proxy.filters.XForward
      params:
        createHeaders: "true"
    - className: com.tremolosecurity.proxy.filters.HideCookie
      params: {}
    uri: "/"
    proxyTo: "http://prom-stack-kube-prometheus-prometheus.monitoring.svc:9090${fullURI}"
    authChain: prometheus-oidc
    azRules:
    - scope: dn
      constraint: o=Tremolo
    results:
      azFail: default-login-failure
    overrideHost: true
    overrideReferer: true
    proxyConfiguration:
      connectionTimeoutMillis: 5000
      requestTimeoutMillis: 5000
      socketTimeoutMillis: 5000
  cookieConfig:
    sessionCookieName: tremolosession
    domain: "prometheus.domain.com"
    secure: true
    httpOnly: true
    logoutURI: "/logout"
    keyAlias: session-unison

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.