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

Valery Kharseko
9 hours ago eea0e09b6dae634ff95f0862aeff45a1a45c7d2f
[#1092] Probe the Docker container's health without binding as the root user (#1102)
5 files modified
1 files added
234 ■■■■■ changed files
.github/workflows/build.yml 136 ●●●●● patch | view | raw | blame | history
opendj-packages/opendj-docker/Dockerfile 14 ●●●●● patch | view | raw | blame | history
opendj-packages/opendj-docker/Dockerfile-alpine 14 ●●●●● patch | view | raw | blame | history
opendj-packages/opendj-docker/README.md 24 ●●●●● patch | view | raw | blame | history
opendj-packages/opendj-docker/healthcheck.sh 44 ●●●●● patch | view | raw | blame | history
opendj-packages/opendj-docker/pom.xml 2 ●●●●● patch | view | raw | blame | history
.github/workflows/build.yml
@@ -586,10 +586,10 @@
          cleanup() { docker rm -f test_master $REPLICAS >/dev/null 2>&1 || true; docker network rm test_replication >/dev/null 2>&1 || true; }
          cleanup
          trap 'code=$?; for c in test_master $REPLICAS; do echo "::group::container logs ($c)"; docker logs $c 2>&1 || true; echo "::endgroup::"; done; cleanup; exit $code' ERR
          # every tool reads the root password from a file (#1084); dsreplication run with -n prints
          # every tool reads the root password from a file (#1084, #1092); dsreplication run with -n prints
          # no command line, so a password put back on one would pass every check below
          rc=0; docker run --rm --entrypoint grep "$IMAGE" -nE -- '(^|[[:space:]])(-w|--(bindPassword[12]?|adminPassword))([[:space:]=]|$)' /opt/opendj/bootstrap/replicate.sh || rc=$?
          if [ $rc -ne 1 ]; then echo "::error::replicate.sh passes the root password on a command line, or grep could not read it"; false; fi
          rc=0; docker run --rm --entrypoint grep "$IMAGE" -nE -- '(^|[[:space:]])(-w|--(bindPassword[12]?|adminPassword|rootUserPassword))([[:space:]=]|$)' /opt/opendj/bootstrap/setup.sh /opt/opendj/bootstrap/replicate.sh || rc=$?
          if [ $rc -ne 1 ]; then echo "::error::setup.sh or replicate.sh passes the root password on a command line, or grep could not read them"; false; fi
          # the password file goes to /dev/shm, off the writable layer of the container, and the mktemp of the image puts it there
          docker run --rm --entrypoint grep "$IMAGE" -qF -- 'mktemp -p /dev/shm "opendj-replicate.$ADMIN_PORT.' /opt/opendj/bootstrap/replicate.sh || { echo "::error::replicate.sh no longer puts the password file on /dev/shm"; false; }
          docker run --rm --entrypoint sh "$IMAGE" -c 'f=$(mktemp -p /dev/shm "opendj-replicate.$ADMIN_PORT.XXXXXX") && rm -f "$f" && case $f in /dev/shm/opendj-replicate.4444.*) ;; *) exit 1;; esac' || { echo "::error::mktemp in the image does not create the password file on /dev/shm"; false; }
@@ -636,13 +636,14 @@
          if [ $rc -ne 5 ] || grep -qE "trying again|initializing replication" <<<"$out"; then
            echo "$out"; echo "::error::a second replicate.sh exited with $rc, not with the 5 of its dsreplication enable, or went on after it"; false
          fi
          # the root password shows in no container log, and the file replicate.sh passed it in is gone (#1084)
          # the root password shows in no container log, and the files setup.sh and replicate.sh passed it in are gone (#1084, #1092)
          for c in test_master $REPLICAS; do
            if docker logs $c 2>&1 | grep -F "$ROOT_PASSWORD"; then echo "::error::The root password is in the log of $c"; false; fi
          done
          for c in $REPLICAS; do
            # the JVM of the HEALTHCHECK's ldapsearch keeps its command line, root password included, in /tmp/hsperfdata_* while it runs
            left=$(docker exec $c grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm | grep -v '^/tmp/hsperfdata_' || true)
          for c in test_master $REPLICAS; do
            # a JVM keeps its command line in /tmp/hsperfdata_* while it runs; the HEALTHCHECK no longer binds as root (#1092),
            # so no process left running has the root password on it
            left=$(docker exec $c grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm || true)
            if [ -n "$left" ]; then echo "::error::The root password is left in $left of $c"; false; fi
          done
          docker exec test_replica test -e /dev/shm/opendj-replicate.5444.other || { echo "::error::run.sh of test_replica removed the password file of another container"; false; }
@@ -705,6 +706,59 @@
          done
          docker exec test_bootstrap test -e "$shm_other" || { echo "::error::run.sh removed $shm_other, the password file of another container"; false; }
          docker kill test_bootstrap test_bootstrap_shm
      - name: Docker test health check
        shell: bash
        run: |
          # the ERR trap below has to fire for a check failing inside stays_healthy too
          set -o errtrace
          # the containers are run without --rm, so that one whose bootstrap failed is still there for the trap to print
          trap 'code=$?; for c in test_health test_health_bind; do echo "::group::container logs ($c)"; docker logs $c 2>&1 || true; docker inspect --format="{{json .State.Health}}" $c 2>&1 || true; echo "::endgroup::"; done; docker rm -f test_health test_health_bind >/dev/null 2>&1 || true; exit $code' ERR
          IMAGE=localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}
          # a failed probe leaves a failing streak until the next probe passes, 5 s later at the
          # earliest, so a container found "healthy 0" every 2 s for 45 s passed every probe since
          stays_healthy() {
            local end=$((SECONDS + 45))
            while [ $SECONDS -lt $end ]; do
              test "$(docker inspect --format='{{.State.Health.Status}} {{.State.Health.FailingStreak}}' "$1")" = "healthy 0"
              sleep 2
            done
          }
          # ROOT_PASSWORD is only the initial root password: changing it must not turn the container unhealthy
          docker run -it -d --memory="512m" --health-interval=5s -e ROOT_PASSWORD=initial_password --name=test_health $IMAGE
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_health | grep -q \"healthy\"; do sleep 10; done'
          docker exec test_health /opt/opendj/bin/ldappasswordmodify --hostname localhost --port 1636 --useSsl --trustAll --bindDN "cn=Directory Manager" --bindPassword initial_password --currentPassword initial_password --newPassword rotated_password
          # bind settings kept for the CLI in the home of the image user must not reach the probe
          docker exec test_health sh -c 'mkdir -p /home/opendj/.opendj && printf "bindDN=cn=Directory Manager\nbindPassword=wrong_password\n" > /home/opendj/.opendj/tools.properties'
          stays_healthy test_health
          docker rm -f test_health
          # an instance rejecting unauthenticated requests is probed with the account it is given, whose password is read from a file
          # a password of its own, so the command lines below can be searched for it
          printf hc_secret_1092 > "$RUNNER_TEMP/healthcheck_password"
          chmod 644 "$RUNNER_TEMP/healthcheck_password"
          docker run -it -d --memory="512m" --health-interval=5s -v "$RUNNER_TEMP/healthcheck_password:/tmp/healthcheck_password:ro" -e ROOT_PASSWORD=hc_secret_1092 -e HEALTHCHECK_BIND_DN="cn=Directory Manager" -e HEALTHCHECK_BIND_PASSWORD_FILE=/tmp/healthcheck_password --name=test_health_bind $IMAGE
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_health_bind | grep -q \"healthy\"; do sleep 10; done'
          docker exec test_health_bind /opt/opendj/bin/dsconfig set-global-configuration-prop --hostname localhost --port 4444 --bindDN "cn=Directory Manager" --bindPasswordFile /tmp/healthcheck_password --set reject-unauthenticated-requests:true --no-prompt --trustAll
          # the setting has taken: the anonymous probe would now be refused
          rc=0
          docker exec test_health_bind /opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || rc=$?
          test "$rc" = 53
          # and the probe reports that refusal when it has no account to bind with
          rc=0
          docker exec -e HEALTHCHECK_BIND_DN= test_health_bind /opt/opendj/healthcheck.sh || rc=$?
          test "$rc" = 1
          # a password file it cannot read is reported as such, whether it is missing or there but not
          # readable: the image runs as its own user, not root, so mode 000 keeps the probe out
          docker exec test_health_bind sh -c 'touch /tmp/unreadable_password && chmod 000 /tmp/unreadable_password'
          for f in /nonexistent /tmp/unreadable_password; do
            rc=0
            out=$(docker exec -e HEALTHCHECK_BIND_PASSWORD_FILE=$f test_health_bind /opt/opendj/healthcheck.sh) || rc=$?
            test "$rc" = 1
            grep -q 'is not a readable file' <<< "$out"
          done
          stays_healthy test_health_bind
          # the password never shows on a command line: sample every process's for two probe intervals
          docker exec test_health_bind sh -c 'end=$(($(date +%s) + 12)); while [ "$(date +%s)" -lt "$end" ]; do for f in /proc/[0-9]*/cmdline; do tr "\0" " " < "$f" 2>/dev/null; echo; done | grep -q "[h]c_secret_1092" && exit 1; sleep 0.2; done; exit 0'
          docker rm -f test_health_bind
      - name: Scan image for vulnerabilities (Trivy)
        # trivy resolves the image from the local Docker daemon, so only the runner's
        # linux/amd64 manifest is scanned; cache: false keeps the ~1GB trivy DBs from
