churchtools ldap wrapper with virtual @leiter groups for role based permissions in nextcloud and co
  • JavaScript 97.9%
  • Dockerfile 2.1%
Find a file
2026-09-14 13:40:50 +02:00
lib fix: keep the directory cache refreshing on its own 2026-09-14 13:40:47 +02:00
test fix: keep the directory cache refreshing on its own 2026-09-14 13:40:47 +02:00
.dockerignore build: build the image from the repo root layout 2026-09-13 18:59:40 +02:00
.gitignore chore: ignore the deployment parity baseline directory 2026-09-13 19:01:04 +02:00
changeConfig.js chore: import unmodified production code 2026-09-13 18:02:22 +02:00
CHANGELOG.md feat: add log levels and a health endpoint 2026-09-13 22:05:35 +02:00
ctldap.example.config fix: keep the directory cache refreshing on its own 2026-09-14 13:40:47 +02:00
ctldap.js fix: keep the directory cache refreshing on its own 2026-09-14 13:40:47 +02:00
docker-compose.yml build: pin the base image by digest 2026-09-13 20:48:42 +02:00
Dockerfile feat: add log levels and a health endpoint 2026-09-13 22:05:35 +02:00
LICENSE chore: import unmodified production code 2026-09-13 18:02:22 +02:00
OPTIMIZATION-PLAN.md docs: add fork documentation and plan 2026-09-13 18:03:11 +02:00
package.json refactor: replace native bcrypt with bcryptjs 2026-09-13 20:45:21 +02:00
README.md feat: add log levels and a health endpoint 2026-09-13 22:05:35 +02:00
yarn.lock refactor: replace native bcrypt with bcryptjs 2026-09-13 20:45:21 +02:00

ctsldap — ChurchTools → LDAP bridge

ctsldap is a small Node.js service that presents the people and groups of a ChurchTools instance over LDAP, so that LDAP-aware applications can authenticate and resolve users and groups against ChurchTools.

In this deployment it exists for exactly one consumer: Nextcloud, which uses it as its user backend for single sign-on.

This repository is a fork of karl007/ctldap-ms (itself a fork of churchtools/ctldap-ms), packaged to run as a Docker container on the Nextcloud host.


Security note — read this first

Never expose port 1389 to the internet.

The upstream code depends on ldapjs 1.0.2, which has an unhandled exception in its ASN.1 parser. Any malformed LDAP packet — exactly what internet-wide port scanners send — crashes the process. The previous bare-metal deployment listened on 0.0.0.0:1389 and was restarted 854 times by its supervisor as a direct result.

The containerised setup in this repository therefore:

  • publishes no ports at all (there is no ports: key in docker-compose.yml),
  • joins the pre-existing external Docker network caddy,
  • is reachable only from inside that network, as host ctldap on port 1389.

Nextcloud is configured to talk to ctldap:1389. Nothing outside the Docker network can reach the LDAP listener. Keep it that way.

Also note that ctldap.config contains the ChurchTools API credentials and the LDAP root password in cleartext. It is listed in .gitignore and must never be committed, pasted into an issue, or baked into a container image.


Architecture

Nextcloud  ──LDAP──▶  ctsldap  ──HTTPS/REST──▶  ChurchTools
   (container)        (container)                (external)
        └──────── docker network "caddy" ────────┘
  • ctldap.js — the LDAP server and the ChurchTools API client.
  • changeConfig.js — helper to add/update per-site sections in the config file.
  • Dockerfile — two-stage build on node 22 LTS, pinned by digest so the base image cannot change under an unchanged tag. The old node 16 pin existed only for the native bcrypt@3; since password hashing moved to the pure-JS bcryptjs, nothing in the tree needs a C++ toolchain. The test suite is verified on node 16, 18, 20, 22 and 24. To move the base image, resolve the new digest with docker buildx imagetools inspect node:22-bookworm-slim and update it in both Dockerfile and docker-compose.yml.
  • docker-compose.yml — runs the image with read_only: true, no-new-privileges, a tmpfs for /tmp, log rotation, and the config bind-mounted read-only.

Configuration

The application reads a single INI-style file, ctldap.config, from its working directory. Inside the container that resolves to /app/ctldap.config, which is supplied by a read-only bind mount.

cp ctldap.example.config ctldap.config
$EDITOR ctldap.config
chmod 640 ctldap.config

Settings that matter most:

Key Meaning
ct_uri Root URL of the ChurchTools instance.
api_user ChurchTools user for API access. Needs churchcore:administer persons and churchdb:view.
api_password Password for that user. Use a long random value.
ldap_user LDAP bind user, e.g. root (a virtual user, recommended).
ldap_password Password for the LDAP bind user. Use a long random value.
ldap_base_dn LDAP base, e.g. churchtoolso=churchtools.
ldap_ip Must be 0.0.0.0 in the container so Docker can route to it.
ldap_port 1389.
cache_lifetime How long (ms) fetched ChurchTools data is reused.
log_level error, warn, info (default) or debug. Only debug logs member DNs.
health_port Enables the health/metrics endpoint on loopback. Unset = off.

ctldap.example.config documents every option and contains placeholders only.

Health and metrics

With health_port set, the service exposes two read-only routes on loopback:

  • GET /health200 while starting or healthy, 503 when degraded, with the reasons listed (cache too old, ChurchTools failure rate too high, entry count below an alarm floor, sanity floor tripped).
  • GET /metrics — counters and entry counts as JSON.

Both payloads are counters and sizes only: no member, no DN, no login name and not the ChurchTools URI. The endpoint binds 127.0.0.1, so unlike the LDAP port it is not reachable from the caddy network. The container healthcheck uses it when it is configured and falls back to the plain TCP probe when it is not.

ctldap.config is in .gitignore. Do not remove it from there.

Resulting DNs

With ldap_base_dn=churchtools:

  • bind DN: cn=root,ou=users,o=churchtools
  • users: ou=users,o=churchtools
  • groups: ou=groups,o=churchtools

Build and run

From the repository root, with ctldap.config in place and the external caddy network already existing:

docker compose build
docker compose up -d

Check it:

docker compose ps            # expect: healthy
docker compose logs -f ctldap

The image ships a healthcheck that opens a TCP connection to 1389 inside the container, so an unhealthy state means the LDAP listener is not accepting connections.

To verify from another container on the caddy network:

docker run --rm --network caddy alpine sh -c 'nc -z -v ctldap 1389'

Contributing

Branching and review convention for this repository:

  • main is the integration branch and is expected to stay deployable.
  • Do not commit directly to main.
  • Create a feature branch per change, e.g. fix/ct-uri-debug-leak or feat/asn1-error-guard.
  • Open a pull request against main. Keep the pull request focused on one concern and describe what was verified.
  • Never include real configuration values, credentials, log excerpts containing personal data, or member data in commits, pull requests or issues.

OPTIMIZATION-PLAN.md records the reviewed findings and the planned work.


License

See LICENSE. Upstream changes are documented in CHANGELOG.md.