Jenkins Authentication With Keycloak
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.