@@ -894,10 +948,10 @@
          cleanup() { docker rm -f test_master $REPLICAS >/dev/null 2>&1 || true; docker network rm test_replication >/dev/null 2>&1 || true; }
          cleanup
          trap 'code=$?; for c in test_master $REPLICAS; do echo "::group::container logs ($c)"; docker logs $c 2>&1 || true; echo "::endgroup::"; done; cleanup; exit $code' ERR
          # every tool reads the root password from a file (#1084); dsreplication run with -n prints
          # every tool reads the root password from a file (#1084, #1092); dsreplication run with -n prints
          # no command line, so a password put back on one would pass every check below
          rc=0; docker run --rm --entrypoint grep "$IMAGE" -nE -- '(^|[[:space:]])(-w|--(bindPassword[12]?|adminPassword))([[:space:]=]|$)' /opt/opendj/bootstrap/replicate.sh || rc=$?
          if [ $rc -ne 1 ]; then echo "::error::replicate.sh passes the root password on a command line, or grep could not read it"; false; fi
          rc=0; docker run --rm --entrypoint grep "$IMAGE" -nE -- '(^|[[:space:]])(-w|--(bindPassword[12]?|adminPassword|rootUserPassword))([[:space:]=]|$)' /opt/opendj/bootstrap/setup.sh /opt/opendj/bootstrap/replicate.sh || rc=$?
          if [ $rc -ne 1 ]; then echo "::error::setup.sh or replicate.sh passes the root password on a command line, or grep could not read them"; false; fi
          # the password file goes to /dev/shm, off the writable layer of the container, and the mktemp of the image puts it there
          docker run --rm --entrypoint grep "$IMAGE" -qF -- 'mktemp -p /dev/shm "opendj-replicate.$ADMIN_PORT.' /opt/opendj/bootstrap/replicate.sh || { echo "::error::replicate.sh no longer puts the password file on /dev/shm"; false; }
          docker run --rm --entrypoint sh "$IMAGE" -c 'f=$(mktemp -p /dev/shm "opendj-replicate.$ADMIN_PORT.XXXXXX") && rm -f "$f" && case $f in /dev/shm/opendj-replicate.4444.*) ;; *) exit 1;; esac' || { echo "::error::mktemp in the image does not create the password file on /dev/shm"; false; }
