Using ldap2pg to integrate between AD Groups and Postgres Roles

On a recent customer call regarding Postgres instances and databases provisioned by Data Services Manager (DSM), there was a question regarding Active Directory / LDAP integration. Specifically, there was a question on how to automatically provide an Active Directory / LDAP group access to a Postgres database. Now, as many readers are aware, DSM already supports Directory Services such as Active Directory and LDAPS for Postgres databases. See this earlier blog post on how to do that. But once Directory Services is configured in DSM, and selected during Postgres database provisioning, the DBA needs to create logins and grant access to the database for each individual user. I wanted to take this a step further, and figure out how we could automate the integration of a group of users from Active Directory, and set them up with logins and database access. This led me to the ldap2ad tool. After some trial and error in the lab, I though this might be a useful tool for others facing this requirement. This is why I decided to create this post. Here you will find the steps to search for all the users in an AD group, and automatically set them up with login and superuser access to a particular database.

I began reading up the instructions on how to user ldap2pg from here. This also contains the installation instructions for ldap2pg. The first step that I wanted to test if I could retrieve the member of my AD Group using the ldapsearch tool from my Ubuntu Linux VM. I used the inbuilt ldapsearch tool to do that particular test on my AD Group called DSMUsers-Finance. In this example, user ‘cormac’ is a Domain Admin, and my domain is rainpole.com. My Domain Controller running Active Directory is dc01.rainpole.com. I was able to successfully retrieve the three members of the group using the following command.

$ ldapsearch -H ldaps://dc01.rainpole.com:3269 -cxWD "cn=cormac,cn=users,dc=rainpole,dc=com" \
-LLL -x -b "cn=users, dc=rainpole, dc=com" -s sub cn=DSMUsers-Finance member
Enter LDAP Password: *******
dn: CN=DSMUsers-Finance,CN=Users,DC=rainpole,DC=com
member: CN=dragomir,CN=Users,DC=rainpole,DC=com
member: CN=junchi,CN=Users,DC=rainpole,DC=com
member: CN=paudie,CN=Users,DC=rainpole,DC=com

Once that was working, the next step was to setup a bunch of ENV variables for both LDAP and Postgres in my user’s shell on my Ubuntu Linux VM:

cormac@cjh-ubuntu-01:~/ldap2pg$ env 
. 
. 
PGPORT=5432
PGPASSWORD=xxxxx
PGUSER=pgadmin
PGDATABASE=marketing-pg-01
PGHOST=marketing-sec01.rainpole.com
LDAPPASSWORD=xxxxx
. 
.

I also need to add entries for URI, LDAPSASL_AUTHCID and BINDDN to my /etc/ldap/ldap.conf. These are below, but are similar to some of the arguments used in the ldapsearch command seen earlier:

cormac@cjh-ubuntu-01:~/ldap2pg$ cat /etc/ldap/ldap.conf
#
# LDAP Defaults
#

# See ldap.conf(5) for details
# This file should be world readable but not world writable.

#BASE dc=example,dc=com
#URI ldap://ldap.example.com ldap://ldap-provider.example.com:666

URI ldaps://dc01.rainpole.com:3269
LDAPSASL_AUTHCID cn=cormac,cn=users,dc=rainpole,dc=com
BINDDN cn=cormac,cn=users,dc=rainpole,dc=com

#SIZELIMIT 12
#TIMELIMIT 15
#DEREF never

# TLS certificates (needed for GnuTLS)
TLS_CACERT /etc/ssl/certs/ca-certificates.crt
TLS_REQCERT never

Next, make the configuration file for ldap2pg. It is YAML formatted. I won’t go into all the details, but in a nutshell, I am asking ldap2pg to ignore all of the roles in the database by adding those entries to the “roles_blacklist_query” section.  These roles are thus not impacted by this utility. I am then asking it to search through users that are members of the DSMUsers-Finance group and create new roles on the database with LOGIN and Superuser privileges using the “userPrincipalName” that it discovers for those users. The LDAP configuration for the database as implemented by DSM Directory Services is looking for the userPrincipalName. A snippet of the pg_hba.conf is here, and queried from the pg_hba_file_rules table on the database. Note that the ldapsearchattribute is set to userPrincipalName, as per the DSM Directory Services configuration. This will be set to whatever ldapsearchattribute was set on the Directory Services configaruation by the way, but it does default to userPrincipalName.

