From b1a45fe17b5d7eb90112a67399240168c1995804 Mon Sep 17 00:00:00 2001 From: Bertrand Gouny Date: Mon, 19 Oct 2026 20:10:01 +0200 Subject: [PATCH] feat(bootstrap): expose global request limits --- CHANGELOG.md | 2 + README.md | 5 ++ build/README.md | 2 + build/tests/limits.go | 48 +++++++++++++++ build/tests/test.go | 1 + build/tests/testdata/limits.sh | 60 +++++++++++++++++++ environment/.env.bootstrap | 5 ++ .../openldap-bootstrap/assets/ldif/README.md | 2 +- .../ldif/config/base/00-global.ldif.template | 5 ++ .../assets/scripts/README.md | 2 +- 10 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 build/tests/limits.go create mode 100644 build/tests/testdata/limits.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 64374cf..20d557d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www ### Added +- Expose bootstrap settings for pending request limits, incoming LDAP message sizes + and blocked network write timeouts, preserving OpenLDAP defaults. - 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 diff --git a/README.md b/README.md index 906e155..6264fa9 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,11 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \ | `OPENLDAP_BOOTSTRAP_GLOBAL_SIZE_LIMIT` | Global LDAP size limit | `500` | | `OPENLDAP_BOOTSTRAP_GLOBAL_TIME_LIMIT` | Global LDAP time limit | `120` | | `OPENLDAP_BOOTSTRAP_GLOBAL_IDLE_TIMEOUT` | Global LDAP idle timeout | `3600` | +| `OPENLDAP_BOOTSTRAP_GLOBAL_WRITE_TIMEOUT` | Timeout for blocked network writes, in seconds (`0`: disabled) | `0` | +| `OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING` | Maximum pending requests per anonymous connection | `100` | +| `OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING_AUTH` | Maximum pending requests per authenticated connection | `1000` | +| `OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING` | Maximum incoming LDAP message size for anonymous connections, in bytes | `262143` | +| `OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING_AUTH` | Maximum incoming LDAP message size for authenticated connections, in bytes | `4194303` | | `OPENLDAP_BOOTSTRAP_FRONTEND_PASSWORD_HASH` | Password hash scheme configured in `olcPasswordHash` | `{ARGON2}` | | `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 | `` | diff --git a/build/README.md b/build/README.md index 175c203..33fb8d9 100644 --- a/build/README.md +++ b/build/README.md @@ -21,6 +21,8 @@ own native runner, then publishes them together from their OCI archives After the shared version check, `tests/` runs integration scenarios on each built image: - default bootstrap; +- global request limits: native defaults, custom values and incoming message size + enforcement for anonymous and authenticated connections; - read-only bootstrap templates with projected-volume symlinks, custom overrides and temporary-copy cleanup on success and failure; - TLS certificate renewals: restart slapd on changed contents, ignore identical diff --git a/build/tests/limits.go b/build/tests/limits.go new file mode 100644 index 0000000..936a69f --- /dev/null +++ b/build/tests/limits.go @@ -0,0 +1,48 @@ +package tests + +import ( + "context" + _ "embed" + "fmt" + + "dagger.io/dagger" +) + +// GlobalLimits checks native defaults, custom limits and request size enforcement. +func GlobalLimits(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", "custom"} { + settings := env{} + if mode == "custom" { + settings = env{ + "OPENLDAP_BOOTSTRAP_GLOBAL_WRITE_TIMEOUT": "5", + "OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING": "10", + "OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING_AUTH": "20", + "OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING": "1024", + "OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING_AUTH": "2048", + } + } + if err := func() error { + node := "limits-" + 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_LIMITS_MODE": mode, + }, limitsCheck) + }(); err != nil { + return fmt.Errorf("global limits %s: %w", mode, err) + } + } + return nil +} + +//go:embed testdata/limits.sh +var limitsCheck string diff --git a/build/tests/test.go b/build/tests/test.go index 4788e5f..a96de9f 100644 --- a/build/tests/test.go +++ b/build/tests/test.go @@ -19,6 +19,7 @@ type scenario struct { var scenarios = []scenario{ {"default bootstrap", Default}, + {"global request limits", GlobalLimits}, {"read-only bootstrap templates", ReadOnlyTemplates}, {"alternative schemas", AlternativeSchemas}, {"TLS certificate reload", TLSReload}, diff --git a/build/tests/testdata/limits.sh b/build/tests/testdata/limits.sh new file mode 100644 index 0000000..d390927 --- /dev/null +++ b/build/tests/testdata/limits.sh @@ -0,0 +1,60 @@ +#!/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" + +eventually has_entry "${URI}" "${TEST_SUFFIX}" +expect_rc 0 bind_search "${URI}" "${CONFIG_ADMIN_DN}" -b cn=config -s base \ + olcWriteTimeout olcConnMaxPending olcConnMaxPendingAuth \ + olcSockbufMaxIncoming olcSockbufMaxIncomingAuth + +if [ "${TEST_LIMITS_MODE}" = default ]; then + expect_output '^olcWriteTimeout: 0$' + expect_output '^olcConnMaxPending: 100$' + expect_output '^olcConnMaxPendingAuth: 1000$' + expect_output '^olcSockbufMaxIncoming: 262143$' + expect_output '^olcSockbufMaxIncomingAuth: 4194303$' +else + expect_output '^olcWriteTimeout: 5$' + expect_output '^olcConnMaxPending: 10$' + expect_output '^olcConnMaxPendingAuth: 20$' + expect_output '^olcSockbufMaxIncoming: 1024$' + expect_output '^olcSockbufMaxIncomingAuth: 2048$' +fi +ok "global limits (${TEST_LIMITS_MODE}) are loaded in cn=config" + +# These filters match the root DSE while making each incoming request large enough +# to distinguish the anonymous and authenticated message size limits. +MEDIUM=$(printf '%1400s' '' | tr ' ' x) +LARGE=$(printf '%2800s' '' | tr ' ' x) +MEDIUM_FILTER="(|(objectClass=*)(description=${MEDIUM}))" +LARGE_FILTER="(|(objectClass=*)(description=${LARGE}))" + +if [ "${TEST_LIMITS_MODE}" = default ]; then + expect_rc 0 ldapsearch -LLL -x -H "${URI}" -b '' -s base "${LARGE_FILTER}" namingContexts + expect_output '^namingContexts:' + expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b '' -s base "${LARGE_FILTER}" namingContexts + expect_output '^namingContexts:' + ok 'native message size defaults allow ordinary LDAP requests' + exit 0 +fi + +expect_rc 0 ldapsearch -LLL -x -H "${URI}" -b '' -s base '(objectClass=*)' namingContexts +expect_output '^namingContexts:' +expect_fail ldapsearch -LLL -x -H "${URI}" -b '' -s base "${MEDIUM_FILTER}" namingContexts +expect_output "Can't contact LDAP server" +expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b '' -s base "${MEDIUM_FILTER}" namingContexts +expect_output '^namingContexts:' +expect_fail bind_search "${URI}" "${ADMIN_DN}" -b '' -s base "${LARGE_FILTER}" namingContexts +expect_output "Can't contact LDAP server" +expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${TEST_SUFFIX}" -s base dn +expect_output '^dn:' +ok 'oversized messages close the offending connection and preserve normal access' diff --git a/environment/.env.bootstrap b/environment/.env.bootstrap index 5de2963..f83c9a4 100644 --- a/environment/.env.bootstrap +++ b/environment/.env.bootstrap @@ -7,6 +7,11 @@ OPENLDAP_BOOTSTRAP_SCHEMAS="core.ldif cosine.ldif inetorgperson.ldif rfc2307bis. OPENLDAP_BOOTSTRAP_GLOBAL_SIZE_LIMIT=500 OPENLDAP_BOOTSTRAP_GLOBAL_TIME_LIMIT=120 OPENLDAP_BOOTSTRAP_GLOBAL_IDLE_TIMEOUT=3600 +OPENLDAP_BOOTSTRAP_GLOBAL_WRITE_TIMEOUT=0 +OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING=100 +OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING_AUTH=1000 +OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING=262143 +OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING_AUTH=4194303 OPENLDAP_BOOTSTRAP_FRONTEND_PASSWORD_HASH="{ARGON2}" diff --git a/services/openldap-bootstrap/assets/ldif/README.md b/services/openldap-bootstrap/assets/ldif/README.md index 99e1bfc..d2e8ca6 100644 --- a/services/openldap-bootstrap/assets/ldif/README.md +++ b/services/openldap-bootstrap/assets/ldif/README.md @@ -59,7 +59,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) | +| `00-global.ldif.template` | Global search limits, idle/write timeouts, pending requests and incoming message sizes | | `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 | diff --git a/services/openldap-bootstrap/assets/ldif/config/base/00-global.ldif.template b/services/openldap-bootstrap/assets/ldif/config/base/00-global.ldif.template index 73d4c75..4450885 100644 --- a/services/openldap-bootstrap/assets/ldif/config/base/00-global.ldif.template +++ b/services/openldap-bootstrap/assets/ldif/config/base/00-global.ldif.template @@ -4,3 +4,8 @@ cn: config olcSizeLimit: ${OPENLDAP_BOOTSTRAP_GLOBAL_SIZE_LIMIT} olcTimeLimit: ${OPENLDAP_BOOTSTRAP_GLOBAL_TIME_LIMIT} olcIdleTimeout: ${OPENLDAP_BOOTSTRAP_GLOBAL_IDLE_TIMEOUT} +olcWriteTimeout: ${OPENLDAP_BOOTSTRAP_GLOBAL_WRITE_TIMEOUT} +olcConnMaxPending: ${OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING} +olcConnMaxPendingAuth: ${OPENLDAP_BOOTSTRAP_GLOBAL_CONN_MAX_PENDING_AUTH} +olcSockbufMaxIncoming: ${OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING} +olcSockbufMaxIncomingAuth: ${OPENLDAP_BOOTSTRAP_GLOBAL_SOCKBUF_MAX_INCOMING_AUTH} diff --git a/services/openldap-bootstrap/assets/scripts/README.md b/services/openldap-bootstrap/assets/scripts/README.md index 55d56b1..e36efeb 100644 --- a/services/openldap-bootstrap/assets/scripts/README.md +++ b/services/openldap-bootstrap/assets/scripts/README.md @@ -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: root passwords, automatic or explicit modules, schemas and base LDIF files | +| `00-base.sh` | always | Core bootstrap: root passwords, modules, schemas and base LDIF files, including global request limits | | `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 |