Integrating ClickHouse with LDAP (Part One)

LDAP Integration in ClickHouse

ClickHouse has traditionally used XML configuration files to define server configuration including all database users. The users.xml configuration file or separate configuration files inside the /etc/clickhouse-server/users.d directory define users and properties associated with them such as profiles, network restrictions, quotas, and passwords. This approach works well for small organizations and installations but makes it hard to administer ClickHouse clusters in large organizations and installations. There are three main problems with the user configuration files. First, administrators must edit these configuration files by hand each time a new user has to be added, modified, or removed. Second, it suffers from the security burden of having passwords or their hashes stored in the configuration files. Third, assigning and managing user roles is limited and cumbersome.

ClickHouse can now address all three problems. With the introduction of support for RBAC, ClickHouse allows managing users and roles using SQL statements. There is no need to change configuration files to manage users and user roles. However, even with RBAC, you still need to manage ClickHouse users and roles separately from the rest of your organization’s services, and passwords are still stored on the file system.

Altinity has been working since mid-2020 to add ClickHouse support for Lightweight Directory Access Protocol, also known as LDAP. LDAP centralizes storage of usernames and passwords as well as their roles within an organization, which makes it possible to manage users and privileges from a single location. We’re happy to report that ClickHouse LDAP authentication and user directory support is merged, stable, and ready for general use. We have tested it carefully with the popular OpenLDAP package, available in most Linux distributions.

In this series of blog articles, we will describe how the new support for LDAP integration can help medium and large organizations integrate the management of ClickHouse users and roles with their existing services. We will describe different levels of LDAP integration with ClickHouse, starting with authenticating existing ClickHouse users using LDAP to fully integrating LDAP using external user directory and mapping selected LDAP groups to RBAC roles.

Testing Environment

We will use a docker-compose cluster to show how LDAP can be integrated with ClickHouse. You can find the docker-compose environment at https://gitlab.com/altinity-public/blogs/ldap-integration-with-clickhouse that will use yandex/clickhouse-server:21.1.2.15 image for our ClickHouse containers.

To use this environment, you will need git, docker, and docker-compose installed on your system. Then you can clone the repository that contains the test environment to your local system.

git clone https://gitlab.com/altinity-public/blogs/ldap-integration-with-clickhouse.git

Check that you can bring up the docker-compose cluster.

cd  ldap-integration-with-clickhouse
cd docker-compose/
docker-compose up -d
Creating network "docker-compose_default" with the default driver
Creating docker-compose_openldap1_1 ... done
Creating docker-compose_zookeeper_1 ... done
Creating docker-compose_clickhouse1_1 ... done
Creating docker-compose_clickhouse3_1 ... done
Creating docker-compose_clickhouse2_1 ... done
Creating phpldapadmin                 ... done
Creating docker-compose_all_services_ready_1 ... done

If everything goes well, you should see the messages above that indicate that openldap1, zookeeper, clickhouse1, clickhouse2, and clickhouse3, as well as phpldapadmin services, are up and running. We can also check that all services are healthy.

Note that you must execute all docker-compose commands inside the docker-compose folder.

docker-compose ps
               Name                              Command                  State                   Ports            
-------------------------------------------------------------------------------------------------------------------
docker-compose_all_services_ready_1   /hello                           Exit 0                                      
docker-compose_clickhouse1_1          bash -c clickhouse server  ...   Up (healthy)   8123/tcp, 9000/tcp, 9009/tcp 
docker-compose_clickhouse2_1          bash -c clickhouse server  ...   Up (healthy)   8123/tcp, 9000/tcp, 9009/tcp 
docker-compose_clickhouse3_1          bash -c clickhouse server  ...   Up (healthy)   8123/tcp, 9000/tcp, 9009/tcp 
docker-compose_openldap1_1            /container/tool/run --copy ...   Up (healthy)   389/tcp, 636/tcp             
docker-compose_zookeeper_1            /docker-entrypoint.sh zkSe ...   Up (healthy)   2181/tcp, 2888/tcp, 3888/tcp 
phpldapadmin                          /container/tool/run              Up (healthy)   443/tcp, 0.0.0.0:8080->80/tcp

