About LDAP

The Lightweight Directory Access Protocol (LDAP) is an open protocol that you can use to store and retrieve hierarchically structured data over a network. Many enterprises use LDAP to centrally store and secure organizational information. In particular, LDAP is often used for membership directories. You might set up LDAP to store information such as the following:

  • User details like name and email
  • Group membership details for each user
  • Permissions for each group

You can deploy an LDAP server to your Kubernetes cluster. Then, use a Gloo Gateway external auth policy to authenticate users and control access based on their group membership details in the LDAP server.

LDAP in Gloo Gateway

Upon receiving an authentication request that uses LDAP, Gloo Gateway performs the following steps.

  1. Gloo Gateway looks for a Basic Authentication header on the request to extract the username (uid) and credentials (userPassword).

  2. If the header is not present, a 401 response is returned.

  3. If the header is present, Gloo Gateway tries to perform a BIND operation in one of the following ways:

    • User binding: Gloo Gateway extracts the username from the basic auth header. Then, it builds the distinguished name (DN) for the user entry by substituting the username from the header for the %s placeholder in the LDAP userDnTemplate setting.

    • Service account binding: Instead of giving each user access to the group membership information, you can use an LDAP service account to look up this information on behalf of the user. To authenticate with the LDAP server, you must store the LDAP service account credentials in a Kubernetes secret in your cluster. Then, you reference that secret in your AuthConfig resource. Note that you can only verify the user’s group membership in the LDAP server with the service account.

  4. If the BIND operation fails, a 401 response is returned. This response means that the user could not be found or the credentials are incorrect.

  5. Gloo Gateway issues a search operation for the user entry (with a base scope). In the user entry, Gloo Gateway looks for an attribute with a name equal to membershipAttributeName.

  6. Gloo Gateway checks if one of the values for the attribute matches one of the allowedGroups in the policy. If so, Gloo Gateway completes the request. Otherwise, Gloo Gateway returns a 403 response. This response means that although the user can be authenticated, the user does not have the appropriate permissions to complete the request.

Before you begin

  1. Follow the Get started guide to install Gloo Gateway, set up a gateway resource, and deploy the httpbin sample app.

  2. Get the external address of the gateway and save it in an environment variable.

Deploy an LDAP server

Before you can create an LDAP external auth policy, you must have an LDAP server. The following example configures a simple set of users and groups, and deploys the LDAP server to your cluster.

  1. Download the sample LDAP setup script.

  2. Review what the sample LDAP setup script does. For in-depth information, see About the LDAP setup script. In short, the script sets up LDAP users and groups, as well as creates the following Kubernetes resources:

    • A configmap with the LDAP server setup configuration.
    • A deployment to run OpenLDAP.
    • A service to provide access to the deployment.
  3. Make the downloaded script executable.

      chmod +x setup-ldap.sh
      
  4. Run the LDAP setup script. The script accepts an optional string argument to specify the namespace to create the resources in. Otherwise, you can omit the argument to create the resources in the default namespace.

      ./setup-ldap.sh
      

    Example output:

      No namespace provided, using default namespace
    Creating configmap with LDAP server bootstrap config...
    configmap/ldap created
    Creating LDAP service and deployment...
    deployment.apps/ldap created
    service/ldap created
      
  5. Enable port-forwarding on the deployment so that you can test the LDAP server.

      kubectl port-forward deployment/ldap 8088:389
      
  6. In a new tab in your terminal, search for the distinguished names (DNs) of all entries in the solo and io domain components (DCs). For more information about this command, see the LDAP docs.

      ldapsearch -H ldap://localhost:8088 -D "cn=admin,dc=solo,dc=io" -w "solopwd" -b "dc=solo,dc=io" -LLL dn
      

    Example output:

      dn: dc=solo,dc=io
    dn: cn=admin,dc=solo,dc=io
    dn: ou=people,dc=solo,dc=io
    dn: uid=marco,ou=people,dc=solo,dc=io
    dn: uid=rick,ou=people,dc=solo,dc=io
    dn: uid=scottc,ou=people,dc=solo,dc=io
    dn: ou=groups,dc=solo,dc=io
    dn: cn=developers,ou=groups,dc=solo,dc=io
    dn: cn=sales,ou=groups,dc=solo,dc=io
    dn: cn=managers,ou=groups,dc=solo,dc=io
      