@@ -944,13 +998,14 @@
          if [ $rc -ne 5 ] || grep -qE "trying again|initializing replication" <<<"$out"; then
            echo "$out"; echo "::error::a second replicate.sh exited with $rc, not with the 5 of its dsreplication enable, or went on after it"; false
          fi
          # the root password shows in no container log, and the file replicate.sh passed it in is gone (#1084)
          # the root password shows in no container log, and the files setup.sh and replicate.sh passed it in are gone (#1084, #1092)
          for c in test_master $REPLICAS; do
            if docker logs $c 2>&1 | grep -F "$ROOT_PASSWORD"; then echo "::error::The root password is in the log of $c"; false; fi
          done
          for c in $REPLICAS; do
            # the JVM of the HEALTHCHECK's ldapsearch keeps its command line, root password included, in /tmp/hsperfdata_* while it runs
            left=$(docker exec $c grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm | grep -v '^/tmp/hsperfdata_' || true)
          for c in test_master $REPLICAS; do
            # a JVM keeps its command line in /tmp/hsperfdata_* while it runs; the HEALTHCHECK no longer binds as root (#1092),
            # so no process left running has the root password on it
            left=$(docker exec $c grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm || true)
            if [ -n "$left" ]; then echo "::error::The root password is left in $left of $c"; false; fi
          done
          docker exec test_replica test -e /dev/shm/opendj-replicate.5444.other || { echo "::error::run.sh of test_replica removed the password file of another container"; false; }
