Files
akritikos f5a5204b75 ✨ Adds MariaDB image with direct LDAP PAM authentication
- Introduced a new Containerfile to build a MariaDB image with direct LDAP PAM support.
- Implemented a custom PAM module (`pam_ldap_direct.so`) built from the PADL source to avoid dependencies on `libpam-ldap` and `nslcd`.
- Updated the entrypoint script to dynamically generate the PAM configuration file (`/etc/pam_ldap.conf`) based on environment variables.
- Added a README.md to document the image usage, configuration, and build process.
- Created a test suite to validate the image build and entrypoint behavior, ensuring proper handling of environment variables and configuration generation.
- Configured health checks and ensured the stock MariaDB entrypoint and lifecycle are preserved.
2026-10-09 14:13:03 +03:00

8.1 KiB

MariaDB with direct LDAP PAM authentication

This image keeps MariaDB's auth_pam plugin and a direct LDAP PAM module. No nslcd or SSSD daemon is configured or started. The existing SQL accounts IDENTIFIED VIA pam USING 'mariadb' and the MaxScale clear-password pass-through configuration remain unchanged.

Why the package name was insufficient

In Ubuntu 26.04, libpam-ldap is a virtual package provided by libpam-ldapd. That implementation's pam_ldap.so requires nslcd. An apt-get install libpam-ldap request therefore did not install the direct module described by the old image comments.

The Containerfile now builds the direct PADL implementation from commit 656448f091cbeb9efb3ece08e6868e40b8e7b6f8. The downloaded archive must match SHA256 1aa622e85621b9018025e3483956d854566a5bc0636a4e8d9d005093f02dd16b. The build and runtime stages share the same MariaDB base, avoiding cross-distribution library copies. Only the built module and its license files are copied into the runtime stage.

The module is installed at /usr/local/lib/security/pam_ldap_direct.so and selected by that absolute path in pam.d/mariadb. A distro module with the same generic pam_ldap.so name cannot silently replace it in this PAM stack.

This is legacy upstream code, not a claim that the distribution now supports it. Its revision, dependencies and authentication behavior need review when updating the image. The build selects the source's GNU C89 dialect and supplies its source directory in PERL5LIB for the upstream version generator; neither change suppresses an authentication or TLS failure.

Runtime configuration

The entrypoint continues to accept the existing variables:

Variable Meaning
LDAPURI One or more whitespace-separated LDAP endpoints, all using the same transport
LDAP_BASE User-search base DN
LDAPBINDDN Existing directory search account's bind DN
LDAPPASSWORD Its password, supplied at runtime, never in the image or repository
LDAP_LOGIN_ATTR Login-name attribute, such as sAMAccountName
LDAP_TLS_REQCERT demand or hard; weaker verification is rejected
CERTS_GID The actual host TLS-key group ID, retained in mysql's groups after the stock privilege drop

All values must be single-line, and required credentials have no defaults. The renderer writes one root-only /etc/pam_ldap.conf, which is the path compiled into the direct module. It no longer writes a second /etc/ldap.conf.

PADL's verification directive is tls_checkpeer yes, not tls_reqcert. Its CA-file directive is tls_cacertfile; this image points it to /etc/ssl/certs/ca-certificates.crt, installed by ca-certificates. No private CA file is required for the lab's Let's Encrypt certificates. Endpoints must still provide a valid full chain for their hostnames.

ldaps:// endpoints use LDAPS. ldap:// endpoints explicitly use StartTLS before binding. Mixing those transports in one endpoint list is rejected rather than silently selecting an incompatible mode or permitting an unencrypted fallback.

The MariaDB PAM helper remains setuid root so it can read the bind credential. The build checks the MariaDB plugin/helper and direct module dependencies, restricts the helper directory to root and mysql, and verifies mysql can execute the helper. Do not enable no-new-privileges for this backend container: it would prevent that elevation. Startup now reports this configuration error explicitly instead of proceeding to generic access-denied failures.

