diff --git a/CHANGELOG.md b/CHANGELOG.md index 56357d2..64374cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 59dc01d..906e155 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/build/README.md b/build/README.md index 95bc4a9..175c203 100644 --- a/build/README.md +++ b/build/README.md @@ -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, diff --git a/build/tests/ppolicy.go b/build/tests/ppolicy.go index b1e48b4..1b9b218 100644 --- a/build/tests/ppolicy.go +++ b/build/tests/ppolicy.go @@ -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 diff --git a/build/tests/test.go b/build/tests/test.go index 0b3abf5..4788e5f 100644 --- a/build/tests/test.go +++ b/build/tests/test.go @@ -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}, diff --git a/build/tests/testdata/ppolicy-delay.sh b/build/tests/testdata/ppolicy-delay.sh new file mode 100644 index 0000000..aa9281d --- /dev/null +++ b/build/tests/testdata/ppolicy-delay.sh @@ -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 <