Good job, now you have an LDAP server running! Continue to Set up LDAP auth.

About the LDAP setup script

The LDAP setup script sets up a basic LDAP server with a few different users and groups. This setup includes the Kubernetes resources to create in the cluster.

  #!/usr/bin/env bash

####################################################################################################
# This script is used to deploy an LDAP server with sample user/group configuration to Kubernetes #
####################################################################################################
set -e

if [ -z "$1" ]; then
  echo "No namespace provided, using default namespace"
  NAMESPACE='default'
else
  NAMESPACE=$1
fi

echo "Creating configmap with LDAP server bootstrap config..."
kubectl apply -n "${NAMESPACE}" -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: ldap
data:
  01_overlay.ldif: |-
    ######################################################################
    # Create a 'memberof' overlay for 'groupOfNames' entries.
    #
    # This will cause the 'memberOf' attribute to be automatically added
    # to user entries when they are referenced in a group entry.
    ######################################################################
    dn: olcOverlay={1}memberof,olcDatabase={1}mdb,cn=config
    objectClass: olcOverlayConfig
    objectClass: olcMemberOf
    olcOverlay: {1}memberof
    olcMemberOfDangling: ignore
    olcMemberOfRefInt: TRUE
    olcMemberOfGroupOC: groupOfNames
    olcMemberOfMemberAD: member
    olcMemberOfMemberOfAD: memberOf
  02_acl.ldif: |-
    dn: olcDatabase={1}mdb,cn=config
    changeType: modify
    ######################################################################
    # Delete default ACLs that come with Docker image
    ######################################################################
    delete: olcAccess
    olcAccess: to attrs=userPassword,shadowLastChange by self write by dn="cn=admin,dc=solo,dc=io" write by anonymous auth by * none
    -
    delete: olcAccess
    olcAccess: to * by self read by dn="cn=admin,dc=solo,dc=io" write by * none
    -
    ######################################################################
    # Control access to People
    ######################################################################
    add: olcAccess
    olcAccess: to dn.subtree="ou=people,dc=solo,dc=io"
        by dn="cn=admin,dc=solo,dc=io" write
        by group.exact="cn=managers,ou=groups,dc=solo,dc=io" write
        by group.exact="cn=developers,ou=groups,dc=solo,dc=io" read
        by group.exact="cn=sales,ou=groups,dc=solo,dc=io" read
        by anonymous auth
    -
    ######################################################################
    # Control access to Groups
    ######################################################################
    add: olcAccess
    olcAccess: to dn.subtree="ou=groups,dc=solo,dc=io"
        by dn="cn=admin,dc=solo,dc=io" write
        by group.exact="cn=managers,ou=groups,dc=solo,dc=io" write
        by group.exact="cn=developers,ou=groups,dc=solo,dc=io" write
    -
    ######################################################################
    # This policy applies to the 'userPassword' attribute only
    # 'self write' grants only the owner of the entry write permission to this attribute
    # 'anonymous auth' grants an anonymous user access to this attribute only for authentication purposes (required for BIND)
    # 'developers' group members can update any user's password
    ######################################################################
    add: olcAccess
    olcAccess: to attrs=userPassword
      by self write
      by anonymous auth
      by dn="cn=admin,dc=solo,dc=io"
      by group.exact="cn=developers,ou=groups,dc=solo,dc=io" write
      by * none
    -
    ######################################################################
    # This policy applies to all entries under the "dc=solo,dc=io" subtree
    # 'managers' have read access at all the organization's information
    ######################################################################
    add: olcAccess
    olcAccess: to dn.subtree="dc=solo,dc=io"
      by self write
      by dn="cn=admin,dc=solo,dc=io" write
      by group.exact="cn=managers,ou=groups,dc=solo,dc=io" read
      by * none
  03_people.ldif: |
    # Create a parent 'people' entry
    dn: ou=people,dc=solo,dc=io
    objectClass: organizationalUnit
    ou: people
    description: All solo.io people

    # Add 'marco'
    dn: uid=marco,ou=people,dc=solo,dc=io
    objectClass: inetOrgPerson
    cn: Marco Schwarz
    sn: Schwarz
    uid: marco
    userPassword: marcopwd
    mail: marco.schwarz@solo.io

    # Add 'rick'
    dn: uid=rick,ou=people,dc=solo,dc=io
    objectClass: inetOrgPerson
    cn: Rick Duke
    sn: Duke
    uid: rick
    userPassword: rickpwd
    mail: rick.duke@solo.io

    # Add 'scottc'
    dn: uid=scottc,ou=people,dc=solo,dc=io
    objectClass: inetOrgPerson
    cn: Scott Crawley
    sn: Crawley
    uid: scottc
    userPassword: scottcpwd
    mail: scott.crawley@solo.io
  04_groups.ldif: |+
    # Create top level 'group' entry
    dn: ou=groups,dc=solo,dc=io
    objectClass: organizationalUnit
    ou: groups
    description: Generic parent entry for groups

    # Create the 'developers' entry under 'groups'
    dn: cn=developers,ou=groups,dc=solo,dc=io
    objectClass: groupOfNames
    cn: developers
    description: Developers group
    member: uid=marco,ou=people,dc=solo,dc=io
    member: uid=rick,ou=people,dc=solo,dc=io
    member: uid=scottc,ou=people,dc=solo,dc=io

    # Create the 'sales' entry under 'groups'
    dn: cn=sales,ou=groups,dc=solo,dc=io
    objectClass: groupOfNames
    cn: sales
    description: Sales group
    member: uid=scottc,ou=people,dc=solo,dc=io

    # Create the 'managers' entry under 'groups'
    dn: cn=managers,ou=groups,dc=solo,dc=io
    objectClass: groupOfNames
    cn: managers
    description: Managers group
    member: uid=rick,ou=people,dc=solo,dc=io
