mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valera V Harseko
2 days ago de3526645a13633ef6779dfb281255f9f3641fca
Benchmark: migrate OpenLDAP to vegardit (2.6), hash with {SSHA}

Switch the OpenLDAP side from osixia/openldap (OpenLDAP 2.4.57, unmaintained) to
vegardit/docker-openldap (OpenLDAP 2.6.10).

- Adapt the setup to vegardit's interface: `LDAP_INIT_ORG_DN`,
`LDAP_INIT_ROOT_USER_DN` (override to `cn=admin,<base>`), `LDAP_INIT_ROOT_USER_PW`,
disable TLS/LDAPS, and neutralize the image's built-in ppolicy friction
(lockout, pqChecker, min length) for the benchmark.
- vegardit ships no SHA-2 module, so use `{SSHA}` (Salted SHA-1) instead of
`{SSHA256}`: it is OpenLDAP core and a built-in OpenDJ scheme, so both servers
still hash identically. Set `olcPasswordHash {SSHA}` and enable hash-cleartext on
vegardit's already-loaded ppolicy overlay (no module load or restart needed);
set OpenDJ's default storage scheme to Salted SHA-1. cn=config edits go via
EXTERNAL over ldapi as root.
- `mail` is still equality-indexed by default on vegardit (`uid,mail`), so the
indexed SEARCH-on-mail comparison remains fair.
3 files modified
59 ■■■■ changed files
.github/benchmark/benchmark.jmx 2 ●●● patch | view | raw | blame | history
.github/benchmark/summary.sh 4 ●●●● patch | view | raw | blame | history
.github/workflows/benchmark.yml 53 ●●●● patch | view | raw | blame | history
.github/benchmark/benchmark.jmx
@@ -17,7 +17,7 @@
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="OpenDJ vs OpenLDAP - LDAP benchmark" enabled="true">
      <stringProp name="TestPlan.comments">Parametrized LDAP benchmark. Admin bind is cached once per thread (Once Only Controller, labelled ADMIN_CONNECT, excluded from metrics). Data ops (ADD/SEARCH/COMPARE/MODIFY/DELETE/READD) reuse the cached admin connection. Entries use mail as the naming/searchable attribute (equality-indexed by default on BOTH OpenDJ and OpenLDAP/osixia); no cn/sn/uid/givenName/telephoneNumber/member/uniqueMember are stored (those are indexed on OpenDJ but not osixia, which would bias the write cost). Every created value is unique: ADD uses a per-iteration counter, READD uses a UUID. The measured user authentication is a single bind/unbind (test=sbind, own connection) after MODIFY has set the userPassword.</stringProp>
      <stringProp name="TestPlan.comments">Parametrized LDAP benchmark. Admin bind is cached once per thread (Once Only Controller, labelled ADMIN_CONNECT, excluded from metrics). Data ops (ADD/SEARCH/COMPARE/MODIFY/DELETE/READD) reuse the cached admin connection. Entries use mail as the naming/searchable attribute (equality-indexed by default on BOTH OpenDJ and OpenLDAP); only mail + objectClass (both indexed on both servers) are stored, keeping the write cost symmetric. Every created value is unique: ADD uses a per-iteration counter, READD uses a UUID. The measured user authentication is a single bind/unbind (test=sbind, own connection) after MODIFY has set the userPassword.</stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
.github/benchmark/summary.sh
@@ -130,6 +130,6 @@
echo "  own connection) as \`cn=user_<n>,ou=People\` with the password set by \`MODIFY\`. The admin"
echo "  connection bind (\`ADMIN_CONNECT\`) is cached once per thread and excluded from these results."
echo "- MODIFY sends the password in cleartext; each server hashes it on write with the **same"
echo "  scheme (SSHA-256)** — OpenLDAP via the pw-sha2 module + ppolicy hash-cleartext, OpenDJ via"
echo "  its Salted SHA-256 default scheme — so BIND authentication is compared on equal footing."
echo "  scheme (SSHA, Salted SHA-1)** — OpenLDAP via the ppolicy hash-cleartext overlay, OpenDJ via"
echo "  its Salted SHA-1 default scheme — so BIND authentication is compared on equal footing."
echo "- Full interactive JMeter HTML dashboards are attached as the \`jmeter-reports\` artifact."
.github/workflows/benchmark.yml
@@ -25,7 +25,7 @@
    inputs:
      openldap_image:
        description: "OpenLDAP Docker image"
        default: "osixia/openldap:latest"
        default: "vegardit/docker-openldap:latest"
      opendj_image:
        description: "OpenDJ Docker image"
        default: "openidentityplatform/opendj:latest"
@@ -63,7 +63,7 @@
    runs-on: ubuntu-latest
    env:
      # `${{ inputs.X || 'default' }}` so workflow_run (which carries no inputs) falls back.
      OPENLDAP_IMAGE: ${{ inputs.openldap_image || 'osixia/openldap:latest' }}
      OPENLDAP_IMAGE: ${{ inputs.openldap_image || 'vegardit/docker-openldap:latest' }}
      OPENDJ_IMAGE: ${{ inputs.opendj_image || 'openidentityplatform/opendj:latest' }}
      THREADS: ${{ inputs.threads || '200' }}
      DURATION: ${{ inputs.duration || '300' }}
