Authentication with FreeIPA (LDAP) and OpenShift (OKD)

Every major platform, whether it is an app on a phone or an mission critical service running in-house or a SAAS service running in the cloud configured to use authentication from an external source. OpenShift is no exception - in my lab, I got it setup with FreeIPA (a LDAP service), which I would like to demonstrate in this post.

Previously. . .

When I setup my OpenShift cluster, I configure a basic form of authentication using them htpasswd provider. It involves:

  1. Creating a htpasswd file
  2. Storing the htpasswd in a secret.
  3. Referencing the secret in the HTPasswd provider.

It looks like this:

apiVersion: config.openshift.io/v1
kind: OAuth
metadata:
  name: cluster
spec:
  identityProviders:
  - name: htpasswd_provider
    challenge: true
    login: true
    mappingMethod: claim
    type: HTPasswd
    htpasswd:
      fileData:
        name: htpass-secret

This will work for simple small setups as well as break-glass access. For larger implementation, this will not scale, so I will configure OpenShift to authenticate with an existing FreeIPA service I have running in my environment.

Creating LDAP Users

Now, setting up FreeIPA goes beyond this walkthrough, so I will simply point to this site for instructions.

The important thing for our purposes is once you are done, create the following:

  • Your user (in my case, rilindofoster)
  • A user to be used for a service account:

Both users are part of the ipausers group. I then created a group call okdadmins and added my user to that group:

The okdadmins group will let me login as admin, but we are getting ahead of ourselves here.

Setup LDAP Provider

With the users setup, I am ready to create the provider. First, I created a secret ldap-bind-secret, which will contain the sa-bind-account password in openshift-config namespace (or project):

oc create secret generic ldap-bind-secret --from-literal=bindPassword=thisisabindpassword -n openshift-config

Next, I store the FreeIPA certificate authority in a config ma (the CA file is located on the FreeIPA server at /etc/ipa/ca.crt). I copied the file from the FreeIPA to my environment and store the ca.crt file as follows (again, in openshift-config namespace)

oc create cm freeipa-ca --from-file=ca.crt=ca.crt -n openshift-config

Now I am ready to add the provider. I edit oath:

oc edit oauth

And added the following code after my last provider:

 - ldap:
        attributes:
          email:
          - mail
          id:
          - uid
          name:
          - cn
          preferredUsername:
          - uid
        bindDN: uid=sa-bind-account,cn=users,cn=accounts,dc=infra,dc=example,dc=com
        bindPassword:
          name: ldap-bind-secret
        ca:
          name: freeipa-ca
        url: ldaps://ipa.infra.example.com/cn=users,cn=accounts,dc=infra,dc=example,dc=com?uid
      mappingMethod: claim
      name: freeipa-ldap
      type: LDAP

The documentation has a fairly reasonably documented example here, so all you would need to populate are:

  • bindDN - the path to your service account
  • bindPassword - the secret that contains your ldap bind password
  • ca - the config map that contains the ca certificate.
  • url - the query URL used to lookup the user.
  • name - what you would call your provider.

With my existing htpasswd provider setup prior to it, it looks like this:

apiVersion: config.openshift.io/v1
kind: OAuth
metadata:
  name: cluster
spec:
  identityProviders:
  - name: htpasswd_provider
    challenge: true
    login: true
    mappingMethod: claim
    type: HTPasswd
    htpasswd:
      fileData:
        name: htpass-secret
    - ldap:
        attributes:
          email:
          - mail
          id:
          - uid
          name:
          - cn
          preferredUsername:
          - uid
        bindDN: uid=sa-bind-account,cn=users,cn=accounts,dc=infra,dc=example,dc=com
        bindPassword:
          name: ldap-bind-secret
        ca:
          name: freeipa-ca
        url: ldaps://ipa.infra.example.com/cn=users,cn=accounts,dc=infra,dc=example,dc=com?uid
      mappingMethod: claim
      name: freeipa-ldap
      type: LDAP

I then saved it and wait a few minutes. You can watch the pods being relaunched here:

oc get pods -n openshift-authentication

If you don't see the pods replaced, that means there is a problem with the setup, likely either your secret or your ca. You may need to double that those are created properly on top of the attributes setup in your LDAP provider yaml.

Setup LDAP Group Sync

With the authentication setup, I should be able to authenticate now. But I am not done - we need to be able to assign users to groups and then assign the appropriate permissions to those groups. So I will replicate the groups from ldap.

First, I create a new project / namespace to run the sync:

 oc new-project auth-freeipa-sync

Next I create a service account user freeipa-group-syncer:

oc create sa freeipa-group-syncer

Then I create a cluster role freeipa-group-syncer with the permissions to create the group:

oc create clusterrole freeipa-group-syncer --verb get,list,create,update --resource groups

And then I assign that cluster group to the service account:

oc adm policy add-cluster-role-to-user freeipa-group-syncer -z freeipa-group-syncer

I then add secret freeipa-secret that contains sa-bind-account password:

oc create secret generic freeipa-secret --from-literal bindPassword='thisisabindpassword'

At this point, I create an LDAP Sync file:

vi freeipa-sync.yaml

