LDAP
The LDAP feature was introduced with Gloo Gateway Enterprise, release 0.18.27. If you are using an earlier version, this tutorial will not work.
The Lightweight Directory Access Protocol, commonly referred to as LDAP, is an open protocol used to store and retrieve hierarchically structured data over a network. It has been widely adopted by enterprises to centrally store and secure organizational information. A common use case for LDAP is to maintain information about members of an organization, assign them to specific user groups, and give each of them access to resources based on their group memberships.
In this guide you deploy a simple LDAP server to your Kubernetes cluster to explore how you can use it together with Gloo Gateway to authenticate users and control access to a target service based on the user’s group memberships.
Check out this excellent tutorial by Digital Ocean to familiarize yourself with the basic concepts and components of an LDAP server; although it is not strictly necessary, it will help you better understand this guide.
Prerequisites
Before you begin, this guide assumes that you have the following setup.
- Install Gloo Gateway in the
gloo-systemnamespace. - Enable discovery mode for Gloo Gateway. If not, make sure that you created any Upstream resources with the appropriate functions.
- Install the
glooctlcommand line tool. - Identify the URL of the gateway proxy that you want to use for this guide, such as with the
glooctl proxycommand. Note that if you are testing in a local cluster such as Kind, you must use the custom localhost port that you configured instead ofglooctl proxy, such ashttp://localhost:31500.
Step 1: Create a simple Virtual Service
Let’s start by creating a simple service that returns “Hello World” when receiving HTTP requests:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: http-echo
name: http-echo
spec:
selector:
matchLabels:
app: http-echo
replicas: 1
template:
metadata:
labels:
app: http-echo
spec:
containers:
- image: hashicorp/http-echo:latest
name: http-echo
args: ["-text='Hello World!'"]
ports:
- containerPort: 5678
name: http
---
apiVersion: v1
kind: Service
metadata:
name: http-echo
labels:
service: http-echo
spec:
ports:
- port: 5678
protocol: TCP
selector:
app: http-echo
EOF
Now we can create a Virtual Service that routes any requests with the /echo prefix to the http-echo service.
kubectl apply -f - << EOF
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: echo
namespace: gloo-system
spec:
displayName: echo
virtualHost:
domains:
- '*'
routes:
- matchers:
- prefix: /echo
routeAction:
single:
kube:
ref:
name: http-echo
namespace: default
port: 5678
EOFTo verify that the Virtual Service works, let’s send a request to /echo:
curl $(glooctl proxy url)/echo
returns
'Hello World!'
Step 2: Deploy an LDAP server
We also need to deploy an LDAP server to your cluster and configure it with a simple set of users and groups. This information is used to determine which requests can access the upstream that you created earlier.
We have prepared a shell script that takes care of setting up the necessary resources. It creates:
- a
configmapwith the LDAP server bootstrap configuration - a
deploymentrunning OpenLDAP - a
servicefronting the deployment
The script accepts an optional string argument, which determines the namespace in which the resources are created. If no namespace is provided, the resources are created in the default namespace. After you have downloaded the script to your working directory, you can run the following
commands to execute it:
chmod +x setup-ldap.sh
./setup-ldap.sh
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
To understand the user configuration, it is worth looking at the last two data entries in the config map:
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 Schmidt
sn: Schmidt
uid: marco
userPassword: marcopwd
mail: marco.schmidt@solo.io
# Add 'rick'
dn: uid=rick,ou=people,dc=solo,dc=io
objectClass: inetOrgPerson
cn: Rick Ducott
sn: Ducott
uid: rick
userPassword: rickpwd
mail: rick.ducott@solo.io
# Add 'scottc'
dn: uid=scottc,ou=people,dc=solo,dc=io
objectClass: inetOrgPerson
cn: Scott Cranton
sn: Cranton
uid: scottc
userPassword: scottcpwd
mail: scott.cranton@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
We can see that the root of the LDAP directory hierarchy is the dc=solo,dc=io entry, which has two child entries:
-
ou=groups,dc=solo,dc=iois the parent entry for user groups in the organization. It contains 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
- cn=
-
ou=people,dc=solo,dc=iois the parent entry for people in the organization and in turn 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
- uid=
The user credentials and memberships are summarized in the following table:
| username | password | member of developers | member of sales | member of managers |
|---|---|---|---|---|
| marco | marcopwd | Y | N | N |
| rick | rickpwd | Y | N | Y |
| scott | scottpwd | Y | Y | N |
To test that the LDAP server has been correctly deployed, let’s port-forward the corresponding deployment:
kubectl port-forward deployment/ldap 8088:389
In a different terminal instance, run the following command (you must have ldapsearch installed):
ldapsearch -H ldap://localhost:8088 -D "cn=admin,dc=solo,dc=io" -w "solopwd" -b "dc=solo,dc=io" -LLL dn
You should see the following output, listing the distinguished names (DNs) of all entries located in the subtree
rooted at dc=solo,dc=io:
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
Step 3: Set up LDAP authentication for the Virtual Service
The auth configuration format shown on this page was introduced with Gloo Enterprise, release 0.20.1. If you are using an earlier version, please refer to this page to see which configuration formats are supported by each version.
Now that we have all the necessary components in place, let’s use the LDAP server to secure the Virtual Service we created earlier.
LDAP auth flow
Before updating our Virtual Service, it is important to understand how Gloo Gateway interacts with the LDAP server. Let’s first look at the LDAP auth configuration :
address: The address of the LDAP server that Gloo Gateway will query when a request matches the Virtual Service.userDnTemplate: A template string that Gloo Gateway uses to build the DNs of the user entry or service account that needs to be authenticated and authorized. It must contain a single occurrence of the ā%sā placeholder.membershipAttributeName: The case-insensitive name of the attribute that contains the names of the groups an entry is a member of. Defaults tomemberOfif not provided.allowedGroups: The DNs of the user groups that are allowed to access the secured upstream.searchFilter: The filter to use when searching for the user entry that you want to authorize.disableGroupChecking: If set to true, disables validation for the membership attribute of the user entry.groupLookupSettings: Configures a service account to look up group memberships from the LDAP server. The service account must be set up in the LDAP server.
To better understand how this configuration is used, let’s go over the steps that Gloo Gateway performs when it detects a request that needs to be authenticated with LDAP:
- Look for a Basic Authentication header on the request and extract the username and credentials.
- If the header is not present, return a
401response. - Try to perform a BIND operation with the LDAP server. Gloo Gateway supports the following LDAP binding options:
- User binding: Gloo Gateway extracts the username from the basic auth header, and substitutes the name with the
%splaceholder in theuserDnTemplateto build the DN for theBINDoperation. Note that special characters are removed from the username before performing theBINDoperation to prevent injection attacks. Instead of user binding, you can use an LDAP service account to retrieve group membership information on behalf of the user. - 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. Note that you can only verify the user’s group membership in the LDAP server with the service account.
- User binding: Gloo Gateway extracts the username from the basic auth header, and substitutes the name with the
- If the
BINDoperation fails when using user binding, the user is either unknown or their credentials are incorrect, and a401response code is returned. If theBINDoperations fails for the service account, a500response code is returned. - If the
BINDoperation is successful, issue a search operation using thesearchFilterfilter for the user entry (with abasescope) and look for an attribute with a name equal tomembershipAttributeNameon the user entry. - Check if one of the values for the attribute matches one of the
allowedGroups; if so, allow the request, otherwise return a403response.
Create an LDAP AuthConfig
The steps to create an LDAP AuthConfig vary depending on which LDAP binding option you choose.
-
Create the LDAP AuthConfig.
kubectl apply -f - <<EOF apiVersion: enterprise.gloo.solo.io/v1 kind: AuthConfig metadata: name: ldap namespace: gloo-system spec: configs: - ldap: address: "ldap://ldap.default.svc.cluster.local:389" # Substitute your namespace for `default` here userDnTemplate: "uid=%s,ou=people,dc=solo,dc=io" allowedGroups: - "cn=managers,ou=groups,dc=solo,dc=io" searchFilter: "(objectClass=*)" EOFIn this AuthConfig you can find the following settings:
- The configuration points to the Kubernetes DNS name and port of the LDAP service
ldap.default.svc.cluster.local:389that you deployed earlier. - Gloo Gateway looks for user entries with DNs in the format
uid=<USERNAME_FROM_HEADER>,ou=people,dc=solo,dc=io. This is the format of the user entry DNs the LDAP server was bootstrapped with. - Only members of the
cn=managers,ou=groups,dc=solo,dc=iogroup can access the upstream.
For simplicity reasons, the following example uses the
adminaccount as the service account. This setup is NOT a recommended security practice. If you plan to use this setup in production, make sure to set up a service account in your LDAP server that has the required permissions to look up group membership information on behalf of a user. Note that you can verify only the user’s group membership in the LDAP server when using service account binding. For all other LDAP queries, user binding is used by default.-
Create a secret to store the credentials of the service account.
glooctl create secret authcredentials --name ldapcredentials --username cn=admin,dc=solo,dc=io --password solopwd -
Create the Gloo Gateway AuthConfig and enable group membership checking for the service account by setting the
checksGroupsWithServiceAccountoption to true. In addition, you must reference the secret that stores the credentials of the service account in thecredentialsSecretReffield.kubectl apply -f - <<EOF apiVersion: enterprise.gloo.solo.io/v1 kind: AuthConfig metadata: name: ldap namespace: gloo-system spec: configs: - ldap: address: "ldap://ldap.default.svc.cluster.local:389" # Substitute the default namespace if the ldap server was deployed to a different namespace userDnTemplate: "uid=%s,ou=people,dc=solo,dc=io" allowedGroups: - "cn=managers,ou=groups,dc=solo,dc=io" searchFilter: "(objectClass=*)" groupLookupSettings: checkGroupsWithServiceAccount: true credentialsSecretRef: name: ldapcredentials namespace: gloo-system EOF
- The configuration points to the Kubernetes DNS name and port of the LDAP service
-
Edit the Virtual Service and reference the LDAP AuthConfig that you created. This setup configures the Virtual Service to use the
ldapAuthConfig in thegloo-systemnamespace when authenticating requests to/echo.kubectl apply -f - << EOF apiVersion: gateway.solo.io/v1 kind: VirtualService metadata: name: echo namespace: gloo-system spec: displayName: echo virtualHost: domains: - '*' routes: - matchers: - prefix: /echo routeAction: single: kube: ref: name: http-echo namespace: default port: 5678 options: extauth: configRef: name: ldap namespace: gloo-system EOF
Step 4: Verify LDAP auth for your Virtual Service
-
Verify that the Virtual Service behaves as expected. Because the Virtual Service is now enabled for LDAP auth, you must provide the user that you want to authorize in the basic auth header of your request. Note that all credentials in this header must be base64-encoded. You can use the values from the following table to build your basic auth header.
username password basic auth header comments marco marcopwd Authorization: Basic bWFyY286bWFyY29wd2Q= Member of “developers” group rick rickpwd Authorization: Basic cmljazpyaWNrcHdk Member of “developers” and “managers” group john doe Authorization: Basic am9objpkb2U= Unknown user -
Send a request to
/echowithout any request headers and verify that you get back a401response code.curl -v "$(glooctl proxy url)"/echoExample output:
* Trying 192.168.99.100... * TCP_NODELAY set * Connected to 192.168.99.100 (192.168.99.100) port 31940 (#0) > GET /echo HTTP/1.1 > Host: 192.168.99.100:31940 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 401 Unauthorized < date: Tue, 10 Sep 2019 17:14:39 GMT < server: envoy < content-length: 0 < * Connection #0 to host 192.168.99.100 left intact -
Send another request to the
/echoendpoint. This time, you use an unknown user in the basic auth header. Verify that you get back a401response code.curl -v -H "Authorization: Basic am9objpkb2U=" "$(glooctl proxy url)"/echoExample output:
* Trying 192.168.99.100... * TCP_NODELAY set * Connected to 192.168.99.100 (192.168.99.100) port 31940 (#0) > GET /echo HTTP/1.1 > Host: 192.168.99.100:31940 > User-Agent: curl/7.54.0 > Accept: */* > Authorization: Basic am9objpkb2U= > < HTTP/1.1 401 Unauthorized < date: Tue, 10 Sep 2019 17:25:21 GMT < server: envoy < content-length: 0 < * Connection #0 to host 192.168.99.100 left intact -
Send another request and try to authenticate a user that belongs to the
developersgroup. Because your AuthConfig allows only members of themanagergroup to access the endpoint, you get back a403response code.curl -v -H "Authorization: Basic bWFyY286bWFyY29wd2Q=" "$(glooctl proxy url)"/echoExample output:
* Trying 192.168.99.100... * TCP_NODELAY set * Connected to 192.168.99.100 (192.168.99.100) port 31940 (#0) > GET /echo HTTP/1.1 > Host: 192.168.99.100:31940 > User-Agent: curl/7.54.0 > Accept: */* > Authorization: Basic bWFyY286bWFyY29wd2Q= > < HTTP/1.1 403 Forbidden < date: Tue, 10 Sep 2019 17:29:12 GMT < server: envoy < content-length: 0 < * Connection #0 to host 192.168.99.100 left intact -
Send another request and try to authenticate a user that belongs to the
managersgroup. Verify that your request now succeeds.curl -v -H "Authorization: Basic cmljazpyaWNrcHdk" "$(glooctl proxy url)"/echoExample output:
* Trying 192.168.99.100... * TCP_NODELAY set * Connected to 192.168.99.100 (192.168.99.100) port 31940 (#0) > GET /echo HTTP/1.1 > Host: 192.168.99.100:31940 > User-Agent: curl/7.54.0 > Accept: */* > Authorization: Basic cmljazpyaWNrcHdk > < HTTP/1.1 200 OK < x-app-name: http-echo < x-app-version: 0.2.3 < date: Tue, 10 Sep 2019 17:30:12 GMT < content-length: 15 < content-type: text/plain; charset=utf-8 < x-envoy-upstream-service-time: 0 < server: envoy < 'Hello World!' * Connection #0 to host 192.168.99.100 left intact
If you use service account binding and get back a 500 response code, make sure that the credentials in your Kubernetes secret are correct.
Summary
In this tutorial, you learned how Gloo Gateway can integrate with LDAP to authenticate incoming requests and authorize them based on the group memberships of the user that was provided in the request.
To clean up the resources we created, you can run the following commands:
glooctl uninstall
kubectl delete configmap ldap
kubectl delete deployment ldap http-echo
kubectl delete service ldap http-echo