26 | /pgsql/data/pg_hba.conf |         161 | host    | {all}         | {all}   | 0.0.0.0  |
 0.0.0.0     |     ldap    |{ldapserver=dc01.rainpole.com,ldapport=3269,ldapscheme=ldaps,
"ldapbasedn=dc=rainpole,dc=com",ldapbinddn=cormac@rainpole.com,ldapbindpasswd=VMware123!,
ldapsearchattribute=userPrincipalName,ldapscope=2} |

And here is the Directory Services configuration from DSM:

The script will pick up the database from the environment variables that I set earlier, and will also pick up the LDAP password through environment variables. The other LDAP details are coming from the /etc/ldap/ldap.conf.

cormac@cjh-ubuntu-01:~/ldap2pg$ cat ldap2pg.yml
version: 6
postgres:
  roles_blacklist_query:
  - postgres
  - postgres_exporter
  - "pg*"
  - "*slot*"
rules:
  - description: "Sync roles from LDAP to PostgreSQL"
    ldapsearch:
      base: "cn=Users,dc=rainpole,dc=com"
      filter: (memberOf=CN=DSMUsers-Finance,CN=Users,DC=rainpole,DC=com)
    roles:
      - name: "{userPrincipalName.lower()}"
        options: LOGIN SUPERUSER

Start with a dry-run of the configuration. Note that it tells us what it will do. Basically, if run for real, it would create 3 new roles corresponding to the users in my AD group. These are the same users reported in my LDAP search output above, so this looks good.

cormac@cjh-ubuntu-01:~/ldap2pg$ ./ldap2pg -c ldap2pg.yml
11:39:20 INFO   Starting ldap2pg                                 version=6.4.2 commit=07ed9548 runtime=go1.24.2 pid=195160
11:39:20 INFO   Using YAML configuration file.                   path=ldap2pg.yml
11:39:20 WARN   Dry run. Postgres instance will be untouched.   
11:39:20 INFO   Running as superuser.                            user=pgadmin super=true server="PostgreSQL 17.5" cluster="" database=marketing-pg-01
11:39:20 INFO   Connected to LDAP directory.                     uri=ldaps://dc01.rainpole.com:3269
11:39:20 INFO   Sync roles from LDAP to PostgreSQL              
11:39:20 CHANGE Would Create role.                               role=paudie database=marketing-pg-01
11:39:20 CHANGE Would Set role comment.                          role=paudie database=marketing-pg-01
11:39:20 CHANGE Would Create role.                               role=junchi database=marketing-pg-01
11:39:20 CHANGE Would Set role comment.                          role=junchi database=marketing-pg-01
11:39:20 CHANGE Would Create role.                               role=dragomir database=marketing-pg-01
11:39:20 CHANGE Would Set role comment.                          role=dragomir database=marketing-pg-01
11:39:20 INFO   Comparison complete.                             searches=1 roles=3 queries=6 grants=0
11:39:20 INFO   Use --real option to apply changes.             
11:39:20 INFO   Done.                                            elapsed=53.136845ms mempeak=1.2MiB ldap=658.263µs inspect=13.079637ms sync=0s

Let’s now run it for real by adding the ‘–real’ option to the ldap2pg command.