EOF

echo "Creating LDAP service and deployment..."
kubectl apply -n "${NAMESPACE}" -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: ldap
  name: ldap
spec:
  selector:
    matchLabels:
      app: ldap
  replicas: 1
  template:
    metadata:
      labels:
        app: ldap
    spec:
      volumes:
      - name: config
        emptyDir: {}
      - name: configmap
        configMap:
          name: ldap
      # We need this intermediary step because when Kubernetes mounts a configMap to a directory,
      # it generates additional files that the LDAP server tries to load, causing it to fail.
      initContainers:
      - name: copy-config
        image: busybox
        command: ['sh', '-c', 'cp /configmap/*.ldif /config']
        volumeMounts:
        - name: configmap
          mountPath: /configmap
          # This is the volume that will be mounted to the LDAP container
        - name: config
          mountPath: /config
      containers:
      - image: osixia/openldap:1.2.5
        name: openldap
        args: ["--copy-service", "--loglevel", "debug"]
        env:
        - name: LDAP_ORGANISATION
          value: "Solo.io"
        - name: LDAP_DOMAIN
          value: "solo.io"
        - name: LDAP_ADMIN_PASSWORD
          value: "solopwd"
        ports:
        - containerPort: 389
          name: ldap
        - containerPort: 636
          name: ldaps
        volumeMounts:
        - mountPath: /container/service/slapd/assets/config/bootstrap/ldif/custom
          name: config
---
apiVersion: v1
kind: Service
metadata:
  name: ldap
  labels:
    app: ldap
spec:
  ports:
  - port: 389
    protocol: TCP
  selector:
    app: ldap
EOF
  

About the LDAP users and groups

The root of the LDAP directory hierarchy is the dc=solo,dc=io entry, which has two child entries for users and groups.