Sanity Checks

Let’s do some necessary sanity checks before we proceed. First, make sure our OpenLDAP server is up and running. We can perform a search operation to see what users we currently have defined on the LDAP server.

Again, note that you must execute all docker-compose commands inside the docker-compose folder.

docker-compose exec openldap1 bash -c 'ldapsearch -x -H ldap://localhost -b "ou=users,dc=company,dc=com" -D "cn=admin,dc=company,dc=com" -w admin'
# extended LDIF
#
# LDAPv3
# base <ou=users,dc=company,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# users, company.com
dn: ou=users,dc=company,dc=com
objectClass: organizationalUnit
objectClass: top
ou: users

# ldapuser, users, company.com
dn: cn=ldapuser,ou=users,dc=company,dc=com
cn: ldapuser
gidNumber: 501
givenName: John
homeDirectory: /home/users/ldapuser
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: top
sn: User
uid: ldapuser
uidNumber: 1002
userPassword:: bGRhcHVzZXI=

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

We have one user cn=ldapuser,ou=users,dc=company,dc=com already defined. The entry says that the username is ldapuser and, for testing purposes only, the user has the password set to be the same as the username.

Second, make sure ClickHouse services are ready to execute queries.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -q "SELECT version()"'
docker-compose exec clickhouse2 bash -c 'clickhouse-client -q "SELECT version()"'
docker-compose exec clickhouse3 bash -c 'clickhouse-client -q "SELECT version()"'

Configuring LDAP Server In ClickHouse

Before we can play around with different levels of integrating LDAP with ClickHouse, we need to let ClickHouse know what LDAP server is available and how we can connect to it. We can also add multiple LDAP servers. To keep it simple, we will only use the one provided in our docker-compose cluster by the openldap1 service. We need to add the following configuration file to the /etc/clickhouse-server/config.d directory to add an LDAP server.

docker-compose exec clickhouse1 bash -c 'cat <<HEREDOC > /etc/clickhouse-server/config.d/ldap_servers.xml
<?xml version="1.0" encoding="utf-8"?>
<yandex>
  <ldap_servers>
    <!--LDAP servers b9f1f80c_6598_11eb_80c1_39d7fbdc1e26-->
    <openldap1>
      <host>openldap1</host>
      <port>636</port>
      <enable_tls>yes</enable_tls>
      <auth_dn_prefix>cn=</auth_dn_prefix>
      <auth_dn_suffix>,ou=users,dc=company,dc=com</auth_dn_suffix>
      <tls_require_cert>never</tls_require_cert>
    </openldap1>
  </ldap_servers>
</yandex>
HEREDOC'

Check that our ldap_servers.xml config got merged into the preprocessed config.xml.

docker-compose exec clickhouse1 bash -c 'cat /var/lib/clickhouse/preprocessed_configs/config.xml | grep LDAP'
    <!--LDAP servers b9f1f80c_6598_11eb_80c1_39d7fbdc1e26-->

Our unique identification string is inside the preprocessed config, and therefore our config has been applied successfully.

Authenticating Existing ClickHouse Users Using LDAP

The first level of integration with LDAP that is provided by ClickHouse is to allow authentication of existing ClickHouse users using the LDAP server. It will enable us not to specify passwords for the users explicitly, but instead, we’ll let ClickHouse contact the LDAP server to validate user credentials.

Defining LDAP Authenticated Users In The users.xml

We can define LDAP authenticated user by adding a configuration file for the user to the /etc/clickhouse-server/users.d directory. We do not need to specify the password for the user, but instead, we must add the <ldap> section that will define the LDAP <server> to be used for authentications.

docker-compose exec clickhouse1 bash -c 'cat <<HEREDOC > /etc/clickhouse-server/users.d/ldapuser.xml
<?xml version="1.0" encoding="utf-8"?>
<yandex>
  <users>
    <!--LDAP users bb6f3d71_6598_11eb_80c1_39d7fbdc1e26-->
    <ldapuser>
      <ldap>
        <server>openldap1</server>
      </ldap>
    </ldapuser>
  </users>
</yandex>
HEREDOC'

Confirm that our config is merged into the preprocessed users.xml.

