✨ 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:
11 files changed
+519
-282
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user