cormac@cjh-ubuntu-01:~/ldap2pg$ ./ldap2pg -c ldap2pg.yml --real
11:40:18 INFO   Starting ldap2pg                                 version=6.4.2 commit=07ed9548 runtime=go1.24.2 pid=195319
11:40:18 INFO   Using YAML configuration file.                   path=ldap2pg.yml
11:40:18 INFO   Real mode. Postgres instance will be modified. 
11:40:18 INFO   Running as superuser.                            user=pgadmin super=true server="PostgreSQL 17.5" cluster="" database=marketing-pg-01
11:40:18 INFO   Connected to LDAP directory.                     uri=ldaps://dc01.rainpole.com:3269
11:40:18 INFO   Sync roles from LDAP to PostgreSQL              
11:40:18 CHANGE Create role.                                     role=paudie database=marketing-pg-01
11:40:18 CHANGE Set role comment.                                role=paudie database=marketing-pg-01
11:40:18 CHANGE Create role.                                     role=junchi database=marketing-pg-01
11:40:18 CHANGE Set role comment.                                role=junchi database=marketing-pg-01
11:40:18 CHANGE Create role.                                     role=dragomir database=marketing-pg-01
11:40:18 CHANGE Set role comment.                                role=dragomir database=marketing-pg-01
11:40:18 INFO   Comparison complete.                             searches=1 roles=3 queries=6 grants=0
11:40:18 INFO   Done.                                            elapsed=73.766163ms mempeak=1.2MiB ldap=669.39µs inspect=20.893096ms sync=5.861424ms

Now let’s check if it worked by logging into the database as pgadmin, and displaying the roles and logins. Let’s see if our users from the group are present.

$ psql postgresql://pgadmin:xxxxx@marketing-sec01.rainpole.com:5432/marketing-pg-01
psql (16.9 (Ubuntu 16.9-0ubuntu0.24.04.1), server 17.5 (VMware Postgres 17.5.0))
WARNING: psql major version 16, server major version 17.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

marketing-pg-01=# SELECT rolname FROM pg_roles WHERE rolsuper = true;
          rolname          
---------------------------
postgres
pgadmin
pgsql-rep-user
pgsql-rep-user-fin
paudie@rainpole.com
junchi@rainpole.com
psql-rep-slot-sec-user
dragomir@rainpole.com
pgautofailover_replicator
psql-rep-slot-sec-user2
(10 rows)

marketing-pg-01=# SELECT rolname FROM pg_roles WHERE rolcanlogin = true;
          rolname          
---------------------------
postgres
pgautofailover_monitor
postgres_exporter
pgadmin
pgappuser
pgsql-rep-user
pgsql-rep-user-fin
paudie@rainpole.com
junchi@rainpole.com
psql-rep-slot-sec-user
dragomir@rainpole.com
pgrouser
pgautofailover_replicator
psql-rep-slot-sec-user2
pgrwuser
(15 rows)

marketing-pg-01=# \q

It certainly seems to have picked up the userPrincipalNames of each of the group members. And it does appear that have both Superuser and Login capabilities. Now lets see if we can login to the database as one of those users. I am choosing user junchi@rainpole.com. Make sure you unset the PGPASSWORD environment variable btw, or psql will pick that up for this login attempt.

$ psql postgresql://marketing-sec01.rainpole.com:5432/marketing-pg-01 -U junchi@rainpole.com
Password for user junchi@rainpole.com: ******
psql (16.9 (Ubuntu 16.9-0ubuntu0.24.04.1), server 17.5 (VMware Postgres 17.5.0))
WARNING: psql major version 16, server major version 17.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

marketing-pg-01=# \conninfo
You are connected to database "marketing-pg-01" as user "junchi@rainpole.com" on host "marketing-sec01.rainpole.com" (address "xx.xx.xx.xx") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)

marketing-pg-01=# \q

Well that all appears to have worked really well. It certainly seems that ldap2pg could be a useful tool for any admin to have access to, particularly if they need to give lots of AD/LDAP users access to various databases. There is obviously more granular controls in tools around permissions and grants, but this was just a simple proof-of-concept. Check out the rest of the ldap2pg documentation to learn more. And for anyone who is looking for an on-premises DBaaS offering for the VMware Cloud Foundation (VCF) deployment, be sure to check out Data Services Manager and all of the benefits that it brings, from automated lifecycle management to fleet management to automated backups.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.