$ docker-compose exec clickhouse1 bash -c 'cat /var/lib/clickhouse/preprocessed_configs/users.xml | grep bb6f3d71_6598_11eb_80c1_39d7fbdc1e26'
    <!--LDAP users bb6f3d71_6598_11eb_80c1_39d7fbdc1e26-->

We can now try to execute a query using the ldapuser user.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -n --user "ldapuser" --password "ldapuser" -q "SELECT user()"'
ldapuser

It works! Now provide a wrong password to see it fail.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -n --user "ldapuser" --password "ldapuser2" -q "SELECT user()"'
Code: 516. DB::Exception: Received from localhost:9000. DB::Exception: ldapuser: Authentication failed: password is incorrect or there is no user with such name.

Delete the configuration file for the user, and let’s use RBAC commands instead.

docker-compose exec clickhouse1 bash -c 'rm -rf /etc/clickhouse-server/users.d/ldapuser.xml'

Defining LDAP Authenticated Users Using RBAC

Instead of defining users using XML configuration files, we can do the same in a much easier way using RBAC commands. Using RBAC commands is recommended because you can use the ON CLUSTER clause to create or drop users in the whole cluster.

The syntax for the CREATE USER command is defined as follows.

CREATE USER [IF NOT EXISTS | OR REPLACE] name1 [ON CLUSTER cluster_name1] 
        [, name2 [ON CLUSTER cluster_name2] ...]
    [IDENTIFIED [WITH {NO_PASSWORD|PLAINTEXT_PASSWORD|SHA256_PASSWORD|SHA256_HASH|DOUBLE_SHA1_PASSWORD|DOUBLE_SHA1_HASH}] BY {'password'|'hash'}]
    [HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
    [DEFAULT ROLE role [,...]]
    [SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY|WRITABLE] | PROFILE 'profile_name'] [,...]

And now you can also use IDENTIFIED WITH LDAP_SERVER clause to authenticate a user using an LDAP server.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -q "CREATE USER ldapuser IDENTIFIED WITH LDAP_SERVER BY '\''openldap1'\''"'

Check that the user was created.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -q "SHOW USERS"'
default
ldapuser

Finally, execute the query using the ldapuser user.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -n --user "ldapuser" --password "ldapuser" -q "SELECT user()"'
ldapuser

Let’s see it fail if the password is invalid.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -n --user "ldapuser" --password "ldapuser2" -q "SELECT user()"'
Code: 516. DB::Exception: Received from localhost:9000. DB::Exception: ldapuser: Authentication failed: password is incorrect or there is no user with such name.

Defining LDAP Authenticated Users Using RBAC Across The Cluster

The RBAC commands allow to create or drop users on the cluster. Let’s add LDAP server configuration to the other two ClickHouse nodes, clickhouse2 and clickhouse3.

First, add LDAP server configuration to clickhouse2.

docker-compose exec clickhouse2 bash -c 'cat <<HEREDOC > /etc/clickhouse-server/config.d/ldap_servers.xml
<?xml version="1.0" encoding="utf-8"?>
<yandex>
  <ldap_servers>
    <!--LDAP servers b9f1f80c_6598_11eb_80c1_39d7fbdc1e26-->
    <openldap1>
      <host>openldap1</host>
      <port>636</port>
      <enable_tls>yes</enable_tls>
      <auth_dn_prefix>cn=</auth_dn_prefix>
      <auth_dn_suffix>,ou=users,dc=company,dc=com</auth_dn_suffix>
      <tls_require_cert>never</tls_require_cert>
    </openldap1>
  </ldap_servers>
</yandex>
HEREDOC'

Let’s do the same for clickhouse3.

docker-compose exec clickhouse3 bash -c 'cat <<HEREDOC > /etc/clickhouse-server/config.d/ldap_servers.xml
<?xml version="1.0" encoding="utf-8"?>
<yandex>
  <ldap_servers>
    <!--LDAP servers b9f1f80c_6598_11eb_80c1_39d7fbdc1e26-->
    <openldap1>
      <host>openldap1</host>
      <port>636</port>
      <enable_tls>yes</enable_tls>
      <auth_dn_prefix>cn=</auth_dn_prefix>
      <auth_dn_suffix>,ou=users,dc=company,dc=com</auth_dn_suffix>
      <tls_require_cert>never</tls_require_cert>
    </openldap1>
  </ldap_servers>