@@ -1013,6 +1068,59 @@
          done
          docker exec test_bootstrap test -e "$shm_other" || { echo "::error::run.sh removed $shm_other, the password file of another container"; false; }
          docker kill test_bootstrap test_bootstrap_shm
      - name: Docker test health check
        shell: bash
        run: |
          # the ERR trap below has to fire for a check failing inside stays_healthy too
          set -o errtrace
          # the containers are run without --rm, so that one whose bootstrap failed is still there for the trap to print
          trap 'code=$?; for c in test_health test_health_bind; do echo "::group::container logs ($c)"; docker logs $c 2>&1 || true; docker inspect --format="{{json .State.Health}}" $c 2>&1 || true; echo "::endgroup::"; done; docker rm -f test_health test_health_bind >/dev/null 2>&1 || true; exit $code' ERR
          IMAGE=localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine
          # a failed probe leaves a failing streak until the next probe passes, 5 s later at the
          # earliest, so a container found "healthy 0" every 2 s for 45 s passed every probe since
          stays_healthy() {
            local end=$((SECONDS + 45))
            while [ $SECONDS -lt $end ]; do
              test "$(docker inspect --format='{{.State.Health.Status}} {{.State.Health.FailingStreak}}' "$1")" = "healthy 0"
              sleep 2
            done
          }
          # ROOT_PASSWORD is only the initial root password: changing it must not turn the container unhealthy
          docker run -it -d --memory="1g" --health-interval=5s -e ROOT_PASSWORD=initial_password --name=test_health $IMAGE
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_health | grep -q \"healthy\"; do sleep 10; done'
          docker exec test_health /opt/opendj/bin/ldappasswordmodify --hostname localhost --port 1636 --useSsl --trustAll --bindDN "cn=Directory Manager" --bindPassword initial_password --currentPassword initial_password --newPassword rotated_password
          # bind settings kept for the CLI in the home of the image user must not reach the probe
          docker exec test_health sh -c 'mkdir -p /home/opendj/.opendj && printf "bindDN=cn=Directory Manager\nbindPassword=wrong_password\n" > /home/opendj/.opendj/tools.properties'
          stays_healthy test_health
          docker rm -f test_health
          # an instance rejecting unauthenticated requests is probed with the account it is given, whose password is read from a file
          # a password of its own, so the command lines below can be searched for it
          printf hc_secret_1092 > "$RUNNER_TEMP/healthcheck_password"
          chmod 644 "$RUNNER_TEMP/healthcheck_password"
          docker run -it -d --memory="1g" --health-interval=5s -v "$RUNNER_TEMP/healthcheck_password:/tmp/healthcheck_password:ro" -e ROOT_PASSWORD=hc_secret_1092 -e HEALTHCHECK_BIND_DN="cn=Directory Manager" -e HEALTHCHECK_BIND_PASSWORD_FILE=/tmp/healthcheck_password --name=test_health_bind $IMAGE
          timeout 3m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_health_bind | grep -q \"healthy\"; do sleep 10; done'
          docker exec test_health_bind /opt/opendj/bin/dsconfig set-global-configuration-prop --hostname localhost --port 4444 --bindDN "cn=Directory Manager" --bindPasswordFile /tmp/healthcheck_password --set reject-unauthenticated-requests:true --no-prompt --trustAll
          # the setting has taken: the anonymous probe would now be refused
          rc=0
          docker exec test_health_bind /opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || rc=$?
          test "$rc" = 53
          # and the probe reports that refusal when it has no account to bind with
          rc=0
          docker exec -e HEALTHCHECK_BIND_DN= test_health_bind /opt/opendj/healthcheck.sh || rc=$?
          test "$rc" = 1
          # a password file it cannot read is reported as such, whether it is missing or there but not
          # readable: the image runs as its own user, not root, so mode 000 keeps the probe out
          docker exec test_health_bind sh -c 'touch /tmp/unreadable_password && chmod 000 /tmp/unreadable_password'
          for f in /nonexistent /tmp/unreadable_password; do
            rc=0
            out=$(docker exec -e HEALTHCHECK_BIND_PASSWORD_FILE=$f test_health_bind /opt/opendj/healthcheck.sh) || rc=$?
            test "$rc" = 1
            grep -q 'is not a readable file' <<< "$out"
          done
          stays_healthy test_health_bind
          # the password never shows on a command line: sample every process's for two probe intervals
          docker exec test_health_bind sh -c 'end=$(($(date +%s) + 12)); while [ "$(date +%s)" -lt "$end" ]; do for f in /proc/[0-9]*/cmdline; do tr "\0" " " < "$f" 2>/dev/null; echo; done | grep -q "[h]c_secret_1092" && exit 1; sleep 0.2; done; exit 0'
          docker rm -f test_health_bind
      - name: Scan image for vulnerabilities (Trivy)
        # trivy resolves the image from the local Docker daemon, so only the runner's
        # linux/amd64 manifest is scanned; cache: false keeps the ~1GB trivy DBs from
