Now that we’ve figured out how to get Jenkins to talk to FreeIPA directly we can explore other authentication schemes. We are going to start with Keycloak because it provides an OpenID API which we can consume in other applications to avoid some of the complexity of talking LDAP to FreeIPA.

Preparing Keycloak Prerequisites

LDAP Service Account

Since we will be talking to FreeIPA’s LDAP server we will need to create a new service account for Keycloak with a secure password. Set one up in FreeIPA and note down both the bind DN and the password for use later in the realm configuration.

Kubernetes Namespace

Like with all of the other projects in this series we’re going to create a unique namespace for Keycloak to deploy into. This can be done with the rancher tool or with raw kubectl if you don’t mind the namespace showing up in the default project.

# Rancher CLI
$ export RANCHER_URL=https://rancher.example.com
$ export RANCHER_TOKEN=<authentication_token>
$ rancher login $RANCHER_URL --token $RANCHER_TOKEN
$ rancher namespaces create keycloak

# Kubernetes CLI
$ export KUBECONFIG=/path/to/kube_config.yml
$ kubectl create namespace keycloak

Administrator and Database Passwords

We need two password secrets for Keycloak to run properly, one for the Keycloak administrative user and one to pass to the bundled PostgreSQL chart to set up the database.

# PostgreSQL
$ kubectl create secret generic keycloak-postgres-password \
    --namespace keycloak \
    --from-literal password=<db_password>

# Keycloak Administrator
$ kubectl create secret generic keycloak-admin-password \
    --namespace keycloak \
    --from-literal password=<admin_password>

While we can mostly ignore the Postgres password so long as it is secure, we should note down the Keycloak administrator password so that we can access the Keycloak web UI during any testing and debugging.

Note: In re-reading the Keycloak Helm chart documentation it seems like this configuration, and the later database value configuration is unnecessary if you are using the bundled PostgreSQL chart. I have left it in here to document my actual steps.

SSL Certificates

Like all of my other services, this one is deployed with a SSL certificate issued by FreeIPA. I have the key and signed certificate and added them to the namespace as a standard TLS Secret.

$ kubectl create secret tls tls-keycloak-ingress \
    --namespace keycloak \
    --cert=keycloak.cert \
    --key=keycloak.key

FreeIPA Keycloak Realm

This is where the real fun begins! I built my configuration using a combination of official documentation and a realm example provided by the keycloak-freeipa-docker project on GitHub. You can download the full example file here.

Unpacking freeipa-realm.json

The JSON file above is daunting when you first look at it but what it breaks down to is mostly a series of mappings which we’ll go through now. More documentation is available from the Keycloak Server Administration Guide.

The first section is where we configure the realm ID and provide a display name to show on the realm login screen. This is also where we set if we want the realm to be active and what credentials it requires. In my case I went with password only but Kerberos tokens can also be used for an even more seamless interface.

