mirror of
https://github.com/osixia/container-openldap.git
synced 2026-11-06 20:48:18 -04:00
feat(ppolicy): add progressive account lockouts
This commit is contained in:
@@ -6,6 +6,8 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
### Added
|
||||
|
||||
- Add `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY` and `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY`
|
||||
for progressive per-account lockouts after failed authentications (disabled by default).
|
||||
- Add `openldap-ctl password hash --file` and `--stdin` to hash secrets without
|
||||
passing their contents as command arguments.
|
||||
- Add `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_IDLE` to reject logins after a
|
||||
|
||||
@@ -414,6 +414,8 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_FAILURE_COUNT_INTERVAL` | Value for `pwdFailureCountInterval` | `300` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT` | Value for `pwdLockout` | `true` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT_DURATION` | Value for `pwdLockoutDuration` | `900` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY` | Initial per-account lockout after a failed bind, in seconds (`0`: disabled) | `0` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY` | Maximum progressive lockout in seconds; requires `0 < MIN_DELAY <= MAX_DELAY` and policy lockout enabled | `0` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_AGE` | Value for `pwdMaxAge` | `7776000` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_IDLE` | Maximum inactivity in seconds (`0`: disabled); requires last bind tracking and policy lockout | `0` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_EXPIRE_WARNING` | Value for `pwdExpireWarning` | `1209600` |
|
||||
|
||||
@@ -33,6 +33,8 @@ After the shared version check, `tests/` runs integration scenarios on each buil
|
||||
and compatibility with ppolicy;
|
||||
- password policy inactivity: disabled by default, active and idle accounts,
|
||||
first logins and missing prerequisites;
|
||||
- progressive password policy lockouts: disabled by default, doubling and cap,
|
||||
recovery, independent accounts and compatibility with the fixed lockout threshold;
|
||||
- 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,
|
||||
|
||||
@@ -62,3 +62,62 @@ func PasswordPolicyIdle(ctx context.Context, client *dagger.Client, image *dagge
|
||||
|
||||
//go:embed testdata/ppolicy-idle.sh
|
||||
var ppolicyIdleCheck string
|
||||
|
||||
// PasswordPolicyDelay checks progressive lockouts and the existing failure threshold.
|
||||
func PasswordPolicyDelay(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := merge(passwords, env{"OPENLDAP_BOOTSTRAP_PPOLICY": "true"})
|
||||
for _, tc := range []struct {
|
||||
settings env
|
||||
message string
|
||||
}{
|
||||
{env{"OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY": "2"}, "Password policy delays must satisfy"},
|
||||
{env{"OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY": "4"}, "Password policy delays must satisfy"},
|
||||
{env{
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY": "4",
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY": "2",
|
||||
}, "Password policy delays must satisfy"},
|
||||
{env{
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY": "2",
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY": "4",
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT": "false",
|
||||
}, "Password policy delays require OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT=true"},
|
||||
} {
|
||||
if _, err := expect(ctx, withEnv(image, merge(base, tc.settings)), false,
|
||||
[]string{tc.message}, "container", "--skip-process"); err != nil {
|
||||
return fmt.Errorf("password policy delay prerequisites: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, mode := range []string{"default", "enabled", "threshold"} {
|
||||
settings := env{}
|
||||
if mode != "default" {
|
||||
settings["OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY"] = "2"
|
||||
settings["OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY"] = "4"
|
||||
}
|
||||
if mode == "threshold" {
|
||||
settings["OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_FAILURE"] = "2"
|
||||
}
|
||||
if err := func() error {
|
||||
node := "ppolicy-delay-" + mode
|
||||
service, err := startNode(ctx, image, node, merge(base, settings), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stopNodes(ctx, []*dagger.Service{service})
|
||||
return check(ctx, image, nil, env{
|
||||
"TEST_NODES": node,
|
||||
"TEST_PPOLICY_DELAY_MODE": mode,
|
||||
}, ppolicyDelayCheck)
|
||||
}(); err != nil {
|
||||
return fmt.Errorf("password policy delay %s: %w", mode, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testdata/ppolicy-delay.sh
|
||||
var ppolicyDelayCheck string
|
||||
|
||||
@@ -26,6 +26,7 @@ var scenarios = []scenario{
|
||||
{"audit logging", AuditLog},
|
||||
{"last successful binds", LastBind},
|
||||
{"password policy inactivity", PasswordPolicyIdle},
|
||||
{"progressive password policy lockouts", PasswordPolicyDelay},
|
||||
{"password helpers", PasswordHelpers},
|
||||
{"refint empty-group placeholders", RefintNothing},
|
||||
{"all features with 3 replicated nodes", AllFeatures},
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#!/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"
|
||||
POLICY_DN="cn=default,ou=policies,${TEST_SUFFIX}"
|
||||
USER_DN="uid=delay-user,${TEST_SUFFIX}"
|
||||
OTHER_DN="uid=other-user,${TEST_SUFFIX}"
|
||||
|
||||
eventually has_entry "${URI}" "${POLICY_DN}"
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${POLICY_DN}" -s base pwdMinDelay pwdMaxDelay
|
||||
if [ "${TEST_PPOLICY_DELAY_MODE}" = default ]; then
|
||||
expect_output '^pwdMinDelay: 0$'
|
||||
expect_output '^pwdMaxDelay: 0$'
|
||||
else
|
||||
expect_output '^pwdMinDelay: 2$'
|
||||
expect_output '^pwdMaxDelay: 4$'
|
||||
fi
|
||||
|
||||
for user in delay-user other-user; do
|
||||
expect_rc 0 bind_modify "${URI}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: uid=${user},${TEST_SUFFIX}
|
||||
objectClass: inetOrgPerson
|
||||
uid: ${user}
|
||||
cn: ${user}
|
||||
sn: User
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
LDIF
|
||||
done
|
||||
|
||||
timestamp_epoch() {
|
||||
local stamp="$1"
|
||||
date -u -d "${stamp:0:4}-${stamp:4:2}-${stamp:6:2} ${stamp:8:2}:${stamp:10:2}:${stamp:12:2}" +%s
|
||||
}
|
||||
|
||||
check_delay() {
|
||||
local expected="$1"
|
||||
local failure_time lockout_end
|
||||
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 pwdFailureTime pwdAccountTmpLockoutEnd
|
||||
failure_time=$(sed -n 's/^pwdFailureTime: //p' "${OUTPUT_FILE}" | sort | tail -1)
|
||||
lockout_end=$(sed -n 's/^pwdAccountTmpLockoutEnd: //p' "${OUTPUT_FILE}")
|
||||
[ -n "${failure_time}" ] && [ -n "${lockout_end}" ] || fail 'missing failure or lockout timestamp'
|
||||
[ "$(( $(timestamp_epoch "${lockout_end}") - $(timestamp_epoch "${failure_time}") ))" -eq "${expected}" ] || fail "expected ${expected}-second lockout"
|
||||
expect_rc 49 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w "${TEST_PASSWORD}"
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${OTHER_DN}" -w "${TEST_PASSWORD}"
|
||||
}
|
||||
|
||||
expire_delay() {
|
||||
# Expire only the temporary lockout, preserving the failure history for progression.
|
||||
expect_rc 0 bind_modify "${URI}" "${ADMIN_DN}" -e relax <<LDIF
|
||||
dn: ${USER_DN}
|
||||
changetype: modify
|
||||
replace: pwdAccountTmpLockoutEnd
|
||||
pwdAccountTmpLockoutEnd: 20000101000000Z
|
||||
LDIF
|
||||
}
|
||||
|
||||
if [ "${TEST_PPOLICY_DELAY_MODE}" = default ]; then
|
||||
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 pwdAccountTmpLockoutEnd
|
||||
expect_no_output '^pwdAccountTmpLockoutEnd:'
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w "${TEST_PASSWORD}"
|
||||
ok 'progressive lockouts are disabled by default'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_delay 2
|
||||
expire_delay
|
||||
|
||||
if [ "${TEST_PPOLICY_DELAY_MODE}" = threshold ]; then
|
||||
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 pwdAccountLockedTime
|
||||
expect_output '^pwdAccountLockedTime: [0-9]{14}Z$'
|
||||
expect_rc 49 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w "${TEST_PASSWORD}"
|
||||
ok 'the fixed lockout still applies when pwdMaxFailure is reached'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_delay 4
|
||||
expire_delay
|
||||
check_delay 4
|
||||
expire_delay
|
||||
ok 'per-account lockouts double and stop at the configured maximum'
|
||||
|
||||
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 pwdFailureTime
|
||||
expect_no_output '^pwdFailureTime:'
|
||||
check_delay 2
|
||||
ok 'successful authentication clears failures and resets the progression'
|
||||
@@ -64,6 +64,8 @@ OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_FAILURE=5
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_FAILURE_COUNT_INTERVAL=300
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT=true
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT_DURATION=900
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY=0
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY=0
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_AGE=7776000
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_IDLE=0
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_EXPIRE_WARNING=1209600
|
||||
|
||||
@@ -81,6 +81,10 @@ ACL files ending in `-close.ldif` append a final `access to * by * none` rule to
|
||||
`pwdMaxIdle` in seconds (`0`: disabled). Positive values require
|
||||
`OPENLDAP_BOOTSTRAP_DATA_LAST_BIND=true` and `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT=true`.
|
||||
|
||||
`pwdMinDelay` and `pwdMaxDelay` configure progressive per-account lockouts after
|
||||
failed binds (`0`/`0`: disabled). Successful authentication resets the progression;
|
||||
`pwdMaxFailure` and `pwdLockoutDuration` still apply when the failure threshold is reached.
|
||||
|
||||
## config/replication
|
||||
|
||||
Syncrepl-based multi-provider replication. Applied when `OPENLDAP_BOOTSTRAP_REPLICATION=true`.
|
||||
|
||||
@@ -16,6 +16,8 @@ pwdMaxFailure: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_FAILURE}
|
||||
pwdFailureCountInterval: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_FAILURE_COUNT_INTERVAL}
|
||||
pwdLockout: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT}
|
||||
pwdLockoutDuration: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT_DURATION}
|
||||
pwdMinDelay: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY}
|
||||
pwdMaxDelay: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY}
|
||||
pwdMaxAge: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_AGE}
|
||||
pwdMaxIdle: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_IDLE}
|
||||
pwdExpireWarning: ${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_EXPIRE_WARNING}
|
||||
|
||||
@@ -20,6 +20,15 @@ if [ "${OPENLDAP_BOOTSTRAP_PPOLICY}" = "true" ]; then
|
||||
if [ "${TYPE}" = "env" ]; then
|
||||
ENV_FILE="$2"
|
||||
|
||||
if [ "${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY}" -ne 0 ] || [ "${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY}" -ne 0 ]; then
|
||||
if [ "${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY}" -le 0 ] || [ "${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY}" -lt "${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY}" ]; then
|
||||
container log fatal "Password policy delays must satisfy 0 < OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY <= OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY, or both must be 0"
|
||||
fi
|
||||
if [ "${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT^^}" != "TRUE" ]; then
|
||||
container log fatal "Password policy delays require OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_LOCKOUT=true"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_IDLE}" -gt 0 ]; then
|
||||
if [ "${OPENLDAP_BOOTSTRAP_DATA_LAST_BIND^^}" != "TRUE" ]; then
|
||||
container log fatal "OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_IDLE requires OPENLDAP_BOOTSTRAP_DATA_LAST_BIND=true"
|
||||
|
||||
@@ -19,7 +19,7 @@ Scripts are executed in numeric order. Most are feature-gated by an environment
|
||||
| `35-auditlog.sh` | `OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED=true` | Configures file-based auditing on the data database |
|
||||
| `40-refint.sh` | `OPENLDAP_BOOTSTRAP_REFINT=true` | Configures the referential integrity overlay and optional replacement DN |
|
||||
| `45-refint-nothing.sh` | `OPENLDAP_BOOTSTRAP_REFINT=true` and `OPENLDAP_BOOTSTRAP_REFINT_NOTHING_DN` non-empty | Creates the passwordless replacement role |
|
||||
| `50-ppolicy.sh` | `OPENLDAP_BOOTSTRAP_PPOLICY=true` | Configures the password policy and checks that a positive max idle has last bind tracking and policy lockout enabled |
|
||||
| `50-ppolicy.sh` | `OPENLDAP_BOOTSTRAP_PPOLICY=true` | Configures the password policy and validates inactivity and progressive lockout prerequisites |
|
||||
| `60-unique.sh` | `OPENLDAP_BOOTSTRAP_UNIQUE=true` | Configures the unique attributes overlay |
|
||||
| `70-memberof.sh` | `OPENLDAP_BOOTSTRAP_MEMBEROF=true` | Configures the memberof overlay |
|
||||
| `80-replication.sh` | `OPENLDAP_BOOTSTRAP_REPLICATION=true` | Sets up multi-provider syncrepl replication (requires 2 to 9 hosts) |
|
||||
|
||||
Reference in New Issue
Block a user