Updated 7 min read

Signing in to admin tools through Active Directory: LDAPS or OIDC

A CI system, a GitOps platform, monitoring and a wiki can all sign users in with a domain account. Direct LDAPS and OIDC through a broker such as Dex or Keycloak give the user the same result and security a completely different one.

Fig. 1. With direct LDAP the password reaches every application; with federation it reaches only the identity provider, which also enforces the second factor.

An administrator leaves the team. Their Active Directory account is disabled the same day and the matter seems closed. A month later a review shows that the local account in the CI system, in monitoring and in the GitOps platform’s console still works, because each of those tools had its own user list.

Connecting the tools to the directory solves this, but how they are connected matters more than the configuration form suggests, where both options take up one tab each. The difference lies in who sees the password, where a second factor can be enforced, and how many places need checking when access is revoked.

Option one: direct LDAPS

The tool takes the user’s login and password, connects to a domain controller and checks them with a bind operation. It reads group membership with an LDAP query and assigns roles accordingly. The advantages are simple configuration, no extra components and support in almost every tool.

The disadvantages follow from the user’s password reaching the tool:

  • every tool sees the passwords of everyone who signs in, so compromising one of them means leaking domain passwords,
  • multi-factor authentication cannot be enforced, because an LDAP bind accepts only a password,
  • every tool needs an account for group queries, and when its password expires, sign-in stops for everyone,
  • session, lockout and logout policy differs in every tool.

Option two: OIDC through a broker

The tool redirects the user to an identity broker via OpenID Connect. The broker, connected to Active Directory, checks the password and returns a signed token with the user name and group list to the tool. The tool never sees the password.

Two open-source options are most common. Dex is a lightweight broker with no user database of its own: it translates LDAP to OIDC and stores no accounts. Keycloak is a full identity provider with user federation from LDAP, its own session policy and a second factor (one-time codes, WebAuthn keys). A third route is a cloud identity provider with accounts synchronised from the directory.

PropertyDirect LDAPSOIDC through DexOIDC through Keycloak or Entra ID
Who sees the passwordevery toolDex onlythe identity provider only
Second factornono, the LDAP connector checks only the passwordyes, enforced in one place
Directory bind accountsone per tooloneone, or none with synchronisation
Extra componentsnoneone service without a databasea server with a database, or a cloud service
When to chooseone or two toolsseveral tools, a Kubernetes platformMFA required, many tools

For one or two tools in a small team, direct LDAPS is an acceptable start, provided the certificate is verified and the bind account is dedicated. With several tools a broker starts to pay off, because passwords reach one place instead of every tool. When MFA is required, you need a provider that supports it. Moving from LDAPS to OIDC requires no changes in the directory: the groups and naming convention stay the same, only the place where the password is checked changes.

The LDAP connector in Dex

The configuration below connects Dex to the directory over LDAPS, admits only enabled user accounts and passes only groups prefixed with GG- to the tools, so the token does not carry hundreds of irrelevant groups.

connectors:
  - type: ldap
    id: ad
    name: Active Directory
    config:
      # The domain name instead of a single controller address
      host: corp.example.com:636
      # The internal certificate authority's certificate
      rootCA: /etc/dex/tls/corp-root-ca.pem
      # Bind account, password from an environment variable
      bindDN: CN=svc-dex-bind,OU=Service Accounts,DC=corp,DC=example,DC=com
      bindPW: $LDAP_BIND_PASSWORD
      usernamePrompt: Domain login
      userSearch:
        baseDN: OU=Users,DC=corp,DC=example,DC=com
        # Enabled user accounts only
        filter: "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
        username: sAMAccountName
        idAttr: DN
        emailAttr: userPrincipalName
        nameAttr: displayName
        preferredUsernameAttr: sAMAccountName
      groupSearch:
        baseDN: OU=Groups,DC=corp,DC=example,DC=com
        filter: "(&(objectClass=group)(cn=GG-*))"
        userMatchers:
          - userAttr: DN
            groupAttr: member
        nameAttr: cn

Some platforms, Argo CD for example, ship Dex built in. The connector configuration is then the same; it just goes into their settings file, and the bind account password into their secret.

Permissions from groups

Whichever option you choose, permissions come from directory group membership, not from manual assignments in the tool. A simple convention works: a separate group per role per tool, with a name that says what it grants, such as GG-GitOps-Admins, GG-GitOps-ReadOnly, GG-Grafana-Editors. Group membership is the only way to obtain a role, and an access review comes down to reviewing groups.

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  # No policy.default: anyone outside these groups has no access
  scopes: '[groups]'
  policy.csv: |
    g, GG-GitOps-Admins, role:admin
    g, GG-GitOps-ReadOnly, role:readonly

The bind account

The broker needs an account to look up users and groups. An ordinary domain account is enough, because authenticated users can read the attributes needed for sign-in by default. The account belongs to no group other than Domain Users, is denied local and Remote Desktop logon, and its description names the owner and the date of the next password change.

The password has at least 30 random characters and is changed on a plan, for example once a year, together with the secret in the broker. An account whose password expires on its own at an unexpected moment stops sign-in to every tool at once.

Deployment pitfalls

  • The controller certificate. LDAPS works only when the domain controller has a server authentication certificate. The name used in the configuration must appear in the certificate’s SAN field, or verification fails.
  • The controller address. The configuration points at the domain name or several controllers, never at the IP address of one. That controller’s failure must not stop sign-in.
  • Delay in revoking access. A token issued by the broker is valid until it expires. In Dex the token lifetime is set by expiry.idTokens (24 hours by default). For tools with access to production, an hour is a sensible value.
  • The local account. The tool’s local administrator stays, disabled for daily work and secured as emergency access in case the directory or the broker is unavailable.

Check your own environment

Port 636 availability on every domain controller. A good result is True for every controller; False means the controller has no certificate or the port is blocked.

foreach ($dc in Get-ADDomainController -Filter *) {
    [pscustomobject]@{
        DC    = $dc.HostName
        LDAPS = Test-NetConnection -ComputerName $dc.HostName -Port 636 -InformationLevel Quiet
    }
}

The certificate as the tool sees it. The subjectAltName field must contain the name from the configuration, the issuer should be the internal certificate authority, and the notAfter date should be at least a month away. The second command checks verification against the authority’s certificate; a good result is Verify return code: 0 (ok).

openssl s_client -connect corp.example.com:636 -servername corp.example.com </dev/null 2>/dev/null |
  openssl x509 -noout -subject -issuer -enddate -ext subjectAltName

openssl s_client -connect corp.example.com:636 -CAfile corp-root-ca.pem </dev/null 2>/dev/null |
  grep 'Verify return code'

A query with the bind account over LDAPS, including nested groups. The result should list every member of the group. A Can't contact LDAP server error while the port is open usually means the certificate is not trusted.

LDAPTLS_CACERT=corp-root-ca.pem ldapsearch -x -H ldaps://corp.example.com:636 \
  -D 'svc-dex-bind@corp.example.com' -W \
  -b 'OU=Users,DC=corp,DC=example,DC=com' \
  '(memberOf:1.2.840.113556.1.4.1941:=CN=GG-GitOps-Admins,OU=Groups,DC=corp,DC=example,DC=com)' \
  sAMAccountName

The state of the bind account. A good result is an empty MemberOf (Domain Users is the primary group and does not appear here) and a PasswordLastSet within the planned rotation period.

Get-ADUser -Identity svc-dex-bind -Properties PasswordLastSet, PasswordNeverExpires, MemberOf |
    Select-Object SamAccountName, Enabled, PasswordLastSet, PasswordNeverExpires, MemberOf