</yandex>
HEREDOC'

Now we can create ldapuser on the cluster by adding the ON CLUSTER clause to our CREATE USER command.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -q "CREATE USER IF NOT EXISTS ldapuser IDENTIFIED WITH LDAP_SERVER BY '\''openldap1'\'' ON CLUSTER '\''replicated_cluster'\''"'
clickhouse2	9000	0		2	0
clickhouse1	9000	0		1	0
clickhouse3	9000	0		0	0

Now we can execute commands as ldapuser on each cluster node.

docker-compose exec clickhouse1 bash -c 'clickhouse-client -n --user "ldapuser" --password "ldapuser" -q "SELECT user()"'
docker-compose exec clickhouse2 bash -c 'clickhouse-client -n --user "ldapuser" --password "ldapuser" -q "SELECT user()"'
docker-compose exec clickhouse3 bash -c 'clickhouse-client -n --user "ldapuser" --password "ldapuser" -q "SELECT user()"'

Optimizing Login of LDAP Authenticated Users

When users are authenticated using the LDAP server, ClickHouse must contact the LDAP server for each user login. This might not be optimal when a lot of users are being authenticated. To address this problem we can use the <verification_cooldown> parameter in the LDAP server configuration that specifies a period of time, in seconds, after a successful bind attempt, during which the LDAP user will be assumed to be successfully authenticated for all consecutive requests without contacting the LDAP server. By default, this parameter is set to 0 to disable caching and force contacting the LDAP server for each authentication request.

Right now, caching is disabled so let’s measure our baseline performance on the clickhouse1.

docker-compose exec clickhouse1 bash -c 'time for n in {1..1000}; do clickhouse-client -n --user "ldapuser" --password "ldapuser" -q "SELECT 1" > /dev/null; done'
real  0m30.189s
user  0m14.492s
sys   0m9.847s

Let’s change how we define the LDAP server on the clickhouse1 node to cache LDAP user authentications for five minutes.

docker-compose exec clickhouse1 bash -c 'cat <<HEREDOC > /etc/clickhouse-server/config.d/ldap_servers.xml
<?xml version="1.0" encoding="utf-8"?>
<yandex>
  <ldap_servers>
    <!--LDAP servers b9f1f80c_6598_11eb_80c1_39d7fbdc1e26-->
    <openldap1>
      <host>openldap1</host>
      <port>636</port>
      <enable_tls>yes</enable_tls>
      <verification_cooldown>300</verification_cooldown>
      <auth_dn_prefix>cn=</auth_dn_prefix>
      <auth_dn_suffix>,ou=users,dc=company,dc=com</auth_dn_suffix>
      <tls_require_cert>never</tls_require_cert>
    </openldap1>
  </ldap_servers>
</yandex>
HEREDOC'

Here are the new numbers when caching is enabled.

docker-compose exec clickhouse1 bash -c 'time for n in {1..1000}; do clickhouse-client -n --user "ldapuser" --password "ldapuser" -q "SELECT 1" > /dev/null; done'
real  0m22.472s
user  0m12.000s
sys   0m8.894s

As we can see, we get about 26% improvement in login performance as we minimize the overhead due to repeated requests to the LDAP server.

Conclusion

In this article, we have introduced support for LDAP integration with ClickHouse. We have looked at how an LDAP server definition can be added to the ClickHouse configuration. We have also looked at the simplest case where we can use LDAP server to authenticated ClickHouse defined users either using an XML configuration file or RBAC commands. The RBAC commands provide support for the ON CLUSTER clause that helps create or drop users on the cluster without the need to modify configuration files on each cluster node. Finally, we have looked at how we can enable caching of LDAP user authentications to optimize repeated logins.

Stay tuned for the next part in this series, where we will talk about how we can use an LDAP server as an external user directory that eliminates the need to define users on the ClickHouse server explicitly. If you have further questions or want to discuss your security requirements, feel free to contact us at info@altinity.com. We’ll be glad to help!

Share