✨ 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.
This commit is contained in:
akritikos committed 2026-10-09 14:13:03 +03:00
1 parent a4fc4e2ae2
commit f5a5204b75
11 files changed
+519 -282

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
# 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](https://packages.ubuntu.com/resolute/libpam-ldap) is a virtual package
provided by [libpam-ldapd](https://packages.ubuntu.com/resolute/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](Containerfile) now builds the direct PADL implementation from
[commit 656448f091cbeb9efb3ece08e6868e40b8e7b6f8](https://github.com/PADL/pam_ldap/tree/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](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](docker-entrypoint.sh) 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:
```sh
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:
```sh
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:
```sh
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](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:
```sh
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.