Summary of LDAP credentials

The user credentials and memberships are summarized in the following table.

UsernamePasswordMember of developersMember of salesMember of managers
marcomarcopwd
rickrickpwd
scottscottpwd

Set up LDAP auth

  1. Create an AuthConfig resource and add your external authentication rules.

  2. Create a RouteOption resource and reference the AuthConfig resource that you just created.

      kubectl apply -f- <<EOF
    apiVersion: gateway.solo.io/v1
    kind: RouteOption
    metadata:
      name: ldap-auth
      namespace: httpbin
    spec:
      options:
        extauth:
          configRef:
            name: ldap-auth
            namespace: httpbin
    EOF
      
  3. Create an HTTPRoute resource for the httpbin app that requires authentication for requests on the extauth.example domain.

      kubectl apply -f- <<EOF
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: httpbin-ldap-auth
      namespace: httpbin
    spec:
      parentRefs:
      - name: http
        namespace: gloo-system
      hostnames:
        - extauth.example
      rules:
        - filters:
            - type: ExtensionRef
              extensionRef:
                group: gateway.solo.io
                kind: RouteOption
                name: ldap-auth
          backendRefs:
            - name: httpbin
              port: 8000
    EOF
      

Test LDAP auth

To test the LDAP policy, make a series of requests as different users. The following table is based off the users that you created in your LDAP config map. The username and password are encoded to base 64 in the format username:password so that you can pass them in a basic auth header.

UsernamePasswordBasic auth headerNotes
marcomarcopwdAuthorization: Basic bWFyY286bWFyY29wd2Q=Member of developers group
rickrickpwdAuthorization: Basic cmljazpyaWNrcHdkMember of developers and managers groups
johndoeAuthorization: Basic am9objpkb2U=Unknown user, not a member of any group
  1. Send a request to the httpbin app on the extauth.example domain without a user. Verify that your request is denied and that you get back a 401 HTTP response code.

    Example output:

      * Mark bundle as not supporting multiuse
    < HTTP/1.1 401 Unauthorized
    < www-authenticate: Basic realm="gloo"
    < date: Fri, 19 Apr 2024 17:41:01 GMT
    < server: envoy
    < content-length: 0
      
  2. Send another request to the httpbin app. This time, you include the credentials of the unknown member, john. Verify that the request is blocked and that you get back a 401 HTTP response code.

    Example output:

      * Mark bundle as not supporting multiuse
    < HTTP/1.1 401 Unauthorized
    < www-authenticate: Basic realm="gloo"
    < date: Fri, 19 Apr 2024 17:41:01 GMT
    < server: envoy
    < content-length: 0
      
  3. Send another request to the httpbin app. This time, you use the credentials of the known member, marco. This user is a member of the developers group, but the LDAP policy only grants permission to members of the managers group. The request is blocked, but this time with a 403 Forbidden response to indicate the lack of permissions.

    Example output:

      * Mark bundle as not supporting multiuse
    < HTTP/1.1 403 Forbidden
    < date: Tue, 04 Jun 2024 19:52:55 GMT
    < server: envoy
    < content-length: 0
      
  4. Finally, send another request with the credentials of the rick, who is a member of the managers group and meets the LDAP policy requirements. Verify that you get back a 200 HTTP response code.

    Example output:

      * Mark bundle as not supporting multiuse
    < HTTP/1.1 200 OK
    < access-control-allow-credentials: true
    < access-control-allow-origin: *
    < date: Tue, 04 Jun 2024 19:53:19 GMT
    < content-length: 0
    < x-envoy-upstream-service-time: 1
    < server: envoy
      

Cleanup

You can optionally remove the resources that you set up as part of this guide.
  kubectl delete authconfig ldap-auth -n httpbin
kubectl delete routeoption ldap-auth -n httpbin
kubectl delete httproute httpbin-ldap-auth -n httpbin
kubectl delete secret ldapcredentials -n httpbin
kubectl delete configmap ldap
kubectl delete deployment ldap
kubectl delete service ldap