Skip to content

LDAP

Page as Markdown

Authenticate requests against membership information that is stored in a Lightweight Directory Access Protocol (LDAP) server.

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 Mesh (Gloo Platform APIs) external auth policy to authenticate users and control access based on their group membership details in the LDAP server.

LDAP in Gloo Mesh (Gloo Platform APIs)

Upon receiving an authentication request that uses LDAP, Gloo Mesh (Gloo Platform APIs) performs the following steps.

  1. Gloo Mesh (Gloo Platform APIs) 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 Mesh (Gloo Platform APIs) tries to perform a BIND operation in one of the following ways:

    • User binding: Gloo Mesh (Gloo Platform APIs) 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 Mesh (Gloo Platform APIs) issues a search operation for the user entry (with a base scope). In the user entry, Gloo Mesh (Gloo Platform APIs) looks for an attribute with a name equal to membershipAttributeName.

  6. Gloo Mesh (Gloo Platform APIs) checks if one of the values for the attribute matches one of the allowedGroups in the policy. If so, Gloo Mesh (Gloo Platform APIs) completes the request. Otherwise, Gloo Mesh (Gloo Platform APIs) 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. Complete the multicluster getting started guide to set up the following testing environment.

    • Three clusters along with environment variables for the clusters and their Kubernetes contexts.
    • The Gloo meshctl CLI, along with other CLI tools such as kubectl and istioctl.
    • The Gloo management server in the management cluster, and the Gloo agents in the workload clusters.
    • Istio installed in the workload clusters.
    • A simple Gloo workspace setup.
  2. Install Bookinfo and other sample apps.
  3. Make sure that the external auth service is installed and running. If not, install the external auth service.

    kubectl get pods –context $REMOTE_CONTEXT1 -A -l app=ext-auth-service
  4. Make sure that you have the ldapsearch CLI tool on your local machine. This tool is part of the default developer tools in macOS. You can check if you have the tool by running the following command to print the usage details. To install LDAP and Helper utilities, see this Digital Ocean guide.

    ldapsearch --help

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
    ```<ol start="4">
  • Set your Kubernetes context to the cluster that you want to deploy the LDAP server to and create the external auth policy in.
    kubectl config use-context ${REMOTE_CONTEXT1}
    1. 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
    2. Enable port-forwarding on the deployment so that you can test the LDAP server.

      kubectl port-forward deployment/ldap 8088:389
    3. 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 Configure an external auth policy with LDAP.

    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.

    ou=groups,dc=solo,dc=io is the parent entry for user groups in the organization. It has three groups:

    • cn=developers,ou=groups,dc=solo,dc=io
    • cn=sales,ou=groups,dc=solo,dc=io
    • cn=managers,ou=groups,dc=solo,dc=io
    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

    ou=people,dc=solo,dc=io is the parent entry for people in the organization. It has the following entries:

    • uid=marco,ou=people,dc=solo,dc=io
    • uid=rick,ou=people,dc=solo,dc=io
    • uid=scott,ou=people,dc=solo,dc=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

    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

    Configure an external auth policy with LDAP

    Create the external auth policy with LDAP.

    1. Create an external auth server to use for your policy. The following example refers directly to the default Gloo external auth service, but you can also use a virtual destination instead. For more information, see External auth server setup.

      kubectl &ndash;context $REMOTE_CONTEXT1 apply -f - <<EOF
      apiVersion: admin.gloo.solo.io/v2
      kind: ExtAuthServer
      metadata:
        name: ext-auth-server
        namespace: bookinfo
      spec:
        destinationServer:
          port:
            number: 8083
          ref:
            cluster: $REMOTE_CLUSTER1
            name: ext-auth-service
            namespace: gloo-mesh
      EOF
    2. Create an external auth policy that uses the LDAP server.

    kubectl --context ${REMOTE_CONTEXT1} apply -f - <<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: ExtAuthPolicy
    metadata:
      name: ratings-ldap
      namespace: bookinfo
    spec:
      applyToDestinations:
      - selector:
          labels:
            app: ratings
      config:
        server:
          name: ext-auth-server
          namespace: bookinfo
          cluster: $REMOTE_CLUSTER1
        glooAuth:
          configs:
          - ldap:
              address: "ldap://ldap.default.svc.cluster.local:389"
              userDnTemplate: "uid=%s,ou=people,dc=solo,dc=io"
              membershipAttributeName: memberOf
              allowedGroups:
              - "cn=managers,ou=groups,dc=solo,dc=io"
    EOF

    Review the following table to understand this configuration. For more information, see the API reference.

    SettingDescription
    applyToDestinationsUse labels to apply the policy to destinations. Destinations might be a Kubernetes service, VirtualDestination, or ExternalService (if supported by the policy). If you do not specify any destinations or routes, the policy applies to all destinations in the workspace by default. If you do not specify any destinations but you do specify a route, the policy applies to the route but to no destinations.
    serverThe external auth server to use for the policy.
    ldapConfigure the LDAP server details.
    addressThe address of the LDAP server that Gloo queries when a request matches the policy. This example uses the Kubernetes DNS name and port of the LDAP service that you deployed in the default namespace.
    userDnTemplateThe template string for Gloo to build the DNs of the user entry to authenticate and authorize. The string must have a single occurrence of the %s placeholder. This placeholder is used to substitute the value from the request header that you want to look for, such as the user ID (uid) in this example. In this example, the template matches the format of the user entry DNs in the config map.
    membershipAttributeNameA case-insensitive name of the attribute with the names of the groups that a user entry is a member of. If not set, the default value memberOf is used. In the example, the config map sets memberOf automatically for each user entry that is in a group.
    allowedGroupsThe DNs of the user groups that are allowed to access services that are protected by this policy. In this example, only members of "cn=managers,ou=groups,dc=solo,dc=io" group can get successful responses.

    Verify the external auth LDAP policy

    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 an unauthenticated request to the app. Now, the request is blocked with a 401 Unauthorized response.

      Create a temporary curl pod in the bookinfo namespace, so that you can test the app setup. You can also use this method in Kubernetes 1.23 or later, but an ephemeral container might be simpler.

    1. Create the curl pod.
      kubectl run -it -n bookinfo --context $REMOTE_CONTEXT1 curl \
        --image=curlimages/curl:7.73.0 --rm  -- sh
    2. Send a request to the reviews app.
      curl -v http://ratings:9080/ratings/1
    1. Repeat the previous step with the credentials of the unknown member, john. The request is blocked with the same 401 Unauthorized response.
    curl -v -H "Authorization: Basic am9objpkb2U=" http://ratings:9080/ratings/1
    1. Try a request with the credentials of a 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.
    curl -v -H "Authorization: Basic bWFyY286bWFyY29wd2Q=" http://ratings:9080/ratings/1
    1. Finally, send a request with the credentials of rick, who is a member of the managers group and meets the LDAP policy requirements.
      1. From the curl pod, send a request to the ratings app.
        curl -v -H "Authorization: Basic cmljazpyaWNrcHdk" http://ratings:9080/ratings/1
      2. Exit the temporary pod. The pod deletes itself.
        exit

      The request succeeds!

      HTTP/1.1 200 OK
      ...
      {"id":1,"ratings":{"Reviewer1":5,"Reviewer2":4}}

      Cleanup

      You can optionally remove the resources that you set up as part of this guide.

      1. Remove the LDAP server that you created.
        kubectl delete &ndash;context $REMOTE_CONTEXT1 configmap ldap
        kubectl delete &ndash;context $REMOTE_CONTEXT1 deployment ldap
        kubectl delete &ndash;context $REMOTE_CONTEXT1 service ldap
      2. Remove the Gloo resources that you created.
        kubectl &ndash;context $REMOTE_CONTEXT1 -n bookinfo delete ExtAuthServer ext-auth-server
        kubectl &ndash;context $REMOTE_CONTEXT1 -n bookinfo delete ExtAuthPolicy ratings-ldap
      API keysOPA