feat(bootstrap): add last successful bind tracking

This commit is contained in:
Bertrand Gouny
2026-10-18 23:29:58 +02:00
parent d7fd334797
commit 8e34fdbc19
11 changed files with 131 additions and 3 deletions
+2
View File
@@ -6,6 +6,8 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
### Added
- 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
automatically bootstrap a passwordless replacement role for empty groups. An empty
DN disables both, preserving the default behavior.
+2
View File
@@ -344,6 +344,8 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \
| `OPENLDAP_BOOTSTRAP_CONFIG_ROOT_DN` | Root DN for `cn=config` | `cn=admin,cn=config` |
| `OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED` | Hashed password for `cn=config`; generated if empty | `` |
| `OPENLDAP_BOOTSTRAP_DATA_DATABASE_MAX_SIZE` | MDB max size for main database | `10737418240` |
| `OPENLDAP_BOOTSTRAP_DATA_LAST_BIND` | Track successful LDAP authentications in `pwdLastSuccess` | `false` |
| `OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION` | Minimum interval between `pwdLastSuccess` updates, in seconds | `3600` |
| `OPENLDAP_BOOTSTRAP_DATA_ROOT_DN` | Root DN for main database | `cn=admin,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
| `OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED` | Hashed password for main database root DN; generated if empty | `` |
| `OPENLDAP_BOOTSTRAP_DATA_READONLY` | Enable read-only data account creation | `false` |
+2
View File
@@ -22,6 +22,8 @@ After the shared version check, `tests/` runs integration scenarios on each buil
- default bootstrap;
- alternative schemas (`nis`, `virtualMail`);
- last successful binds: disabled by default, precision throttling, failed binds
and compatibility with ppolicy;
- refint empty-group placeholders: default behavior, automatic role creation
and deletion of the last real group member with memberof enabled;
- all features with 3 replicated nodes: all compatible modules and schemas, TLS required,
+48
View File
@@ -0,0 +1,48 @@
package tests
import (
"context"
_ "embed"
"fmt"
"dagger.io/dagger"
)
// LastBind checks successful bind tracking and precision, with and without ppolicy.
func LastBind(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
passwords, err := rootPasswords(ctx, image)
if err != nil {
return err
}
for _, mode := range []string{"default", "enabled", "precise", "ppolicy"} {
settings := env{}
if mode != "default" {
settings["OPENLDAP_BOOTSTRAP_DATA_LAST_BIND"] = "true"
}
if mode == "precise" {
settings["OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION"] = "0"
}
if mode == "ppolicy" {
settings["OPENLDAP_BOOTSTRAP_PPOLICY"] = "true"
}
if err := func() error {
node := "lastbind-" + mode
service, err := startNode(ctx, image, node, merge(passwords, settings), nil, nil)
if err != nil {
return err
}
defer stopNodes(ctx, []*dagger.Service{service})
return check(ctx, image, nil, env{
"TEST_NODES": node,
"TEST_LAST_BIND_MODE": mode,
}, lastBindCheck)
}(); err != nil {
return fmt.Errorf("last bind %s: %w", mode, err)
}
}
return nil
}
//go:embed testdata/lastbind.sh
var lastBindCheck string
+1
View File
@@ -20,6 +20,7 @@ type scenario struct {
var scenarios = []scenario{
{"default bootstrap", Default},
{"alternative schemas", AlternativeSchemas},
{"last successful binds", LastBind},
{"refint empty-group placeholders", RefintNothing},
{"all features with 3 replicated nodes", AllFeatures},
{"backup and restore", BackupRestore},
+67
View File
@@ -0,0 +1,67 @@
#!/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
URI="ldap://${TEST_NODES}:3890"
USER_DN="uid=lastbind-user,${TEST_SUFFIX}"
eventually has_entry "${URI}" "${TEST_SUFFIX}"
expect_rc 0 bind_search "${URI}" "${CONFIG_ADMIN_DN}" \
-b 'olcDatabase={1}mdb,cn=config' -s base olcLastBind olcLastBindPrecision
if [ "${TEST_LAST_BIND_MODE}" = "default" ]; then
expect_output '^olcLastBind: FALSE$'
else
expect_output '^olcLastBind: TRUE$'
fi
if [ "${TEST_LAST_BIND_MODE}" = "precise" ]; then
expect_output '^olcLastBindPrecision: 0$'
else
expect_output '^olcLastBindPrecision: 3600$'
fi
expect_rc 0 bind_modify "${URI}" "${ADMIN_DN}" -a <<LDIF
dn: ${USER_DN}
objectClass: inetOrgPerson
uid: lastbind-user
cn: Last Bind User
sn: User
userPassword: ${TEST_PASSWORD}
LDIF
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${USER_DN}" -s base pwdLastSuccess
expect_no_output '^pwdLastSuccess:'
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w "${TEST_PASSWORD}"
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${USER_DN}" -s base pwdLastSuccess
if [ "${TEST_LAST_BIND_MODE}" = "default" ]; then
expect_no_output '^pwdLastSuccess:'
ok 'successful binds are not tracked by default'
exit 0
fi
expect_output '^pwdLastSuccess: [0-9]{14}Z$'
FIRST_SUCCESS=$(sed -n 's/^pwdLastSuccess: //p' "${OUTPUT_FILE}")
sleep 2
expect_rc 49 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w incorrect-password
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${USER_DN}" -s base pwdLastSuccess
expect_output "^pwdLastSuccess: ${FIRST_SUCCESS}$"
ok 'failed binds do not update pwdLastSuccess'
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w "${TEST_PASSWORD}"
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${USER_DN}" -s base pwdLastSuccess
expect_output '^pwdLastSuccess: [0-9]{14}Z$'
if [ "${TEST_LAST_BIND_MODE}" = "precise" ]; then
expect_no_output "^pwdLastSuccess: ${FIRST_SUCCESS}$"
ok 'zero precision records the next successful bind'
else
expect_output "^pwdLastSuccess: ${FIRST_SUCCESS}$"
ok "last bind (${TEST_LAST_BIND_MODE}) tracks success and throttles updates"
fi
+3
View File
@@ -15,6 +15,9 @@ OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED=""
OPENLDAP_BOOTSTRAP_DATA_DATABASE_MAX_SIZE="10737418240" # 10Go
OPENLDAP_BOOTSTRAP_DATA_LAST_BIND=false
OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION=3600
OPENLDAP_BOOTSTRAP_DATA_ROOT_DN="cn=admin,${OPENLDAP_BOOTSTRAP_SUFFIX}"
OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED=""
@@ -65,7 +65,7 @@ Core OpenLDAP configuration applied on every bootstrap. Files are numbered to co
| `40-frontend.ldif.template` | Frontend database (password hash algorithm) |
| `50-config.ldif.template` | Config database (`olcDatabase=config`) with root DN and credentials |
| `59-config-acl-close.ldif` | Closes config ACLs (deny all) |
| `60-database.ldif.template` | Main data database (MDB backend) with indexes |
| `60-database.ldif.template` | Main data database (MDB backend) with indexes and last successful bind settings |
| `65-database-acl.ldif.template` | Data database ACLs (password self-service, basic attributes readable by authenticated users) |
| `69-database-acl-close.ldif` | Closes data ACLs (deny all) |
| `70-monitor.ldif.template` | Monitor database configuration |
@@ -7,6 +7,8 @@ olcSuffix: ${OPENLDAP_BOOTSTRAP_SUFFIX}
olcRootDN: ${OPENLDAP_BOOTSTRAP_DATA_ROOT_DN}
olcRootPW: ${OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED}
olcDbMaxSize: ${OPENLDAP_BOOTSTRAP_DATA_DATABASE_MAX_SIZE}
olcLastBind: ${OPENLDAP_BOOTSTRAP_DATA_LAST_BIND}
olcLastBindPrecision: ${OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION}
olcDbIndex: objectClass eq
olcDbIndex: uid eq,sub
olcDbIndex: mail eq,sub
@@ -40,6 +40,7 @@ if [ "${TYPE}" = "env" ]; then
done
OPENLDAP_BOOTSTRAP_MONITOR_ENABLED=${OPENLDAP_BOOTSTRAP_MONITOR_ENABLED^^}
OPENLDAP_BOOTSTRAP_DATA_LAST_BIND=${OPENLDAP_BOOTSTRAP_DATA_LAST_BIND^^}
OPENLDAP_BOOTSTRAP_ORGANIZATION_DC=$(get_dn_attr dc "${OPENLDAP_BOOTSTRAP_SUFFIX}")
@@ -47,7 +48,7 @@ if [ "${TYPE}" = "env" ]; then
container log fatal "OPENLDAP_BOOTSTRAP_SUFFIX must start with dc= (got ${OPENLDAP_BOOTSTRAP_SUFFIX})"
fi
prepare_env_file "${ENV_FILE}" OPENLDAP_GROUP_GID OPENLDAP_USER_UID OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED OPENLDAP_BOOTSTRAP_MODULES_LDIF OPENLDAP_BOOTSTRAP_SCHEMAS_LDIF OPENLDAP_BOOTSTRAP_MONITOR_ENABLED OPENLDAP_BOOTSTRAP_ORGANIZATION_DC
prepare_env_file "${ENV_FILE}" OPENLDAP_GROUP_GID OPENLDAP_USER_UID OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED OPENLDAP_BOOTSTRAP_MODULES_LDIF OPENLDAP_BOOTSTRAP_SCHEMAS_LDIF OPENLDAP_BOOTSTRAP_MONITOR_ENABLED OPENLDAP_BOOTSTRAP_DATA_LAST_BIND OPENLDAP_BOOTSTRAP_ORGANIZATION_DC
elif [ "${TYPE}" = "ldif" ]; then
SOURCE_DIR="$2"
@@ -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, base LDIF files |
| `00-base.sh` | always | Core bootstrap: password hashing for root DNs, module and schema setup, last successful bind settings, 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 |