David - Musings of an SRE

Soju IRC bouncer auth with LDAP (LLDAP + PAM)

2026-09-18

While working on a little side excursion to build an Intranet for our small community at https://hackerdrinks.sg, I figured that having a unified identity provider for our users would be a great start to having a single login for every service.

I wanted the simplest possible identity setup without having to configure too much complexity, and I ended up settling on LDAP as our main identity source.

I was contemplating between using:

  • Authentik - too cumbersome, and too many levers
  • Keycloak - Java, not my style and also too many levers (for now; nothing wrong with it)
  • LLDAP - One config file is all you need

And I eventually chose LLDAP because it’s just a simple tool with a simple configuration, no extra levers, no need for complex IAM.

For the purpose of this article, I will not be talking about how to install LLDAP. You can follow their guide here


Connecting LDAP to Soju

Soju does not have LDAP support out of the box, but you can get there indirectly: soju supports PAM, and PAM can talk to LDAP via pam_ldap. The chain ends up looking like:

soju → PAM → pam_ldap → LLDAP

Technically it only requires a few things.

1. Compile soju with PAM support

$ CGO_ENABLED=1 GOFLAGS="-tags=pam" go build -o soju ./cmd/soju

If you’re using sqlite3, remember to add sqlite3 in the tags too, like -tags=pam,sqlite3.

2. Point soju at PAM

Add into soju’s config:

auth pam
enable-user-on-auth true

enable-user-on-auth makes soju create and enable a user the first time they authenticate successfully, so you don’t have to sojuctl create-user everyone by hand.

3. Install pam_ldap

$ sudo apt install libpam-ldap ldap-utils

ldap-utils isn’t strictly required, but you’ll want ldapsearch handy for debugging.

4. Create the PAM policy for soju

PAM looks up its policy by service name, and soju registers itself as soju, so the file has to be named accordingly:

$ sudo vim /etc/pam.d/soju

auth      required    pam_ldap.so
account   required    pam_ldap.so

5. Configure the LDAP connection

Edit /etc/ldap.conf and put in your necessary configuration:

base    dc=example,dc=com
uri     ldaps://ldap.example.com:6360
binddn  uid=admin,ou=people,dc=example,dc=com
bindpw  <password>
scope   sub
ldap_version 3
tls_checkpeer no

A few things worth calling out:

  • Port 6360 is not a typo. LLDAP’s defaults are 3890 for LDAP/STARTTLS and 6360 for LDAPS, precisely so it doesn’t need root to bind the privileged 389/636 ports.
  • The binddn is a search identity, not the login. LLDAP does not allow anonymous binds, so pam_ldap needs an authenticated DN to look up the user’s entry before binding as that user with their password. Consider creating a dedicated read-only user in LLDAP for this instead of admin; your future self will thank you. And since /etc/ldap.conf now contains a password, chmod 600 it.
  • TLS verification. LLDAP does not ship with a self-signed cert by default. If you’re using a self-signed certificate, make sure you set tls_checkpeer to yes and also configure the tls_cacertfile and tls_cacertdir.

6. Restart soju

$ sudo systemctl restart soju

Sanity checking it

Before blaming soju, confirm LLDAP itself is happy:

$ ldapsearch -H ldaps://ldap.example.com:6360 \
    -D 'uid=admin,ou=people,dc=example,dc=com' -W \
    -b 'ou=people,dc=example,dc=com' '(uid=alice)'

If that returns your user, connect with an IRC client as an LLDAP user and watch the logs do their thing:

$ journalctl -u soju -f
I’m currently working on fyra.sh, a CLI-first static site deployment tool where you push your site and it’s served globally through a built-in CDN, without the overhead of heavy platforms.