feat(bootstrap): load enabled feature modules automatically

This commit is contained in:
Bertrand Gouny
2026-10-19 19:24:34 +02:00
parent bb946e50eb
commit 6cf97026b6
11 changed files with 144 additions and 18 deletions
+3 -1
View File
@@ -8,8 +8,10 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
- Add optional `openldap-tls-reload` service to restart slapd after TLS file
contents change, ignoring duplicate events and unchanged projected volumes.
- Default `OPENLDAP_BOOTSTRAP_MODULES` to `auto` to load base and enabled feature
modules; explicit lists retain full control over module selection and order.
- Add optional file-based auditing with `OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED`
and `OPENLDAP_BOOTSTRAP_AUDITLOG_FILE`; load `auditlog.so` automatically when enabled.
and `OPENLDAP_BOOTSTRAP_AUDITLOG_FILE`.
- Add `OPENLDAP_BOOTSTRAP_DATA_LAST_BIND` and `OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION`
to track successful LDAP authentications in `pwdLastSuccess` (disabled by default).
- Add `OPENLDAP_BOOTSTRAP_REFINT_NOTHING_DN` to configure `olcRefintNothing` and
+2 -2
View File
@@ -337,7 +337,7 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \
| ----------------------------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `OPENLDAP_BOOTSTRAP_ORGANIZATION` | Organization name used in bootstrap data | `Example Org` |
| `OPENLDAP_BOOTSTRAP_SUFFIX` | Base LDAP suffix; must start with `dc=` | `dc=example,dc=org` |
| `OPENLDAP_BOOTSTRAP_MODULES` | OpenLDAP modules loaded during bootstrap | `back_mdb.so argon2.so ppolicy.so unique.so refint.so memberof.so syncprov.so` |
| `OPENLDAP_BOOTSTRAP_MODULES` | `auto`: base and enabled feature modules; an explicit list sets the exact modules and order | `auto` |
| `OPENLDAP_BOOTSTRAP_SCHEMAS` | LDIF schemas imported during bootstrap | `core.ldif cosine.ldif inetorgperson.ldif rfc2307bis.ldif` |
| `OPENLDAP_BOOTSTRAP_GLOBAL_SIZE_LIMIT` | Global LDAP size limit | `500` |
| `OPENLDAP_BOOTSTRAP_GLOBAL_TIME_LIMIT` | Global LDAP time limit | `120` |
@@ -357,7 +357,7 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \
| `OPENLDAP_BOOTSTRAP_MONITOR_READONLY` | Enable read-only monitor account creation | `false` |
| `OPENLDAP_BOOTSTRAP_MONITOR_READONLY_DN` | DN of the read-only monitor account | `cn=readonly-monitor,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
| `OPENLDAP_BOOTSTRAP_MONITOR_READONLY_PASSWORD_HASHED` | Hashed password of the read-only monitor account; generated if empty when enabled | `` |
| `OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED` | Log successful LDAP writes; loads `auditlog.so` automatically | `false` |
| `OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED` | Log successful LDAP writes | `false` |
| `OPENLDAP_BOOTSTRAP_AUDITLOG_FILE` | Audit LDIF file; requires a writable parent directory | `/var/log/openldap/audit.ldif` |
| `OPENLDAP_BOOTSTRAP_LDIF_CONFIG_DIR` | Source directory for config LDIF bootstrap files | `/container/services/openldap-bootstrap/assets/ldif/config` |
| `OPENLDAP_BOOTSTRAP_LDIF_DATA_DIR` | Source directory for data LDIF bootstrap files | `/container/services/openldap-bootstrap/assets/ldif/data` |
+1
View File
@@ -27,6 +27,7 @@ After the shared version check, `tests/` runs integration scenarios on each buil
projected volumes and duplicate events, and preserve intentional service stops;
run bootstrap, schema conversion and TLS renewals with `/tmp` mounted read-only;
- alternative schemas (`nis`, `virtualMail`);
- automatic modules: feature flags, replication, exact custom ordering and aliases;
- auditlog: automatic module loading, writes, custom paths, rotation and restarts;
- last successful binds: disabled by default, precision throttling, failed binds
and compatibility with ppolicy;
+2 -2
View File
@@ -26,9 +26,9 @@ func AuditLog(ctx context.Context, client *dagger.Client, image *dagger.Containe
settings["OPENLDAP_BOOTSTRAP_AUDITLOG_FILE"] = file
}
if mode == "minimal" || mode == "explicit" {
settings["OPENLDAP_BOOTSTRAP_MODULES"] = "back_mdb.so argon2.so"
settings["OPENLDAP_BOOTSTRAP_MODULES"] = "back_mdb.so argon2.so auditlog.so"
if mode == "explicit" {
settings["OPENLDAP_BOOTSTRAP_MODULES"] += " /usr/lib/openldap/auditlog.so"
settings["OPENLDAP_BOOTSTRAP_MODULES"] = "back_mdb.so argon2.so /usr/lib/openldap/auditlog.so"
}
}
if err := check(ctx, withEnv(image, merge(passwords, settings)), nil, env{
+72
View File
@@ -0,0 +1,72 @@
package tests
import (
"context"
_ "embed"
"fmt"
"dagger.io/dagger"
)
// AutomaticModules checks automatic selection and exact explicit module ordering.
func AutomaticModules(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
passwords, err := rootPasswords(ctx, image)
if err != nil {
return err
}
const baseModules = "back_mdb.so argon2.so"
const explicitModules = "back_mdb.so pw-sha2.so argon2.so memberof refint.so unique /usr/lib/openldap/ppolicy.so auditlog"
type moduleCase struct {
name string
settings env
modules string
fails bool
}
cases := []moduleCase{
{"default", nil, baseModules, false},
{"custom order", env{"OPENLDAP_BOOTSTRAP_MODULES": "back_mdb.so pw-sha2.so argon2.so memberof.so"}, "back_mdb.so pw-sha2.so argon2.so memberof.so", false},
{"explicit aliases", env{
"OPENLDAP_BOOTSTRAP_MODULES": explicitModules,
"OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED": "true",
"OPENLDAP_BOOTSTRAP_PPOLICY": "true",
"OPENLDAP_BOOTSTRAP_UNIQUE": "true",
"OPENLDAP_BOOTSTRAP_REFINT": "true",
"OPENLDAP_BOOTSTRAP_MEMBEROF": "true",
}, explicitModules, false},
{"missing explicit module", env{
"OPENLDAP_BOOTSTRAP_MODULES": baseModules,
"OPENLDAP_BOOTSTRAP_UNIQUE": "true",
}, "", true},
{"all automatic", env{
"OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED": "true",
"OPENLDAP_BOOTSTRAP_PPOLICY": "true",
"OPENLDAP_BOOTSTRAP_UNIQUE": "true",
"OPENLDAP_BOOTSTRAP_REFINT": "true",
"OPENLDAP_BOOTSTRAP_MEMBEROF": "true",
"OPENLDAP_BOOTSTRAP_REPLICATION": "true",
"OPENLDAP_BOOTSTRAP_REPLICATION_DATA_READONLY_PASSWORD": replicationPassword,
}, baseModules + " ppolicy.so unique.so refint.so memberof.so auditlog.so syncprov.so", false},
}
for _, feature := range []struct{ flag, module string }{
{"AUDITLOG_ENABLED", "auditlog"},
{"PPOLICY", "ppolicy"},
{"UNIQUE", "unique"},
{"REFINT", "refint"},
{"MEMBEROF", "memberof"},
} {
cases = append(cases, moduleCase{feature.module, env{"OPENLDAP_BOOTSTRAP_" + feature.flag: "true"}, baseModules + " " + feature.module + ".so", false})
}
for _, tc := range cases {
if err := check(ctx, withEnv(image, merge(passwords, tc.settings)), nil, env{
"TEST_MODULES": tc.modules,
"TEST_BOOTSTRAP_FAILS": fmt.Sprint(tc.fails),
}, modulesCheck); err != nil {
return fmt.Errorf("modules %s: %w", tc.name, err)
}
}
return nil
}
//go:embed testdata/modules.sh
var modulesCheck string
+1
View File
@@ -22,6 +22,7 @@ var scenarios = []scenario{
{"read-only bootstrap templates", ReadOnlyTemplates},
{"alternative schemas", AlternativeSchemas},
{"TLS certificate reload", TLSReload},
{"automatic module loading", AutomaticModules},
{"audit logging", AuditLog},
{"last successful binds", LastBind},
{"refint empty-group placeholders", RefintNothing},
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -euo pipefail
# if container log level is trace:
# print commands and their arguments as they are executed
container log level eq trace && set -x
# shellcheck disable=SC1091
source /tmp/openldap-test-lib.sh
if [ "${OPENLDAP_BOOTSTRAP_REPLICATION:-false}" = "true" ]; then
OPENLDAP_BOOTSTRAP_REPLICATION_HOSTS="ldap://$(hostname -f):3890 ldap://other:3890"
export OPENLDAP_BOOTSTRAP_REPLICATION_HOSTS
fi
if container --log-level debug --skip-process > /tmp/openldap-modules-bootstrap.log 2>&1; then
[ "${TEST_BOOTSTRAP_FAILS}" = "false" ] || fail 'missing explicit module was automatically added'
else
if [ "${TEST_BOOTSTRAP_FAILS}" = "true" ]; then
if ! grep -q 'str2ad(olcUniqueURI): attribute type undefined' /tmp/openldap-modules-bootstrap.log; then
tail -60 /tmp/openldap-modules-bootstrap.log
fail 'bootstrap did not fail for the missing unique module'
fi
ok 'explicit module lists do not add missing feature modules'
exit 0
fi
tail -60 /tmp/openldap-modules-bootstrap.log
exit 1
fi
expect_rc 0 container run -- slapcat -n 0 -a '(objectClass=olcModuleList)' -o ldif_wrap=no
ACTUAL_MODULES=$(sed -n 's/^olcModuleLoad: {[0-9]*}//p' "${OUTPUT_FILE}")
read -ra EXPECTED_MODULES <<< "${TEST_MODULES}"
EXPECTED_ORDER=$(printf '%s\n' "${EXPECTED_MODULES[@]}")
if [ "${ACTUAL_MODULES}" != "${EXPECTED_ORDER}" ]; then
cat "${OUTPUT_FILE}" >&2
fail "expected modules in order: ${TEST_MODULES}"
fi
ok "loaded modules in order: ${TEST_MODULES}"
+1 -1
View File
@@ -1,7 +1,7 @@
OPENLDAP_BOOTSTRAP_ORGANIZATION="Example Org"
OPENLDAP_BOOTSTRAP_SUFFIX="dc=example,dc=org"
OPENLDAP_BOOTSTRAP_MODULES="back_mdb.so argon2.so ppolicy.so unique.so refint.so memberof.so syncprov.so"
OPENLDAP_BOOTSTRAP_MODULES="auto"
OPENLDAP_BOOTSTRAP_SCHEMAS="core.ldif cosine.ldif inetorgperson.ldif rfc2307bis.ldif"
OPENLDAP_BOOTSTRAP_GLOBAL_SIZE_LIMIT=500
@@ -60,7 +60,7 @@ Core OpenLDAP configuration applied on every bootstrap. Files are numbered to co
| File | Description |
|------|-------------|
| `00-global.ldif.template` | Global limits (size, time, idle timeout) |
| `10-module.ldif.template` | Module path and dynamic module list |
| `10-module.ldif.template` | Module path and module list (`auto` or explicit order) |
| `20-schema.ldif` | Schema configuration container |
| `30-schema-import.ldif.template` | Additional schemas to load |
| `40-frontend.ldif.template` | Frontend database (password hash algorithm) |
@@ -25,18 +25,28 @@ if [ "${TYPE}" = "env" ]; then
ensure_hashed_password OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED "config root"
ensure_hashed_password OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED "database root"
auditlog_module=false
for module in ${OPENLDAP_BOOTSTRAP_MODULES}; do
OPENLDAP_BOOTSTRAP_MODULES_LDIF+="olcModuleload: ${module}"$'\n'
case "${module##*/}" in
auditlog|auditlog.so|auditlog.la) auditlog_module=true ;;
esac
done
if [ "${OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED}" = "true" ] && [ "${auditlog_module}" = "false" ]; then
OPENLDAP_BOOTSTRAP_MODULES_LDIF+="olcModuleload: auditlog.so"$'\n'
modules="${OPENLDAP_BOOTSTRAP_MODULES}"
if [ "${modules}" = "auto" ]; then
modules="back_mdb.so argon2.so"
declare -A auto_modules=(
[ppolicy]="${OPENLDAP_BOOTSTRAP_PPOLICY}"
[unique]="${OPENLDAP_BOOTSTRAP_UNIQUE}"
[refint]="${OPENLDAP_BOOTSTRAP_REFINT}"
[memberof]="${OPENLDAP_BOOTSTRAP_MEMBEROF}"
[auditlog]="${OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED}"
[syncprov]="${OPENLDAP_BOOTSTRAP_REPLICATION}"
)
for module in ppolicy unique refint memberof auditlog syncprov; do
if [ "${auto_modules[${module}]}" = "true" ]; then
modules+=" ${module}.so"
fi
done
fi
for module in ${modules}; do
OPENLDAP_BOOTSTRAP_MODULES_LDIF+="olcModuleload: ${module}"$'\n'
done
# schemas are included by path instead of by content: large schemas (e.g. msuser.ldif)
# exceed the kernel size limit of a single environment variable and make every
# following command fail with "Argument list too long"
@@ -11,7 +11,7 @@ Scripts are executed in numeric order. Most are feature-gated by an environment
| Script | Trigger | Description |
|--------|---------|-------------|
| `00-base.sh` | always | Core bootstrap: password hashing for root DNs, module and schema setup, last successful bind settings, base LDIF files |
| `00-base.sh` | always | Core bootstrap: root passwords, automatic or explicit modules, schemas and base LDIF files |
| `10-tls.sh` | `OPENLDAP_BOOTSTRAP_TLS=true` | Applies TLS certificate and protocol configuration |
| `15-tls-required.sh` | `OPENLDAP_BOOTSTRAP_TLS_REQUIRED=true` | Enforces minimum 128-bit SSF (warns if TLS is not also enabled) |
| `20-database-readonly.sh` | `OPENLDAP_BOOTSTRAP_DATA_READONLY=true` | Creates a read-only service account for the data database |