@@ -97,13 +97,18 @@
      - name: Start OpenLDAP
        run: |
          docker run -d --name openldap -p 2389:389 \
            -e LDAP_ORGANISATION="Example" \
            -e LDAP_DOMAIN="example.com" \
            -e LDAP_ADMIN_PASSWORD="password" \
            -e LDAP_TLS=false \
            -e LDAP_INIT_ORG_NAME="Example" \
            -e LDAP_INIT_ORG_DN="$BASEDN" \
            -e LDAP_INIT_ROOT_USER_DN="cn=admin,$BASEDN" \
            -e LDAP_INIT_ROOT_USER_PW="password" \
            -e LDAP_TLS_ENABLED=false \
            -e LDAP_LDAPS_ENABLED=false \
            -e LDAP_INIT_PPOLICY_PW_MIN_LENGTH=1 \
            -e LDAP_INIT_PPOLICY_MAX_FAILURES=0 \
            -e LDAP_PPOLICY_PQCHECKER_RULE="0|00000000" \
            "$OPENLDAP_IMAGE"
      - name: Configure OpenLDAP (SSHA-256 hash-on-write, seed)
      - name: Configure OpenLDAP (SSHA hash-on-write, seed)
        run: |
          wait_ldap() {
            for i in $(seq 1 90); do
@@ -112,23 +117,17 @@
              sleep 2
            done
          }
          le() { docker exec -i openldap "$@" -Y EXTERNAL -H ldapi:/// >/dev/null 2>&1 || true; }
          # cn=config edits via EXTERNAL over ldapi as root (-u 0).
          le() { docker exec -u 0 -i openldap ldapmodify -Y EXTERNAL -H ldapi:/// >/dev/null 2>&1 || true; }
          wait_ldap
          # Load pw-sha2 (provides {SSHA256}) and ppolicy (overlay) modules; osixia ships both
          # in /usr/lib/ldap. Create the module list entry or append the values, then restart so
          # the modules are active in the running slapd.
          printf 'dn: cn=module{0},cn=config\nobjectClass: olcModuleList\nolcModuleLoad: pw-sha2\nolcModuleLoad: ppolicy\n' | le ldapadd
          printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: pw-sha2\n' | le ldapmodify
          printf 'dn: cn=module{0},cn=config\nchangetype: modify\nadd: olcModuleLoad\nolcModuleLoad: ppolicy\n' | le ldapmodify
          docker restart openldap
          wait_ldap
          # Make slapd hash cleartext userPassword with {SSHA256} on a plain modify: set the global
          # password-hash and enable the ppolicy overlay's hash_cleartext on the mdb database.
          printf 'dn: olcDatabase={-1}frontend,cn=config\nchangetype: modify\nreplace: olcPasswordHash\nolcPasswordHash: {SSHA256}\n' | le ldapmodify
          DBDN="$(docker exec openldap ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config '(olcDatabase=*mdb)' dn 2>/dev/null | sed -n 's/^dn: //p' | head -1)"
          [ -n "$DBDN" ] || DBDN="olcDatabase={1}mdb,cn=config"
          printf 'dn: olcOverlay=ppolicy,%s\nobjectClass: olcOverlayConfig\nobjectClass: olcPPolicyConfig\nolcOverlay: ppolicy\nolcPPolicyHashCleartext: TRUE\n' "$DBDN" | le ldapadd
          # Seed ou=People (after the restart so it survives any re-init).
          # This image ships no SHA-2 module, so use {SSHA} (Salted SHA-1, OpenLDAP core) — also a
          # built-in OpenDJ scheme, so both servers hash identically. Set it as the global hash.
          printf 'dn: olcDatabase={-1}frontend,cn=config\nchangetype: modify\nreplace: olcPasswordHash\nolcPasswordHash: {SSHA}\n' | le
          # The image already loads the ppolicy overlay; enable hash-cleartext on it so a plain
          # admin modify of a cleartext userPassword is hashed with {SSHA} on write.
          PPDN="$(docker exec -u 0 openldap ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config '(olcOverlay=*ppolicy)' dn 2>/dev/null | sed -n 's/^dn: //p' | head -1)"
          [ -z "$PPDN" ] || printf 'dn: %s\nchangetype: modify\nreplace: olcPPolicyHashCleartext\nolcPPolicyHashCleartext: TRUE\n' "$PPDN" | le
          # Seed ou=People.
          ldapadd -x -H ldap://localhost:2389 -D "cn=admin,$BASEDN" -w password \
            -f .github/benchmark/people.ldif || true
@@ -163,7 +162,7 @@
        run: |
          mkdir -p logs/openldap
          docker logs openldap > logs/openldap/server.log 2>&1 || true
          # osixia mostly logs to stdout (captured above); grab the in-container /var/log too.
          # slapd mostly logs to stdout (captured above); grab the in-container /var/log too.
          docker cp openldap:/var/log logs/openldap/var-log 2>/dev/null || true
          tail -n 100 logs/openldap/server.log || true
          docker rm -f openldap || true
@@ -188,12 +187,12 @@
          ldapadd -x -H ldap://localhost:1389 -D "cn=Directory Manager" -w password \
            -f .github/benchmark/people.ldif || true
      - name: Configure OpenDJ password policy (SSHA-256 hash-on-write)
      - name: Configure OpenDJ password policy (SSHA hash-on-write)
        run: |
          # Hash cleartext userPassword with Salted SHA-256 on write, matching OpenLDAP.
          # Hash cleartext userPassword with Salted SHA-1 on write, matching OpenLDAP {SSHA}.
          docker exec opendj /opt/opendj/bin/dsconfig set-password-policy-prop \
            --policy-name "Default Password Policy" \
            --set default-password-storage-scheme:"Salted SHA-256" \
            --set default-password-storage-scheme:"Salted SHA-1" \
            --hostname localhost --port 4444 \
            --bindDN "cn=Directory Manager" --bindPassword password \
            --trustAll --no-prompt