Conversation
312fa58 to
f5f095f
Compare
|
@maximthomas, rebased onto the current The merge of #1091 made this PR conflict in three files:
The description is updated to match. |
maximthomas
left a comment
There was a problem hiding this comment.
praise: The probe now checks what it should without holding the root password.
healthcheck.sh:39passes the password with--bindPasswordFile, so it no longer shows up in/proc/<pid>/cmdline.- The
rc=53check atbuild.yml:585-587proves thatreject-unauthenticated-requestshas taken effect beforestays_healthyruns, so the bind case cannot pass without actually testing anything. pom.xmladdshealthcheck.shtoDockerfile.zip, which both CI and the release build the image from.
suggestion (non-blocking): Say in the README that existing instances which reject unauthenticated requests need HEALTHCHECK_BIND_DN before they upgrade.
opendj-packages/opendj-docker/healthcheck.sh:42-43, opendj-packages/opendj-docker/README.md:33-46
Take an instance with reject-unauthenticated-requests:true in its volume config whose root password is still ROOT_PASSWORD. It is healthy on 5.1.2, because the old probe bound as root. On this image, run.sh:44-48 upgrades it and touches the marker. The anonymous probe then gets 53, and after three retries the container is unhealthy even though it is serving. Swarm replaces it, and compose services that depend on service_healthy never start. The README explains the variables for such instances, but it does not say that an instance which is already running has to set them before it upgrades.
Upgrading from an image that probed as the root user: an instance with
`reject-unauthenticated-requests:true` is now probed anonymously and turns `unhealthy`
unless `HEALTHCHECK_BIND_DN` and `HEALTHCHECK_BIND_PASSWORD_FILE` are set before it starts
on this image.suggestion (non-blocking): Pass --noPropertiesFile so a tools.properties in the image user's home cannot turn the anonymous probe into a bind.
opendj-packages/opendj-docker/healthcheck.sh:42
/opt/opendj/bin/ldapsearch is the toolkit LDAPSearch. Unless it gets --noPropertiesFile, it loads ~/.opendj/tools.properties (ArgumentParser.java:383-403, LDAPSearch.java:263-269), and the image creates /home/opendj with useradd -m. Suppose an operator puts ldapsearch.bindDN/bindPassword there so they can use the CLI through docker exec. The probe then binds with those credentials, and rotating that password makes the container unhealthy again: the #1092 failure through a different path. The old probe passed both bind arguments itself, so a properties file could not change how it bound.
/opt/opendj/bin/ldapsearch --noPropertiesFile --hostname localhost --port "${LDAPS_PORT:-1636}" --useSsl --trustAll \
"${BIND_ARGS[@]}" --baseDN "" --searchScope base "(objectClass=*)" 1.1 || exit 1suggestion (non-blocking): No CI case expects the probe to fail, so a probe that always exits 0 would pass CI.
.github/workflows/build.yml:559-589, :756-786, opendj-packages/opendj-docker/healthcheck.sh:43
Both new cases, and every older docker step, only assert that a container becomes or stays healthy. The rc=53 check runs its own ldapsearch rather than the probe. If || exit 1 becomes || true, or the ldapsearch is replaced by exit 0, both jobs stay green. That is the opposite of #1092: the container reports healthy while the server refuses the probe.
test "$rc" = 53
# the probe itself reports the refusal
rc=0
docker exec -e HEALTHCHECK_BIND_DN= test_health_bind /opt/opendj/healthcheck.sh || rc=$?
test "$rc" = 1Pin: the || true and exit 0 mutants both make the final test "$rc" = 1 fail. Add the same lines to the alpine job.
suggestion (non-blocking): stays_healthy checks the status once after 45 s, which does not show that every probe since then passed.
.github/workflows/build.yml:566-571, :763-768
A successful probe resets FailingStreak to 0 and sets the status back to healthy. So a full unhealthy episode that recovers (fail, fail, fail, pass) still reads back as healthy 0, and so do alternating failures. The regressions this PR targets fail on every probe, so they are still caught. What slips through is intermittent failures. Either keep the check and reword the comment to "no failing streak at the end of the window", or check every probe:
stays_healthy() {
# Docker keeps the last 5 probes (>= 20 s at a 5 s interval): every one must have passed
for _ in 1 2 3; do
sleep 15
test -z "$(docker inspect --format='{{range .State.Health.Log}}{{if ne .ExitCode 0}}x{{end}}{{end}}' "$1")"
done
test "$(docker inspect --format='{{.State.Health.Status}}' "$1")" = healthy
}Pin: a probe that fails once in the middle of the window makes the check fail.
suggestion (non-blocking): Nothing tests that the password never appears on a command line.
opendj-packages/opendj-docker/healthcheck.sh:39
Suppose BIND_ARGS used --bindPassword "$(cat "${HEALTHCHECK_BIND_PASSWORD_FILE}")" instead. It binds with the same password, both cases stay green, and the /proc/<pid>/cmdline exposure this PR removes would be back.
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 -- "--bindPassword[ ]" && exit 1; sleep 0.2; done; exit 0'Pin: this samples the command lines for two probe intervals, and the mutant's probe JVM shows --bindPassword password. The [ ] keeps the check from matching its own command line.
suggestion (non-blocking): No CI case runs the guard for an unreadable password file.
opendj-packages/opendj-docker/healthcheck.sh:35-38
test_health leaves HEALTHCHECK_BIND_DN unset, and test_health_bind mounts a readable file, so line 36 never runs. If the guard is deleted, the health status does not change, because ldapsearch rejects the file itself. Only the explanatory message in .State.Health.Log is lost.
rc=0
out=$(docker exec -e HEALTHCHECK_BIND_PASSWORD_FILE=/nonexistent test_health_bind /opt/opendj/healthcheck.sh) || rc=$?
test "$rc" = 1
grep -q 'is not a readable file' <<< "$out"issue (non-blocking): A single probe that fails just before the 45 s check fails the job, even though the container is healthy.
.github/workflows/build.yml:568-571, :573, :581
The status must be exactly healthy 0 at one moment, while the probe starts a second JVM every 5 s next to the server in a 512m container. One failure that lands just before the check leaves healthy 1. I found nothing that would cause such a failure. Not run: that needs repeated runs of the step on ubuntu-latest, and the docker jobs have not reported on this head yet. If it does flake, the ERR trap already prints .State.Health for both containers.
|
@maximthomas, thanks. Round 1 is in 1c49ed2. Six of the seven points are taken, #4 and #5 in a slightly different form; #7 is left as it is.
Verification: the CI step was extracted from
|
…ut binding as the root user The HEALTHCHECK bound as ROOT_USER_DN with ROOT_PASSWORD, which is only the initial root password: once an operator changed it every probe failed with 49 and the container stayed unhealthy for good, and every probe put the password on a command line. The probe now lives in healthcheck.sh: it tests the bootstrap marker, then reads the root DSE with 1.1 anonymously. An instance that rejects unauthenticated requests (53) is probed with the account named by HEALTHCHECK_BIND_DN, its password read from HEALTHCHECK_BIND_PASSWORD_FILE. Fixes OpenIdentityPlatform#1092
…ealth check bind, and test that the probe fails when it should
- healthcheck.sh passes --noPropertiesFile: the toolkit ldapsearch otherwise reads
~/.opendj/tools.properties, and /home/opendj exists in the image, so bind settings put there
for the CLI would turn the anonymous probe into a bind whose password can change.
- README: an instance that rejects unauthenticated requests was healthy on images that probed
as root; it has to set HEALTHCHECK_BIND_DN and HEALTHCHECK_BIND_PASSWORD_FILE before it
starts on this one.
- CI, both docker jobs:
- stays_healthy reads the status every 2 s for 45 s instead of once at the end, so a single
failed probe in the window fails the step;
- the probe itself is run with HEALTHCHECK_BIND_DN cleared and must exit 1 on the instance
that rejects unauthenticated requests;
- with an unreadable password file it must exit 1 and say so;
- the bind case uses a root password of its own, and the command lines of every process in
the container are sampled for two probe intervals to check it never appears on one.
maximthomas
left a comment
There was a problem hiding this comment.
praise: Round 1 is taken in full and the new checks are stronger than the ones suggested.
opendj-packages/opendj-docker/healthcheck.sh:43passes--noPropertiesFile, so atools.propertiesin/home/opendj/.opendjcan no longer turn the anonymous probe into a bind.- The command-line check (
.github/workflows/build.yml:603) searches for the password value itself ([h]c_secret_1092), so it catches-w,--bindPassword=and--bindPassword "$(cat …)"alike. set -o errtrace(.github/workflows/build.yml:563) makes the ERR trap fire for a check that fails insidestays_healthy.
suggestion (non-blocking): No CI case pins --noPropertiesFile; removing it leaves both docker jobs green.
opendj-packages/opendj-docker/healthcheck.sh:43, .github/workflows/build.yml:578-579, :790-791
The toolkit parser reads ~/.opendj/tools.properties only when the file exists. Nothing in the image or in the "Docker test health check" steps creates /home/opendj/.opendj/tools.properties, so without the flag every case runs with the same arguments and the mutant survives both jobs. You checked this by hand (reply, point 2). Writing the same file in the rotation case turns that check into a CI pin.
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
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_healthPin: without --noPropertiesFile, every probe after the new line fails with 49 (Invalid Credentials) and stays_healthy test_health goes red. With the flag, the case stays green. Add the same line to the Alpine job.
suggestion (non-blocking): The unreadable-password-file case only uses a path that does not exist, so a guard weakened from -r to -e or -f still passes.
opendj-packages/opendj-docker/healthcheck.sh:36, .github/workflows/build.yml:597-600, :809-812
/nonexistent fails -r, -e and -f alike, so the weakened guard prints the same message and exits 1. The case the message describes, a file that exists but that the probe user cannot read, never runs: the mounted file is 644 (:584). Your pin (reply, point 6) catches a removed guard, not a weakened one. Container health is the same either way, so only the message is unpinned.
docker exec test_health_bind sh -c 'touch /tmp/unreadable_password && chmod 000 /tmp/unreadable_password'
rc=0
out=$(docker exec -e HEALTHCHECK_BIND_PASSWORD_FILE=/tmp/unreadable_password test_health_bind /opt/opendj/healthcheck.sh) || rc=$?
test "$rc" = 1
grep -q 'is not a readable file' <<< "$out"Pin: the image runs as opendj (uid 1001), not as root, so mode 000 is unreadable to the probe. Under -e or -f the file passes the guard, ldapsearch fails on it with its own error on stderr, and the grep fails.
suggestion (non-blocking): The two health-check containers start with --rm, so the ERR trap has nothing to print when a bootstrap fails.
.github/workflows/build.yml:576, :585, :788, :797
If setup fails before it starts the server, run.sh:106 execs start-ds --nodetach, which exits when there is no usable config (#1088: exit 98). PID 1 exits and --rm removes the container. The wait loop then runs until timeout 3m, and the trap (:564) prints only "No such container" for docker logs and .State.Health. A check that fails after the container is healthy still gets its logs. The earlier docker steps (:526, :541, :551) are built the same way, so this repeats an existing pattern.
docker run -it -d --memory="512m" --health-interval=5s -e ROOT_PASSWORD=initial_password --name=test_health $IMAGEOr: also leave the wait loop once {{.State.Running}} is false, so the step fails at once instead of after 3 minutes. Runners are ephemeral, and the docker kill at the end stays.
…ne of the Docker bootstrap as well - setup.sh writes ROOT_PASSWORD to a file readable by its owner only, on /dev/shm where there is one, and removes it on exit, as replicate.sh does since OpenIdentityPlatform#1094. setup, dsconfig, import-ldif and ldapmodify read the password from it (--rootUserPasswordFile, --bindPasswordFile), so it no longer shows on a command line during the bootstrap, where the ps of the Docker host lists it to every user of the host. ldapmodify used to get it unquoted, which also split a password with a space in it. - run.sh removes a password file a killed setup.sh leaves in /dev/shm, next to the one of replicate.sh. - CI: - the replication step's check that no tool gets the password on its command line covers setup.sh too, --rootUserPassword included; - the check that no password is left in /tmp or /dev/shm covers the master, which only setup.sh bootstrapped, and no longer skips /tmp/hsperfdata_*: the HEALTHCHECK kept the root password there, and after this change no process left running has it; - the health check step's dsconfig reads the password from the mounted file, the one the probe reads, rather than taking it on its command line.
1c49ed2 to
b272e53
Compare
…password file in CI, and keep a failed container for the trap Both docker jobs, Docker test health check: - the rotation case writes bind settings with a wrong password to /home/opendj/.opendj/tools.properties before it checks the container stays healthy, so a probe without --noPropertiesFile fails there; - the unreadable password file case also runs with a file that exists with mode 000, which the probe, running as the image user rather than root, cannot read, so a guard weakened from -r to -e or -f fails it; - the two containers run without --rm and are removed by the ERR trap and at the end of the step, so one whose bootstrap failed is still there for the trap to print its log.
|
@maximthomas, thanks. Round 2 is in 4621a5c. Your review covered 1c49ed2; since then the branch has also been rebased and has one more commit, described first. Rebased onto b272e53 – the bootstrap keeps the root password off the command line as well. In round 1
Verified locally on 5.1.2 images with the branch's scripts. Every process's command line was sampled from the container start until the bootstrap marker, with Round 2 – all three points taken, in both jobs:
|
Fixes #1092
Problem
The image
HEALTHCHECKbound asROOT_USER_DNwithROOT_PASSWORD, which is only the initial root password. Once an operator changed the root password, every probe failed with49 (Invalid Credentials)and the container stayedunhealthyfor good while the server kept serving. The same happens to a new container started over an existing volume without the originalROOT_PASSWORD:run.shdoes not use the password on an existing instance, but the probe did. Every probe also put the password on a command line, readable from/proc/<pid>/cmdlinewhile it ran, and so did every toolbootstrap/setup.shran during the bootstrap, where thepsof the Docker host lists it to every user of the host.Change
healthcheck.sh(new, next torun.sh): tests the$BOOTSTRAP_COMPLETEmarker first, as before (Report the OpenDJ container healthy only once its bootstrap has succeeded #898), then reads the root DSE with the attribute list1.1over LDAPS anonymously, with--noPropertiesFileso that atools.propertiesin the image user's home cannot turn it into a bind. It sits next torun.shrather than inbootstrap/becausebootstrap/is the directory users mount their own content over. Any failure exits with1, since Docker reserves2.reject-unauthenticated-requests:truethe server refuses the anonymous search with53 (Unwilling to Perform)before any ACI is evaluated (DirectoryServer.checkCanEnqueueRequest). For such an instance,HEALTHCHECK_BIND_DNnames the account the probe binds as, and the probe reads its password fromHEALTHCHECK_BIND_PASSWORD_FILEwith--bindPasswordFile, so it never shows on a command line. Neither variable is set by default.bootstrap/setup.sh: writesROOT_PASSWORDto a file readable by its owner only, on/dev/shmwhere there is one, removed by anEXITtrap, asreplicate.shdoes since [#1084] Keep the root password out of the log and off the command line when a Docker container joins replication #1094;setup,dsconfig,import-ldifandldapmodifyread it with--rootUserPasswordFile/--bindPasswordFile.ldapmodifyused to get it unquoted, so a password with a space in it broke the loading ofbootstrap/schemaandbootstrap/data.run.sh: removes a password file a killedsetup.shleaves in/dev/shm, next to the one ofreplicate.sh.Dockerfile,Dockerfile-alpine: copy the script with--chown=$OPENDJ_USER:0, like the other scripts since [#1088] Let the Docker image start under an arbitrary uid in group 0 #1091, and setHEALTHCHECK ... CMD ["/opt/opendj/healthcheck.sh"]. Interval, timeout, start period and retries are unchanged.pom.xml:Dockerfile.zip, which both CI and the release build the image from, lists its files explicitly, sohealthcheck.shis added there.README.md: the Health check section explains why the probe does not bind as root and how to use the two new variables; both are added to the variable table. It also says that an instance which rejects unauthenticated requests was healthy on images that probed as root, and has to set both variables before it starts on this one.Docker test health checkis added to bothbuild-dockerandbuild-docker-alpine, with--health-interval=5s:healthywith a failing streak of 0 every 2 s for 45 s, so a single failed probe in that window fails the step;ROOT_PASSWORD=initial_passwordturnshealthy, its root password is changed withldappasswordmodify, bind settings with a wrong password are written to/home/opendj/.opendj/tools.properties, and it stays healthy;HEALTHCHECK_BIND_DNand a mounted password file turnshealthy,reject-unauthenticated-requestsis switched on, and an anonymous search is checked to fail with53(so the setting has taken effect). On that instancehealthcheck.shmust exit1withHEALTHCHECK_BIND_DNcleared, and exit1reporting an unreadable password file, both for a missing one and for one with mode000.dsconfigreads the password from the same mounted file. The container must stay healthy, and the command lines of every process in it are sampled for 12 s to check that the password never appears on one.--rm, so one whose bootstrap failed is still there for the ERR trap to print; the trap and the end of the step remove them.Docker test replication(from [#1084] Keep the root password out of the log and off the command line when a Docker container joins replication #1094): the check that no tool gets the password on its command line coverssetup.shtoo,--rootUserPasswordincluded; the check that nothing is left in/tmpor/dev/shmcovers the master as well, and no longer skips/tmp/hsperfdata_*, which only the old probe needed.Verification
Images built locally from release 5.1.2, with the CI step run as a script:
openidentityplatform/opendj:latest(the old probe): the rotation case fails; after the password change every probe logsThe LDAP search request failed: 49 (Invalid Credentials);The review round was checked the same way, with
healthcheck.shmounted over the image's copy: the branch passes on both images, and the step fails for each of these mutants of the probe:|| exit 1→|| true,--bindPasswordFile→--bindPassword "$(cat …)", the unreadable-file guard removed, and a probe that fails every fourth call. With bind settings in/home/opendj/.opendj/tools.properties, the probe without--noPropertiesFilefails with49, and with it passes.The bootstrap: every process's command line was sampled from the container start until the bootstrap marker, with
bootstrap/schemaandbootstrap/datamounted and a root password with a space in it. With this branch'ssetup.sh: no hits on either image, both LDIFs loaded,/dev/shmempty. Withmaster's: the password shows on the command lines ofsetup, its JVMs,import-ldif,dsconfigandldapmodify, and the LDIFs are not loaded.On thin images over 5.1.2 carrying this branch's scripts,
Docker test replicationandDocker test health checkpass on both images as extracted frombuild.yml(only the waits relaxed for a slower host). The health check step fails without--noPropertiesFileand with the unreadable-file guard weakened to-e.Notes
HEALTHCHECK; the Helm chart discussed in Add an official Helm chart so OpenDJ can be deployed on any Kubernetes cluster #1079 should use the same probe.Docker test arbitrary uidstep waits forhealthy, so it also runs this probe under an arbitrary uid in group 0. The probe only reads, so it needs nothing more than the group the image gives it.build.yml, and [#1093] Hand the root password to the Docker bootstrap tools in a file #1099 also changesbootstrap/setup.sh, so whichever merges later needs a rebase.