diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8469aec276..cd9cf53b76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -569,8 +569,8 @@ jobs: 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 # 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.' /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.XXXXXX) && rm -f "$f" && case $f in /dev/shm/opendj-replicate.*) ;; *) exit 1;; esac' || { echo "::error::mktemp in the image does not create the password file on /dev/shm"; false; } + 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; } # a password with a space in it reaches every tool as one value ROOT_PASSWORD='replication secret' docker network create test_replication @@ -579,8 +579,10 @@ jobs: # a replica reports itself healthy only once replicate.sh has succeeded; the sdsr replica joins after # the simple one, as two dsreplication enable at once would both rewrite the admin data of the master # a Kubernetes pod keeps its /dev/shm across container restarts: the replica shares the /dev/shm of the master, where - # a password file waits as a killed replicate.sh would have left it, and its run.sh has to remove it (checked below) - docker exec test_master sh -c 'printf "%s\n" "$ROOT_PASSWORD" >/dev/shm/opendj-replicate.killed' + # a password file waits as a killed replicate.sh would have left it, and its run.sh has to remove it (checked below); + # the file of another container of the pod, which listens on another admin port, has to be kept + docker exec test_master sh -c 'printf "%s\n" "$ROOT_PASSWORD" >/dev/shm/opendj-replicate.4444.killed' + docker exec test_master sh -c ': >/dev/shm/opendj-replicate.5444.other' docker run --rm -it -d --memory="512m" --network test_replication --ipc=container:test_master --name=test_replica --hostname=dj-replica -e ROOT_PASSWORD="$ROOT_PASSWORD" -e MASTER_SERVER=dj-master -e OPENDJ_REPLICATION_TYPE=simple "$IMAGE" timeout 5m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_replica | grep -q \"healthy\"; do sleep 10; done' docker run --rm -it -d --memory="512m" --network test_replication --name=test_replica_sdsr --hostname=dj-replica-sdsr -e ROOT_PASSWORD="$ROOT_PASSWORD" -e MASTER_SERVER=dj-master -e OPENDJ_REPLICATION_TYPE=sdsr "$IMAGE" @@ -602,7 +604,66 @@ jobs: left=$(docker exec $c grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm | grep -v '^/tmp/hsperfdata_' || 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; } cleanup + - name: Docker test bootstrap LDIFs + shell: bash + run: | + IMAGE=localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }} + trap 'code=$?; for c in test_bootstrap_lf test_bootstrap_cr test_bootstrap; do echo "::group::container logs ($c)"; docker logs $c 2>&1 || true; echo "::endgroup::"; done; exit $code' ERR + # a root password with a line break is refused: the tools read only the first line of the password file + docker run -d --memory="512m" -e ROOT_PASSWORD=$'ab\ncd' --name=test_bootstrap_lf "$IMAGE" + docker run -d --memory="512m" -e ROOT_PASSWORD=$'ab\rcd' --name=test_bootstrap_cr "$IMAGE" + for c in test_bootstrap_lf test_bootstrap_cr; do + timeout 2m bash -c 'until docker logs $0 2>&1 | grep -qF "ROOT_PASSWORD must not contain a line break"; do sleep 2; done' $c + done + docker rm -f test_bootstrap_lf test_bootstrap_cr + # a password with a space and a pattern that matches files in /opt/opendj: every tool + # of the bootstrap must get it as one value (#1093); the LDIF names have a space as well + ROOT_PASSWORD='p@ss b*' + BOOTSTRAP_DIR=$(mktemp -d) + mkdir -p "$BOOTSTRAP_DIR/schema" "$BOOTSTRAP_DIR/data" "$BOOTSTRAP_DIR/config/schema" + printf "dn: cn=schema\nchangetype: modify\nadd: attributeTypes\nattributeTypes: ( 1.3.6.1.4.1.99999.1.1 NAME 'bootstrapProbe' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )\n" > "$BOOTSTRAP_DIR/schema/10 schema.ldif" + # a schema file of bootstrap/config/schema is copied into the configuration of the instance before setup runs + printf "dn: cn=schema\nobjectClass: top\nobjectClass: ldapSubentry\nobjectClass: subschema\ncn: schema\nattributeTypes: ( 1.3.6.1.4.1.99999.1.2 NAME 'bootstrapConfigProbe' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )\n" > "$BOOTSTRAP_DIR/config/schema/99 config.ldif" + # a pre-encoded password is accepted only once the bootstrap has allowed it on the admin + # port the server listens on, which is not the default one here + USER_PASSWORD=$(python3 -c 'import base64, hashlib, os; s = os.urandom(8); print("{SSHA}" + base64.b64encode(hashlib.sha1(b"userpw" + s).digest() + s).decode())') + printf 'dn: ou=probe,dc=example,dc=com\nobjectClass: organizationalUnit\nou: probe\n\ndn: uid=pre,ou=probe,dc=example,dc=com\nobjectClass: inetOrgPerson\nuid: pre\ncn: pre\nsn: pre\nuserPassword: %s\n' "$USER_PASSWORD" > "$BOOTSTRAP_DIR/data/10 probe.ldif" + chmod -R a+rX "$BOOTSTRAP_DIR" + # the containers of a Kubernetes pod share its /dev/shm, which outlives a restart of one of them: the holder stands in for the pod + docker run -d --no-healthcheck --ipc=shareable --entrypoint sleep --name=test_bootstrap_shm "$IMAGE" 3600 + docker run -it -d --memory="512m" --ipc=container:test_bootstrap_shm -e ADD_BASE_ENTRY="--addBaseEntry" -e ROOT_PASSWORD="$ROOT_PASSWORD" -e ADMIN_PORT=5444 -v "$BOOTSTRAP_DIR/schema:/opt/opendj/bootstrap/schema:ro" -v "$BOOTSTRAP_DIR/data:/opt/opendj/bootstrap/data:ro" -v "$BOOTSTRAP_DIR/config/schema:/opt/opendj/bootstrap/config/schema:ro" --name=test_bootstrap "$IMAGE" + # while the bootstrap runs, the password file is on the tmpfs of /dev/shm rather than in the writable + # layer, and its name carries the admin port of the container + timeout 2m bash -c 'until docker exec test_bootstrap sh -c "ls /dev/shm/opendj-setup-password.5444.*" >/dev/null 2>&1; do sleep 1; done' + timeout 5m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_bootstrap | grep -q \"healthy\"; do sleep 10; done' + # the schema and the data LDIFs were loaded: the user of the data LDIF binds with its pre-encoded password + docker exec test_bootstrap /opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword "$ROOT_PASSWORD" --useSsl --trustAll --baseDN "cn=schema" --searchScope base "(objectClass=*)" attributeTypes > "$BOOTSTRAP_DIR/schema.out" + grep -q bootstrapProbe "$BOOTSTRAP_DIR/schema.out" + grep -q bootstrapConfigProbe "$BOOTSTRAP_DIR/schema.out" + docker exec test_bootstrap /opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "uid=pre,ou=probe,dc=example,dc=com" --bindPassword userpw --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 + if docker logs test_bootstrap 2>&1 | grep -F "$ROOT_PASSWORD"; then echo "::error::The root password is in the container log"; false; fi + # the password file of setup.sh is gone once the bootstrap is over; the perf data of the + # JVM of the HEALTHCHECK's ldapsearch holds the password too while it runs (#1092) + left=$(docker exec test_bootstrap grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm | grep -v '^/tmp/hsperfdata_' || true) + if [ -n "$left" ]; then echo "::error::The root password is left in $left"; false; fi + # no tool of the bootstrap gets the password on its command line, which only a sample taken + # while that tool runs would show; grep exits with 1 only when it has read the file and found nothing + rc=0; docker exec test_bootstrap grep -nE -- '(^|[[:space:]])(-w|--(bindPassword|rootUserPassword))([[:space:]=]|$)' /opt/opendj/bootstrap/setup.sh || rc=$? + if [ $rc -ne 1 ]; then echo "::error::setup.sh passes the root password on a command line, or grep could not read it"; false; fi + # run.sh removes the password file that a bootstrap killed before its EXIT trap leaves behind, in /tmp + # and in /dev/shm, but not the one of another container of the pod, which listens on another admin port + left=$(docker exec test_bootstrap mktemp /tmp/opendj-setup-password.5444.XXXXXX) + shm_left=$(docker exec test_bootstrap mktemp -p /dev/shm opendj-setup-password.5444.XXXXXX) + shm_other=$(docker exec test_bootstrap mktemp -p /dev/shm opendj-setup-password.4444.XXXXXX) + docker restart test_bootstrap + timeout 5m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_bootstrap | grep -q \"healthy\"; do sleep 10; done' + for f in "$left" "$shm_left"; do + docker exec test_bootstrap test ! -e "$f" || { echo "::error::$f is still there after a restart"; false; } + 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: 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 @@ -782,8 +843,8 @@ jobs: 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 # 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.' /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.XXXXXX) && rm -f "$f" && case $f in /dev/shm/opendj-replicate.*) ;; *) exit 1;; esac' || { echo "::error::mktemp in the image does not create the password file on /dev/shm"; false; } + 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; } # a password with a space in it reaches every tool as one value ROOT_PASSWORD='replication secret' docker network create test_replication @@ -792,8 +853,10 @@ jobs: # a replica reports itself healthy only once replicate.sh has succeeded; the sdsr replica joins after # the simple one, as two dsreplication enable at once would both rewrite the admin data of the master # a Kubernetes pod keeps its /dev/shm across container restarts: the replica shares the /dev/shm of the master, where - # a password file waits as a killed replicate.sh would have left it, and its run.sh has to remove it (checked below) - docker exec test_master sh -c 'printf "%s\n" "$ROOT_PASSWORD" >/dev/shm/opendj-replicate.killed' + # a password file waits as a killed replicate.sh would have left it, and its run.sh has to remove it (checked below); + # the file of another container of the pod, which listens on another admin port, has to be kept + docker exec test_master sh -c 'printf "%s\n" "$ROOT_PASSWORD" >/dev/shm/opendj-replicate.4444.killed' + docker exec test_master sh -c ': >/dev/shm/opendj-replicate.5444.other' docker run --rm -it -d --memory="1g" --network test_replication --ipc=container:test_master --name=test_replica --hostname=dj-replica -e ROOT_PASSWORD="$ROOT_PASSWORD" -e MASTER_SERVER=dj-master -e OPENDJ_REPLICATION_TYPE=simple "$IMAGE" timeout 5m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_replica | grep -q \"healthy\"; do sleep 10; done' docker run --rm -it -d --memory="1g" --network test_replication --name=test_replica_sdsr --hostname=dj-replica-sdsr -e ROOT_PASSWORD="$ROOT_PASSWORD" -e MASTER_SERVER=dj-master -e OPENDJ_REPLICATION_TYPE=sdsr "$IMAGE" @@ -815,7 +878,66 @@ jobs: left=$(docker exec $c grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm | grep -v '^/tmp/hsperfdata_' || 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; } cleanup + - name: Docker test bootstrap LDIFs + shell: bash + run: | + IMAGE=localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}-alpine + trap 'code=$?; for c in test_bootstrap_lf test_bootstrap_cr test_bootstrap; do echo "::group::container logs ($c)"; docker logs $c 2>&1 || true; echo "::endgroup::"; done; exit $code' ERR + # a root password with a line break is refused: the tools read only the first line of the password file + docker run -d --memory="1g" -e ROOT_PASSWORD=$'ab\ncd' --name=test_bootstrap_lf "$IMAGE" + docker run -d --memory="1g" -e ROOT_PASSWORD=$'ab\rcd' --name=test_bootstrap_cr "$IMAGE" + for c in test_bootstrap_lf test_bootstrap_cr; do + timeout 2m bash -c 'until docker logs $0 2>&1 | grep -qF "ROOT_PASSWORD must not contain a line break"; do sleep 2; done' $c + done + docker rm -f test_bootstrap_lf test_bootstrap_cr + # a password with a space and a pattern that matches files in /opt/opendj: every tool + # of the bootstrap must get it as one value (#1093); the LDIF names have a space as well + ROOT_PASSWORD='p@ss b*' + BOOTSTRAP_DIR=$(mktemp -d) + mkdir -p "$BOOTSTRAP_DIR/schema" "$BOOTSTRAP_DIR/data" "$BOOTSTRAP_DIR/config/schema" + printf "dn: cn=schema\nchangetype: modify\nadd: attributeTypes\nattributeTypes: ( 1.3.6.1.4.1.99999.1.1 NAME 'bootstrapProbe' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )\n" > "$BOOTSTRAP_DIR/schema/10 schema.ldif" + # a schema file of bootstrap/config/schema is copied into the configuration of the instance before setup runs + printf "dn: cn=schema\nobjectClass: top\nobjectClass: ldapSubentry\nobjectClass: subschema\ncn: schema\nattributeTypes: ( 1.3.6.1.4.1.99999.1.2 NAME 'bootstrapConfigProbe' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )\n" > "$BOOTSTRAP_DIR/config/schema/99 config.ldif" + # a pre-encoded password is accepted only once the bootstrap has allowed it on the admin + # port the server listens on, which is not the default one here + USER_PASSWORD=$(python3 -c 'import base64, hashlib, os; s = os.urandom(8); print("{SSHA}" + base64.b64encode(hashlib.sha1(b"userpw" + s).digest() + s).decode())') + printf 'dn: ou=probe,dc=example,dc=com\nobjectClass: organizationalUnit\nou: probe\n\ndn: uid=pre,ou=probe,dc=example,dc=com\nobjectClass: inetOrgPerson\nuid: pre\ncn: pre\nsn: pre\nuserPassword: %s\n' "$USER_PASSWORD" > "$BOOTSTRAP_DIR/data/10 probe.ldif" + chmod -R a+rX "$BOOTSTRAP_DIR" + # the containers of a Kubernetes pod share its /dev/shm, which outlives a restart of one of them: the holder stands in for the pod + docker run -d --no-healthcheck --ipc=shareable --entrypoint sleep --name=test_bootstrap_shm "$IMAGE" 3600 + docker run -it -d --memory="1g" --ipc=container:test_bootstrap_shm -e ADD_BASE_ENTRY="--addBaseEntry" -e ROOT_PASSWORD="$ROOT_PASSWORD" -e ADMIN_PORT=5444 -v "$BOOTSTRAP_DIR/schema:/opt/opendj/bootstrap/schema:ro" -v "$BOOTSTRAP_DIR/data:/opt/opendj/bootstrap/data:ro" -v "$BOOTSTRAP_DIR/config/schema:/opt/opendj/bootstrap/config/schema:ro" --name=test_bootstrap "$IMAGE" + # while the bootstrap runs, the password file is on the tmpfs of /dev/shm rather than in the writable + # layer, and its name carries the admin port of the container + timeout 2m bash -c 'until docker exec test_bootstrap sh -c "ls /dev/shm/opendj-setup-password.5444.*" >/dev/null 2>&1; do sleep 1; done' + timeout 5m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_bootstrap | grep -q \"healthy\"; do sleep 10; done' + # the schema and the data LDIFs were loaded: the user of the data LDIF binds with its pre-encoded password + docker exec test_bootstrap /opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword "$ROOT_PASSWORD" --useSsl --trustAll --baseDN "cn=schema" --searchScope base "(objectClass=*)" attributeTypes > "$BOOTSTRAP_DIR/schema.out" + grep -q bootstrapProbe "$BOOTSTRAP_DIR/schema.out" + grep -q bootstrapConfigProbe "$BOOTSTRAP_DIR/schema.out" + docker exec test_bootstrap /opt/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "uid=pre,ou=probe,dc=example,dc=com" --bindPassword userpw --useSsl --trustAll --baseDN "" --searchScope base "(objectClass=*)" 1.1 + if docker logs test_bootstrap 2>&1 | grep -F "$ROOT_PASSWORD"; then echo "::error::The root password is in the container log"; false; fi + # the password file of setup.sh is gone once the bootstrap is over; the perf data of the + # JVM of the HEALTHCHECK's ldapsearch holds the password too while it runs (#1092) + left=$(docker exec test_bootstrap grep -rlsF -- "$ROOT_PASSWORD" /tmp /dev/shm | grep -v '^/tmp/hsperfdata_' || true) + if [ -n "$left" ]; then echo "::error::The root password is left in $left"; false; fi + # no tool of the bootstrap gets the password on its command line, which only a sample taken + # while that tool runs would show; grep exits with 1 only when it has read the file and found nothing + rc=0; docker exec test_bootstrap grep -nE -- '(^|[[:space:]])(-w|--(bindPassword|rootUserPassword))([[:space:]=]|$)' /opt/opendj/bootstrap/setup.sh || rc=$? + if [ $rc -ne 1 ]; then echo "::error::setup.sh passes the root password on a command line, or grep could not read it"; false; fi + # run.sh removes the password file that a bootstrap killed before its EXIT trap leaves behind, in /tmp + # and in /dev/shm, but not the one of another container of the pod, which listens on another admin port + left=$(docker exec test_bootstrap mktemp /tmp/opendj-setup-password.5444.XXXXXX) + shm_left=$(docker exec test_bootstrap mktemp -p /dev/shm opendj-setup-password.5444.XXXXXX) + shm_other=$(docker exec test_bootstrap mktemp -p /dev/shm opendj-setup-password.4444.XXXXXX) + docker restart test_bootstrap + timeout 5m bash -c 'until docker inspect --format="{{json .State.Health.Status}}" test_bootstrap | grep -q \"healthy\"; do sleep 10; done' + for f in "$left" "$shm_left"; do + docker exec test_bootstrap test ! -e "$f" || { echo "::error::$f is still there after a restart"; false; } + 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: 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 diff --git a/opendj-packages/opendj-docker/README.md b/opendj-packages/opendj-docker/README.md index 0b85aa0471..ac5868ccac 100644 --- a/opendj-packages/opendj-docker/README.md +++ b/opendj-packages/opendj-docker/README.md @@ -46,7 +46,7 @@ turning `unhealthy` once the start period is over. | LDAPS_PORT | 1636 | LDAPS Listener Port | | BASE_DN | dc=example,dc=com | OpenDJ Base DN | | ROOT_USER_DN | cn=Directory Manager | Initial root user DN | -| ROOT_PASSWORD | password | Initial root user password | +| ROOT_PASSWORD | password | Initial root user password; the bootstrap fails if it contains a line break (CR or LF) | | SECRET_VOLUME | - | Mounted keystore volume, if present copies keystore over | | MASTER_SERVER | - | Replication master server | | VERSION | - | OpenDJ version | diff --git a/opendj-packages/opendj-docker/bootstrap/replicate.sh b/opendj-packages/opendj-docker/bootstrap/replicate.sh index e996e6f9cd..e1bed4c488 100755 --- a/opendj-packages/opendj-docker/bootstrap/replicate.sh +++ b/opendj-packages/opendj-docker/bootstrap/replicate.sh @@ -41,8 +41,9 @@ echo "Replication type: $OPENDJ_REPLICATION_TYPE, base DN: $BASE_DN" # run if the script is killed, and a file left in /tmp would stay in the writable layer of the # container, where no later start removes it, since replicate.sh runs only on the first one. # On Kubernetes /dev/shm belongs to the pod: it is shared by all its containers and outlives a -# restart of this one, so run.sh removes a file left there by name on every start. -PASSWORD_FILE=$(mktemp -p /dev/shm opendj-replicate.XXXXXX 2>/dev/null || mktemp) || exit 1 +# restart of this one, so run.sh removes a file left there by name on every start; the name +# carries ADMIN_PORT, so that it removes only the file of this container. +PASSWORD_FILE=$(mktemp -p /dev/shm "opendj-replicate.$ADMIN_PORT.XXXXXX" 2>/dev/null || mktemp) || exit 1 trap 'rm -f "$PASSWORD_FILE"' EXIT printf '%s\n' "$ROOT_PASSWORD" >"$PASSWORD_FILE" || exit 1 diff --git a/opendj-packages/opendj-docker/bootstrap/setup.sh b/opendj-packages/opendj-docker/bootstrap/setup.sh index c571af0af4..847a22374b 100755 --- a/opendj-packages/opendj-docker/bootstrap/setup.sh +++ b/opendj-packages/opendj-docker/bootstrap/setup.sh @@ -22,6 +22,34 @@ echo "Setting up default OpenDJ instance" +# The tools read only the first line of a password file, and a CR ends it as an LF does, so a +# password with a line break would be cut there - silently, as the root password set up would +# not be the one the container was given +cr=$(printf '\r') +nl=' +' +case $ROOT_PASSWORD in + *"$cr"*|*"$nl"*) + echo "ROOT_PASSWORD must not contain a line break: the tools read only the first line of the password file" >&2 + exit 1 ;; +esac + +# The tools read the root password from a file, so that any password setup accepts reaches +# them as one value, and it does not show on the command line of a process while the tool +# runs. mktemp creates the file readable by its owner only. The EXIT trap does not run when the +# container is killed during the bootstrap, so the file goes to the tmpfs of /dev/shm where +# there is one, rather than to the writable layer of the container, and run.sh removes what a +# killed bootstrap left behind before anything else. On Kubernetes /dev/shm is shared by all +# the containers of the pod and outlives a restart of this one, which is what the removal in +# run.sh is for there; the name carries ADMIN_PORT, so that it removes only the file of this +# container. Busybox mktemp replaces only the last six X of the template. +PASSWORD_FILE=$(mktemp -p /dev/shm "opendj-setup-password.$ADMIN_PORT.XXXXXX" 2>/dev/null \ + || mktemp "/tmp/opendj-setup-password.$ADMIN_PORT.XXXXXX") || exit 1 +# the trap also removes the base entry template below, which a failed import would leave in /tmp +BASE_TEMPLATE= +trap 'rm -f "$PASSWORD_FILE" ${BASE_TEMPLATE:+"$BASE_TEMPLATE"}' EXIT +printf '%s\n' "$ROOT_PASSWORD" >"$PASSWORD_FILE" || exit 1 + # If any optional LDIF files are present load them # There are multiple types of ldif files. @@ -31,7 +59,7 @@ if [ -d /opt/opendj/bootstrap/config/schema/ ]; then echo "Copying schema:" mkdir -p /opt/opendj/template/config/schema for file in /opt/opendj/bootstrap/config/schema/*; do - target_file="/opt/opendj/template/config/schema/$(basename -- $file)" + target_file="/opt/opendj/template/config/schema/$(basename -- "$file")" echo "Copying $file to $target_file" cp "$file" "$target_file" done @@ -45,7 +73,7 @@ fi --enableStartTLS $OPENDJ_SSL_OPTIONS \ --adminConnectorPort $ADMIN_PORT \ --rootUserDN "$ROOT_USER_DN" \ - --rootUserPassword "$ROOT_PASSWORD" \ + --rootUserPasswordFile "$PASSWORD_FILE" \ --acceptLicense \ --no-prompt \ --noPropertiesFile \ @@ -55,7 +83,7 @@ BACKEND_TYPE=${BACKEND_TYPE:-je} BACKEND_DB_DIRECTORY=${BACKEND_DB_DIRECTORY:-db} echo "creating backend: $BACKEND_TYPE db-directory: ${BACKEND_DB_DIRECTORY}" -/opt/opendj/bin/dsconfig create-backend -h localhost -p $ADMIN_PORT --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" \ +/opt/opendj/bin/dsconfig create-backend -h localhost -p $ADMIN_PORT --bindDN "$ROOT_USER_DN" --bindPasswordFile "$PASSWORD_FILE" \ --backend-name=userRoot --type $BACKEND_TYPE --set base-dn:$BASE_DN --set "db-directory:$BACKEND_DB_DIRECTORY" \ --set enabled:true --no-prompt --trustAll || exit 1 @@ -65,13 +93,12 @@ if [ "$ADD_BASE_ENTRY" = "--addBaseEntry" ]; then echo "generating sample data..." /opt/opendj/bin/makeldif -o $BASE_TEMPLATE -c suffix="$BASE_DN" -c numusers=$SAMPLE_DATA /opt/opendj/template/config/MakeLDIF/example.template || exit 1 /opt/opendj/bin/import-ldif --ldifFile $BASE_TEMPLATE \ - --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" || exit 1 + --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPasswordFile "$PASSWORD_FILE" || exit 1 else echo "creating base entry..." - BASE_TEMPLATE=$(mktemp) echo "branch: $BASE_DN" > $BASE_TEMPLATE /opt/opendj/bin/import-ldif --templateFile $BASE_TEMPLATE \ - --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPassword "$ROOT_PASSWORD" || exit 1 + --backendID=userRoot --bindDN "$ROOT_USER_DN" --bindPasswordFile "$PASSWORD_FILE" || exit 1 fi rm $BASE_TEMPLATE fi @@ -85,16 +112,19 @@ if [ -d /opt/opendj/bootstrap/schema/ ]; then echo "Loading initial schema:" for file in /opt/opendj/bootstrap/schema/*; do echo "Loading $file ..." - /opt/opendj/bin/ldapmodify -D "$ROOT_USER_DN" -h localhost -p $PORT -w $ROOT_PASSWORD -f $file + /opt/opendj/bin/ldapmodify -D "$ROOT_USER_DN" -h localhost -p $PORT -j "$PASSWORD_FILE" -f "$file" done fi if [ -d /opt/opendj/bootstrap/data/ ]; then - #allow pre encoded passwords + # allow pre encoded passwords; the port is named, as the tool would otherwise go to 4444 + # whatever ADMIN_PORT the server listens on, and the entries carrying them would be refused /opt/opendj/bin/dsconfig \ set-password-policy-prop \ + -h localhost \ + -p $ADMIN_PORT \ --bindDN "$ROOT_USER_DN" \ - --bindPassword "$ROOT_PASSWORD" \ + --bindPasswordFile "$PASSWORD_FILE" \ --policy-name "Default Password Policy" \ --set allow-pre-encoded-passwords:true \ --trustAll \ @@ -102,6 +132,6 @@ if [ -d /opt/opendj/bootstrap/data/ ]; then for file in /opt/opendj/bootstrap/data/*; do echo "Loading $file ..." - /opt/opendj/bin/ldapmodify -D "$ROOT_USER_DN" -h localhost -p $PORT -w $ROOT_PASSWORD -f $file --continueOnError + /opt/opendj/bin/ldapmodify -D "$ROOT_USER_DN" -h localhost -p $PORT -j "$PASSWORD_FILE" -f "$file" --continueOnError done fi diff --git a/opendj-packages/opendj-docker/run.sh b/opendj-packages/opendj-docker/run.sh index d1ca2bdc81..4a311d4911 100755 --- a/opendj-packages/opendj-docker/run.sh +++ b/opendj-packages/opendj-docker/run.sh @@ -34,9 +34,16 @@ cd /opt/opendj BOOTSTRAP_COMPLETE=${BOOTSTRAP_COMPLETE:-/opt/opendj/.bootstrap-complete} rm -f "$BOOTSTRAP_COMPLETE" -# A replicate.sh killed before its EXIT trap ran leaves the root password in /dev/shm, and on -# Kubernetes that outlives the container: the pod keeps its /dev/shm across container restarts -rm -f /dev/shm/opendj-replicate.* +# A replicate.sh or a setup.sh killed before its EXIT trap ran leaves the root password in +# /dev/shm, and on Kubernetes that outlives the container: the pod keeps its /dev/shm across +# container restarts. It is also shared by all the containers of the pod, and another one may +# be bootstrapping right now, so only the files of this container are removed: they carry +# ADMIN_PORT in their name, which the containers of a pod cannot share as they share one +# network namespace. Containers in distinct network namespaces that share /dev/shm through +# --ipc=host, and listen on the same ADMIN_PORT, are not told apart. /tmp belongs to this +# container alone, so a password file left there is removed whatever its name. +rm -f /dev/shm/opendj-replicate."$ADMIN_PORT".* +rm -f /dev/shm/opendj-setup-password."$ADMIN_PORT".* /tmp/opendj-setup-password.* #if default data folder exists do not change it if [ ! -d ./db ]; then