opendj-packages/opendj-docker/Dockerfile
@@ -65,8 +65,9 @@
# root. The scripts copied below are only read and run, so they just keep the same group.
COPY --chown=$OPENDJ_USER:0 bootstrap/ /opt/opendj/bootstrap/
COPY --chown=$OPENDJ_USER:0 run.sh /opt/opendj/run.sh
COPY --chown=$OPENDJ_USER:0 healthcheck.sh /opt/opendj/healthcheck.sh
RUN chmod +x /opt/opendj/run.sh /opt/opendj/bootstrap/setup.sh /opt/opendj/bootstrap/replicate.sh
RUN chmod +x /opt/opendj/run.sh /opt/opendj/healthcheck.sh /opt/opendj/bootstrap/setup.sh /opt/opendj/bootstrap/replicate.sh
EXPOSE $PORT/tcp $LDAPS_PORT/tcp $ADMIN_PORT/tcp
@@ -75,10 +76,11 @@
# "healthy" has to mean the instance is ready to serve, not just that it answers: setup
# starts the server in the middle of the bootstrap, before the backend of BASE_DN is
# created and its entries imported, so probing the root DSE alone reports ready while a
# search of BASE_DN still fails with "No Such Entry". Testing the marker first also keeps
# the probe from launching a JVM every interval until the bootstrap is through. The start
# period is what a bootstrap importing SAMPLE_DATA into a small container can take; a
# probe that succeeds ends it early, and a bootstrap that failed never writes the marker.
HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD test -f "$BOOTSTRAP_COMPLETE" && opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1
# search of BASE_DN still fails with "No Such Entry". healthcheck.sh tests the marker
# first, then searches the root DSE without binding as the root user, whose password the
# operator is expected to change. The start period is what a bootstrap importing
# SAMPLE_DATA into a small container can take; a probe that succeeds ends it early, and a
# bootstrap that failed never writes the marker.
HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD ["/opt/opendj/healthcheck.sh"]
ENTRYPOINT ["/opt/opendj/run.sh"]
opendj-packages/opendj-docker/Dockerfile-alpine
@@ -69,8 +69,9 @@
# root. The scripts copied below are only read and run, so they just keep the same group.
COPY --chown=$OPENDJ_USER:0 bootstrap/ /opt/opendj/bootstrap/
COPY --chown=$OPENDJ_USER:0 run.sh /opt/opendj/run.sh
COPY --chown=$OPENDJ_USER:0 healthcheck.sh /opt/opendj/healthcheck.sh
RUN chmod +x /opt/opendj/run.sh /opt/opendj/bootstrap/setup.sh /opt/opendj/bootstrap/replicate.sh
RUN chmod +x /opt/opendj/run.sh /opt/opendj/healthcheck.sh /opt/opendj/bootstrap/setup.sh /opt/opendj/bootstrap/replicate.sh
EXPOSE $PORT/tcp $LDAPS_PORT/tcp $ADMIN_PORT/tcp
@@ -79,10 +80,11 @@
# "healthy" has to mean the instance is ready to serve, not just that it answers: setup
# starts the server in the middle of the bootstrap, before the backend of BASE_DN is
# created and its entries imported, so probing the root DSE alone reports ready while a
# search of BASE_DN still fails with "No Such Entry". Testing the marker first also keeps
# the probe from launching a JVM every interval until the bootstrap is through. The start
# period is what a bootstrap importing SAMPLE_DATA into a small container can take; a
# probe that succeeds ends it early, and a bootstrap that failed never writes the marker.
HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD test -f "$BOOTSTRAP_COMPLETE" && opendj/bin/ldapsearch --hostname localhost --port $LDAPS_PORT --bindDN "$ROOT_USER_DN" --bindPassword "${ROOT_PASSWORD:-password}" --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1
# search of BASE_DN still fails with "No Such Entry". healthcheck.sh tests the marker
# first, then searches the root DSE without binding as the root user, whose password the
# operator is expected to change. The start period is what a bootstrap importing
# SAMPLE_DATA into a small container can take; a probe that succeeds ends it early, and a
# bootstrap that failed never writes the marker.
HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 CMD ["/opt/opendj/healthcheck.sh"]
ENTRYPOINT ["/opt/opendj/run.sh"]
opendj-packages/opendj-docker/README.md
@@ -30,6 +30,26 @@
a healthy container - the health check itself searches the root DSE, which every instance
serves whatever it was set up to hold.
The health check does not bind as the root user: `ROOT_PASSWORD` is only the initial root
password, and a probe binding with it would turn the container `unhealthy` once that password
is changed. It reads the root DSE anonymously instead. An instance that rejects
unauthenticated requests (`reject-unauthenticated-requests:true`) answers that search with
`53 (Unwilling to Perform)`; for such an instance set `HEALTHCHECK_BIND_DN` to an account the
probe may bind as and `HEALTHCHECK_BIND_PASSWORD_FILE` to a file in the container holding its
password - the probe reads it from there, so it never shows on a command line:
```bash
docker run -d --name opendj -v /path/to/secrets:/var/secrets/healthcheck:ro \
  -e HEALTHCHECK_BIND_DN="uid=monitor,ou=people,dc=example,dc=com" \
  -e HEALTHCHECK_BIND_PASSWORD_FILE=/var/secrets/healthcheck/password \
  openidentityplatform/opendj
```
Images before this one probed as the root user, so an existing instance that rejects
unauthenticated requests was healthy with them. Started on this image without these two
variables, the same instance is probed anonymously and turns `unhealthy` although it serves:
set them before the upgrade.
The server answering is not enough on a first start: the bootstrap starts the server, and
once it is done that server is stopped and started again in the foreground, so a client
that only waits for the port can have its first requests fail in between. A replica set up
@@ -67,4 +87,6 @@
| OPENDJ_JAVA_ARGS        | -server                         | extra instance java args                                                                                                                                                                                                                                |
| BACKEND_TYPE            | je                              | OpenDJ backend type, see [dsconfig create-backend](https://doc.openidentityplatform.org/opendj/reference/dsconfig-subcommands-ref#dsconfig-create-backend) documentation                                                                                |
| BACKEND_DB_DIRECTORY    | db                              | OpenDJ `db-directory` attribute for backend                                                                                                                                                                                                             |
| SETUP_ARGS              | -                               | extra setup args                                                                                                                                                                                                                                        |
| SETUP_ARGS              | -                               | extra setup args                                                                                                                                                                                                                                        |
| HEALTHCHECK_BIND_DN     | -                               | DN the health check binds as, for an instance that rejects unauthenticated requests; unset, the health check searches the root DSE anonymously |
| HEALTHCHECK_BIND_PASSWORD_FILE | -                               | file in the container holding the password of `HEALTHCHECK_BIND_DN` |
opendj-packages/opendj-docker/healthcheck.sh
New file
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# The contents of this file are subject to the terms of the Common Development and
# Distribution License (the License). You may not use this file except in compliance with the
# License.
#
# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
# specific language governing permission and limitations under the License.
#
# When distributing Covered Software, include this CDDL Header Notice in each file and include
# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
# Header, with the fields enclosed by brackets [] replaced by your own identifying
# information: "Portions copyright [year] [name of copyright owner]".
#
# Copyright 2026 3A Systems, LLC.
# The container health check
#
# The marker comes first: run.sh writes it only once the bootstrap has succeeded, and testing
# it also keeps the probe from launching a JVM every interval until then.
#
# The probe then reads the root DSE with the attribute list 1.1, which needs no bind. It must
# not bind as the root user: ROOT_PASSWORD is only the initial root password, so the probe
# would turn the container unhealthy for good once an operator changes it, and it would put
# the password on a command line every interval. An instance configured to reject
# unauthenticated requests answers the anonymous search with 53 (Unwilling to Perform); for
# such an instance HEALTHCHECK_BIND_DN names an account to bind with, and its password is
# read from HEALTHCHECK_BIND_PASSWORD_FILE, never passed on a command line. --noPropertiesFile
# keeps a tools.properties in the user's home from turning the probe into a bind of its own.
#
# Docker reserves exit code 2, so whatever failed is reported as 1.
test -f "${BOOTSTRAP_COMPLETE:-/opt/opendj/.bootstrap-complete}" || exit 1
BIND_ARGS=()
if [ -n "${HEALTHCHECK_BIND_DN}" ]; then
  if [ ! -r "${HEALTHCHECK_BIND_PASSWORD_FILE}" ]; then
    echo "HEALTHCHECK_BIND_DN is set, but HEALTHCHECK_BIND_PASSWORD_FILE '${HEALTHCHECK_BIND_PASSWORD_FILE}' is not a readable file"
    exit 1
  fi
  BIND_ARGS=(--bindDN "${HEALTHCHECK_BIND_DN}" --bindPasswordFile "${HEALTHCHECK_BIND_PASSWORD_FILE}")
fi
/opt/opendj/bin/ldapsearch --noPropertiesFile --hostname localhost --port "${LDAPS_PORT:-1636}" --useSsl --trustAll \
  "${BIND_ARGS[@]}" --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1
opendj-packages/opendj-docker/pom.xml
@@ -13,6 +13,7 @@
  information: "Portions Copyright [year] [name of copyright owner]".
  Copyright 2018-2019 Open Identity Platform Community.
  Portions Copyright 2026 3A Systems, LLC.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
@@ -81,6 +82,7 @@
                            <include name="../opendj-openshift-template/*.yaml" />
                            <include name="bootstrap/**" />
                            <include name="run.sh" />
                            <include name="healthcheck.sh" />
                        </fileset>
                      </zip>
                  </target>