The privilege check uses setpriv --dump from util-linux, which queries the kernel's PR_GET_NO_NEW_PRIVS interface. It does not depend on a NoNewPrivs line in process status: that field only appeared in Linux 4.10, whereas the kernel query exists since 3.5. This matters on older Synology kernels. A confirmed flag value of 1 is still rejected; an unavailable or malformed query fails explicitly rather than silently assuming privilege elevation will work.

The stock MariaDB entrypoint, certificate group handling, data directory, replication settings and database healthcheck remain in use. This change does not create accounts, change passwords, reseed databases, or alter database ownership.

Build and rollout

The default engine version is explicitly 13.0.2, matching the running primary and replica observed during this incident. The old custom tag 11.8 actually contained MariaDB 13.0.2; do not deploy an actual 11.8 binary against the restored 13.0 data.

From this image directory, the owner can build a distinctly named image:

docker build -f Containerfile -t containers.kritikos.io/system/mariadb-extended:13.0.2-pam-direct .

Publish and pin the resulting image digest through the normal registry workflow. The assistant does not build, push or deploy images to the lab.

Deploy to zagreus first, retaining its data mount, server ID, read-only setting, replication configuration, full-chain certificate and actual CERTS_GID. Check a direct AD-authenticated MariaDB connection to zagreus before changing Plutus. Then update only Plutus's MariaDB container; that briefly disconnects primary clients, but requires no NAS reboot and no PostgreSQL restart.

Verify on each updated backend:

docker exec mariadb mariadbd --version
docker exec mariadb cat /etc/pam.d/mariadb
docker exec mariadb ldd /usr/local/lib/security/pam_ldap_direct.so

The PAM service must name the explicit direct module, its dependencies must resolve, and the engine must remain 13.0.2. Do not print /etc/pam_ldap.conf; it contains the directory bind password.

Then, from a client, use a real directory user's password with verified TLS:

mariadb --no-defaults --protocol=TCP --host=zagreus.ad.kritikos.io --port=3306 --user=kritikos --password --ssl --ssl-verify-server-cert --database=grimmory --execute='SELECT CURRENT_USER(), CURRENT_ROLE(), @@server_id;'

Repeat against plutus and finally mariadb.kritikos.io through MaxScale. A root or monitoring account using native-password authentication does not validate PAM. Check replica status and ordinary database health as well; this image does not change the existing replication channel.

Validation scope

The pinned source was compiled and dynamically loaded locally under WSL with available PAM and OpenLDAP headers. The compiled module exported pam_sm_authenticate and pam_sm_acct_mgmt, linked directly to libldap, and detected both ldap_initialize and StartTLS support. That was a local build, not execution inside the target Ubuntu image or against AD.

tests/test_pam_image.py checks the pinned image recipe and runs a rendered entrypoint in temporary directories with stubbed identity/group/stock-entrypoint commands. It exercises TLS modes, configuration generation, credential redaction, missing components, privilege constraints and argument forwarding without contacting hosts:

python3 -m unittest discover -s tests -v

Entrypoint execution tests need a POSIX environment. The assistant has not built or deployed the Docker image. On 2026-10-03, the owner verified the kritikos PAM/AD login directly against both backends over verified TLS, with the expected aggregate role and primary/replica server IDs.

The later successful read through MaxScale required a separate site configuration correction on zagreus: bind_address = *, providing both IPv4 and IPv6 listeners. Its IPv4-only listener did not cover the IPv6 addresses advertised by its hostname. This was a backend listener change, not an LDAP password, grant or TLS-verification change.

A later controlled test wrote one row through MaxScale to Plutus (server ID 1, read-only OFF) and read the same row directly on zagreus (server ID 2, read-only ON), retaining the recorded writer server ID 1. That verifies primary write routing and replication of a new row. It does not establish failover behavior or crash durability. Cleanup of the disposable probe table was not shown in those results.