(Side note: Funny how I keep defaulting to vi even though I have VSCode on my machine. Some habits are hard to break)

I create LDAPSyncConfig object that will be used to replicate the group.

kind: LDAPSyncConfig
apiVersion: v1
url: ldaps://ipa.infra.example.com:636
bindDN: 'uid=sa-bind-account,cn=users,cn=accounts,dc=infra,dc=example,dc=com'
bindPassword:
  file: /etc/secrets/bindPassword
ca: /etc/config/ca.crt
augmentedActiveDirectory:
    groupsQuery:
        baseDN: "cn=groups,cn=accounts,dc=infra,dc=example,dc=com"
        scope: sub
        derefAliases: never
    groupUIDAttribute: dn
    groupNameAttributes: [ cn ]
    groupMembershipAttributes: [ member ]
    usersQuery:
        baseDN: "cn=users,cn=accounts,dc=infra,dc=example,dc=com"
        scope: sub
        derefAliases: never
        filter: (objectclass=*)
    userNameAttributes: [ mail ]
    userUIDAttribute: dn
    tolerateMemberNotFoundErrors: true
    tolerateMemberOutOfScopeErrors: true

This took a few hours and some consultation with Google and ChatGPT to figure things out. In short, you need to use augementedActiveDirectory attribute and you need to add tolerateMemberNotFoundErrors and tolerateMemberOutofScopeErrors to get it working. For the last two, you should think carefully before adding those if you are working in a production environment.

With that done, I plugging the freeipa-sync.yaml as well as the certificate authority file in their respective config maps:

oc create cm freeipa-config --from-file freeipa-sync.yaml=freeipa-sync.yaml,ca.crt=ca.crt

Finally, I setup a CronJob to run the sync every minute:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: freeipa-group-sync
  namespace: auth-freeipa-sync
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: ldap-group-sync
              image: "quay.io/openshift-pipeline/openshift4-ose-cli"
              command:
                - "/bin/sh"
                - "-c"
                - "oc adm groups sync --sync-config=/etc/config/freeipa-sync.yaml --confirm" 
              volumeMounts:
                - mountPath: "/etc/config"
                  name: "ldap-sync-volume"
                - mountPath: "/etc/secrets"
                  name: "ldap-bind-password"
          volumes:
            - name: "ldap-sync-volume"
              configMap:
                name: "freeipa-config"
            - name: "ldap-bind-password"
              secret:
                secretName: "freeipa-secret"
          serviceAccountName: freeipa-group-syncer
          serviceAccount: freeipa-group-syncer

Very important: You would need to use the Openshift OSE CLI, which you can get from quay.io/openshift-pipeline/openshift4-ose-cli.

After a minute or so, I see the group replicated with the user when I run oc get groups:

➜  home_lab git:(master) ✗ oc get groups
NAME           USERS
admins         admin
audit3         
audit4         
auditone       
audittwo       
ipausers       sa-bind-account, rilindofoster
okd_admins     rilindo
okdadmins      rilindofoster
readonlyone    
trust admins   admin
➜  home_lab git:(master) ✗ 

Logging in

Now I am ready to get login. I went to the OpenShift console and I see a button freeipa-ldap to authenticate with LDAP:

I click on that button and enter my credentials:

And I am in:

Of course, I can also login via the CLI, but instead of passing my credentials, I just login with:

oc login --web

Which will let me with the browser. Once that is done, I get the following in the browser:

access token received successfully; please return to your terminal

It returns me to the terminal.

  home_lab git:(master)  oc login --web
Opening login URL in the default browser: https://oauth-openshift.apps.okd.example.com/oauth/authorize?client_id=openshift-cli-client&code_challenge=<REDACTED>&code_challenge_method=S256&redirect_uri=http%3A%2F%2F127.0.0.1%3A55082%2Fcallback&response_type=code
Login successful.

You have access to 96 projects, the list has been suppressed. You can list all projects with 'oc projects'

Using project "apache2".
  home_lab git:(master)  

I hope you find this useful.

Update: 2025-12-02

Seems that something changed when I upgrade to version 4.20.10, as I was no longer able to sync. After a bit of searching and test, this seems to be a working configuration:

kind: LDAPSyncConfig
apiVersion: v1
url: ldaps://ipa.infra.monzell.com:636
bindDN: 'uid=sa-bind-account,cn=users,cn=accounts,dc=infra,dc=monzell,dc=com'
bindPassword:
  file: /etc/secrets/bindPassword
ca: /etc/config/ca.crt
rfc2307:
    groupsQuery:
        baseDN: "cn=groups,cn=accounts,dc=infra,dc=monzell,dc=com"
        scope: sub
        derefAliases: never
        pageSize: 0
        filter: (objectclass=*)
    groupUIDAttribute: dn
    groupNameAttributes: [ cn ]
    groupMembershipAttributes: [ member ]
    usersQuery:
        baseDN: "cn=users,cn=accounts,dc=infra,dc=monzell,dc=com"
        scope: sub
        derefAliases: never
        pageSize: 0
    userNameAttributes: [ uid ]
    userUIDAttribute: dn
    tolerateMemberNotFoundErrors: false
    tolerateMemberOutOfScopeErrors: false