{
  "id": "freeipa-realm",
  "realm": "freeipa-realm",
  "displayName": "Example.com Realm",
  "enabled": true,
  "requiredCredentials": [
    "password"
  ],

Next, we have the client list which are all of the clients allowed to use that realm for authentication and authorization. Right now this is just Jenkins over the OpenID protocol but more services could be added as we bring more things online. Keycloak supports several different protocols and will interact nicely with most web applications.

  "clients": [
    {
      "clientId": "jenkins",
      "enabled": true,
      "publicClient": true,
      "protocol": "openid-connect",
      "rootUrl": "https://jenkins.rancher.example.com",
      "redirectUris": [
        "https://jenkins.rancher.example.com/*"
      ],
      "webOrigins": [
        "https://jenkins.rancher.example.com"
      ]
    }
  ]

Now we have the actual LDAP configuration to connect Keycloak with FreeIPA. This is where we will use that service account we created earlier. I still haven’t figured out how to get LDAPS working with Keycloak but for now regular LDAP is ok. I have a sneaking suspicion the fix for LDAPS here will be strikingly similar to the one I devised for Jenkins. Big differences from the official LDAP realm is that we’ve configured the vendor to be rhds for RedHat Directory Server, the enterprise version of FreeIPA, and we’ve set the uuidLDAPAttribute to be ipaUniqueID. Additionally, we set the entire provider as read-only and to not sync registrations from Keycloak to FreeIPA. We want our FreeIPA install to be the single source of truth in our domain right now and we will manage our users and groups from there.

  "userFederationProviders": [
    {
      "displayName": "ldap-freeipa",
      "providerName": "ldap",
      "priority": 1,
      "fullSyncPeriod": -1,
      "changedSyncPeriod": -1,
      "config": {
        "pagination": "true",
        "debug": "false",
        "searchScope": "1",
        "connectionPooling": "true",
        "usersDn": "cn=users,cn=accounts,dc=example,dc=com",
        "userObjectClasses": "inetOrgPerson, organizationalPerson",
        "usernameLDAPAttribute": "uid",
        "bindDn": "uid=keycloak,cn=sysaccounts,cn=etc,dc=example,dc=com",
        "bindCredential": "<service_account_password>",
        "rdnLDAPAttribute": "uid",
        "vendor": "rhds",
        "editMode": "READ_ONLY",
        "uuidLDAPAttribute": "ipaUniqueID",
        "connectionUrl": "ldap://freeipa.example.com:389",
        "syncRegistrations": "false",
        "authType": "simple"
      }
    }
  ]

Last, but not least, is the huge mess of data mappers which take various LDAP attributes and map them to their Keycloak equivalent. Most are fairly mundane, things like name and email address but the one shown below deserves a quick pause. This last mapping is how we translate the user groups we have in FreeIPA into roles for Keycloak. There are ways to map the actual roles that FreeIPA has over but I wasn’t as successful with them as I was just mapping groups.

  "userFederationMappers": [
    {
      "name": "groups",
      "federationMapperType": "role-ldap-mapper",
      "federationProviderDisplayName": "ldap-freeipa",
      "config": {
        "roles.dn": "cn=groups,cn=accounts,dc=example,dc=com",
        "membership.ldap.attribute": "member",
        "role.name.ldap.attribute": "cn",
        "role.object.classes": "groupOfNames",
        "mode": "LDAP_ONLY",
        "use.realm.roles.mapping": "true"
      }
    }
  ]

Creating the Secret

We are going to be mounting the realm JSON file as a volume secret in our Keycloak container so we need to create a secret out of the file and inject it into our namespace.

$ kubectl create secret generic freeipa-realm \
    --namespace keycloak \
    --from-file freeipa-realm.json=freeipa-realm.json

Deploying Keycloak With Helm

I am installing Keycloak with the Codecentric Helm chart so the next step is to write up the values file we will feed into Helm. Note: Again I do not think the dbVendor or existingSecret keys are actually required in this configuration but are retained to show what I actually did to get things working.

# keycloak-values.yml
global:
  storageClass: nfs-client
keycloak:
  replicas: 1
  username: admin
  existingSecret: keycloak-admin-password
  extraVolumes: |
    - name: freeipa-realm
      secret:
        secretName: freeipa-realm
  extraVolumeMounts: |
    - name: freeipa-realm
      mountPath: "/realm/"
      readOnly: true
  extraArgs: -Dkeycloak.import=/realm/freeipa-realm.json
  ingress:
    enabled: true
    hosts:
      - keycloak.rancher.example.com
    tls:
      - secretName: tls-keycloak-ingress
        hosts:
          - keycloak.rancher.example.com
  persistence:
    deployPostgres: true
    dbVendor: postgres
    existingSecret: keycloak-postgres-password
    existingSecretPasswordKey: password
postgres:
  persistence:
    enabled: true

The most interesting pieces here are the extraVolumes and extraVolumeMounts which allow us to load our FreeIPA JSON file into the container where it can be imported on startup. Beyond that, everything is pretty bog standard with TLS ingress handled by K8s and the bundled PostgreSQL chart handling data persistance. With the variables ready install is pretty straight forward.

$ helm install keycloak codecentric/keycloak \
    --namespace keycloak \
    --values keycloak-values.yml

Updating Jenkins Configuration

Now, at long last, we get to the whole point of this exercise: Jenkins authenticating with Keycloak. We’re going to open up the Jenkins Helm values we worked with last time and make some minor modifications to switch it over to using Keycloak. Under the JCasC configuration group we’re going to remove the LDAP configuration from configScripts and replace it with the following.

# jenkins-values.yml
keycloak-settings: |
  jenkins:
    securityRealm: keycloak
  keycloakSecurityRealm:
    keycloakJson: |-
      {
        "realm": "freeipa-realm",
        "auth-server-url": "https://keycloak.rancher.example.com/auth",
        "ssl-required": "all",
        "resource": "jenkins",
        "public-client": true,
        "confidential-port": 0
      }

This tells Jenkins to use Keycloak as it’s security realm and provides the Keycloak plugin with the OpenID JSON configuration it needs to connect with our new Keycloak instance. Before we update our Jenkins deploy we need to also make sure that keycloak:latest appears in our Jenkins configuration under the installPlugins key so that the plugin is installed and available when the Jenkins container starts up. Now it’s time to update our Jenkins install with Helm.

$ helm upgrade jenkins stable/jenkins \
    --namespace jenkins \
    --values jenkins-values.yml

Once the deploy is finished you should be able to access jenkins.rancher.example.com and be redirected to Keycloak to authenticate against FreeIPA. If all is well you should be sent back to the Jenkins landing page with all of your user information autofilled once you login successfully through Keycloak.