mirror of
https://github.com/osixia/container-openldap.git
synced 2026-11-06 12:40:36 -04:00
feat: restart openldap when tls file contents change
Add an optional TLS watcher with content fingerprints to ignore duplicate notifications and unchanged Kubernetes projections. Use explicit paths under /run/container/tmp for runtime temporary files and require baseimage 2.1.0. Cover TLS renewal and read-only /tmp.
This commit is contained in:
@@ -6,6 +6,8 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
### Added
|
||||
|
||||
- Add optional `openldap-tls-reload` service to restart slapd after TLS file
|
||||
contents change, ignoring duplicate events and unchanged projected volumes.
|
||||
- Add optional file-based auditing with `OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED`
|
||||
and `OPENLDAP_BOOTSTRAP_AUDITLOG_FILE`; load `auditlog.so` automatically when enabled.
|
||||
- Add `OPENLDAP_BOOTSTRAP_DATA_LAST_BIND` and `OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION`
|
||||
@@ -18,6 +20,8 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
### Fixed
|
||||
|
||||
- Create bootstrap, schema conversion and TLS watcher temporary files under
|
||||
`/run/container/tmp` instead of relying on a writable `/tmp`.
|
||||
- Render bootstrap LDIF templates in a temporary writable copy, allowing read-only
|
||||
sources and Kubernetes projected-volume symlinks without modifying source files.
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
ARG BASE_IMAGE="osixia/baseimage:alpine-2.0.0"
|
||||
ARG BASE_IMAGE="osixia/baseimage:alpine-2.1.0"
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG IMAGE="osixia/openldap:develop"
|
||||
|
||||
@@ -185,6 +185,8 @@ docker run --name openldap --hostname ldap.example.org -p 389:3890 -p 636:6360 \
|
||||
|
||||
If you also set `OPENLDAP_BOOTSTRAP_TLS_REQUIRED=true`, bootstrap adds TLS-required access rules. That setting is stricter than plain TLS enablement and is intended for deployments where non-TLS client access should be rejected.
|
||||
|
||||
The optional `openldap-tls-reload` service restarts slapd when certificate contents change. See [certificate renewal](https://opensource.osixia.net/projects/container-images/openldap/tls/#renewing-certificates).
|
||||
|
||||
## 🔁 Replication
|
||||
|
||||
Set `OPENLDAP_BOOTSTRAP_REPLICATION=true` to bootstrap syncrepl replication.
|
||||
|
||||
@@ -23,6 +23,9 @@ After the shared version check, `tests/` runs integration scenarios on each buil
|
||||
- default bootstrap;
|
||||
- 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
|
||||
projected volumes and duplicate events, and preserve intentional service stops;
|
||||
run bootstrap, schema conversion and TLS renewals with `/tmp` mounted read-only;
|
||||
- alternative schemas (`nis`, `virtualMail`);
|
||||
- auditlog: automatic module loading, writes, custom paths, rotation and restarts;
|
||||
- last successful binds: disabled by default, precision throttling, failed binds
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ func main() {
|
||||
// image
|
||||
|
||||
var OpenLDAPImage = &config.Image{
|
||||
BaseImage: "osixia/baseimage:alpine-2.0.0",
|
||||
BaseImage: "osixia/baseimage:alpine-2.1.0",
|
||||
Distribution: config.Alpine,
|
||||
|
||||
Name: "osixia/openldap",
|
||||
|
||||
@@ -21,6 +21,7 @@ var scenarios = []scenario{
|
||||
{"default bootstrap", Default},
|
||||
{"read-only bootstrap templates", ReadOnlyTemplates},
|
||||
{"alternative schemas", AlternativeSchemas},
|
||||
{"TLS certificate reload", TLSReload},
|
||||
{"audit logging", AuditLog},
|
||||
{"last successful binds", LastBind},
|
||||
{"refint empty-group placeholders", RefintNothing},
|
||||
|
||||
Vendored
+126
@@ -0,0 +1,126 @@
|
||||
#!/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
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# All runtime scratch files must work with /tmp mounted read-only.
|
||||
if touch /tmp/openldap-write-probe 2>/dev/null; then
|
||||
fail '/tmp is unexpectedly writable'
|
||||
fi
|
||||
cat > /run/container/runtime-temp.schema <<'SCHEMA'
|
||||
attributetype ( 1.3.6.1.4.1.4203.666.11.999.1 NAME 'runtimeTempAttribute' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
|
||||
SCHEMA
|
||||
openldap-ctl schema2ldif /run/container/runtime-temp.schema > /run/container/schema-conversion.log
|
||||
test -s /run/container/runtime-temp.ldif
|
||||
|
||||
mkdir /run/container/projected
|
||||
ln -s /run/container/certs1 /run/container/projected/..data
|
||||
for name in tls.crt tls.key ca.crt; do
|
||||
ln -s "..data/${name}" "/run/container/projected/${name}"
|
||||
done
|
||||
|
||||
container --kill-all=false -x openldap -x openldap-bootstrap -x openldap-upgrade \
|
||||
-x openldap-tls-reload > /run/container/tls-container.log 2>&1 &
|
||||
runner=$!
|
||||
trap 'kill -TERM "${runner}" 2>/dev/null || true; wait "${runner}" || true' EXIT
|
||||
|
||||
serial() {
|
||||
openssl s_client -connect 127.0.0.1:6360 </dev/null 2>/dev/null | openssl x509 -noout -serial
|
||||
}
|
||||
wait_serial() {
|
||||
local expected="$1"
|
||||
for _ in {1..150}; do
|
||||
if [ "$(serial 2>/dev/null || true)" = "${expected}" ]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
cat /run/container/tls-container.log
|
||||
fail "TLS certificate did not become ${expected}"
|
||||
}
|
||||
rotate() {
|
||||
ln -s "$1" /run/container/projected/..next
|
||||
mv -Tf /run/container/projected/..next /run/container/projected/..data
|
||||
}
|
||||
restart_count() {
|
||||
grep -c 'TLS file contents changed; restarting OpenLDAP' /run/container/tls-container.log || true
|
||||
}
|
||||
assert_no_restart() {
|
||||
sleep 3
|
||||
[ "$(cat /run/container/processes/openldap.pid)" = "${original_pid}" ] || fail 'unchanged TLS files restarted slapd'
|
||||
[ "$(restart_count)" = "${expected_restarts}" ] || fail 'unexpected TLS restart'
|
||||
}
|
||||
expected1=$(openssl x509 -in /run/container/certs1/tls.crt -noout -serial)
|
||||
expected2=$(openssl x509 -in /run/container/certs2/tls.crt -noout -serial)
|
||||
wait_serial "${expected1}"
|
||||
for _ in {1..100}; do
|
||||
if grep -q 'Watching changes on' /run/container/tls-container.log; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
sleep 0.2
|
||||
original_pid=$(cat /run/container/processes/openldap.pid)
|
||||
expected_restarts=0
|
||||
|
||||
# Metadata events and identical rewrites must not restart slapd.
|
||||
touch /run/container/certs1/tls.crt
|
||||
cp /run/container/certs1/tls.crt /run/container/copy.crt
|
||||
cat /run/container/copy.crt > /run/container/certs1/tls.crt
|
||||
assert_no_restart
|
||||
|
||||
# Even a forced Kubernetes-style projection with identical bytes is ignored.
|
||||
cp -R /run/container/certs1 /run/container/certs-identical
|
||||
rotate /run/container/certs-identical
|
||||
assert_no_restart
|
||||
|
||||
# Multiple events from one renewal must cause exactly one restart.
|
||||
rotate /run/container/certs2
|
||||
wait_serial "${expected2}"
|
||||
[ "$(cat /run/container/processes/openldap.pid)" != "${original_pid}" ] || fail 'slapd was not restarted'
|
||||
original_pid=$(cat /run/container/processes/openldap.pid)
|
||||
expected_restarts=1
|
||||
assert_no_restart
|
||||
|
||||
# A second renewal still works and duplicate callbacks remain harmless.
|
||||
rotate /run/container/certs1
|
||||
wait_serial "${expected1}"
|
||||
[ "$(cat /run/container/processes/openldap.pid)" != "${original_pid}" ] || fail 'second renewal did not restart slapd'
|
||||
original_pid=$(cat /run/container/processes/openldap.pid)
|
||||
expected_restarts=2
|
||||
assert_no_restart
|
||||
|
||||
# Ordinary directory mounts: replacing symlinks with identical regular files
|
||||
# must not restart slapd either, then a direct multi-file renewal restarts once.
|
||||
for name in tls.crt tls.key ca.crt; do
|
||||
cp -L "/run/container/projected/${name}" "/run/container/${name}"
|
||||
mv -f "/run/container/${name}" "/run/container/projected/${name}"
|
||||
done
|
||||
assert_no_restart
|
||||
for name in tls.crt tls.key ca.crt; do
|
||||
cp "/run/container/certs2/${name}" "/run/container/projected/${name}"
|
||||
done
|
||||
wait_serial "${expected2}"
|
||||
[ "$(cat /run/container/processes/openldap.pid)" != "${original_pid}" ] || fail 'direct renewal did not restart slapd'
|
||||
original_pid=$(cat /run/container/processes/openldap.pid)
|
||||
expected_restarts=3
|
||||
assert_no_restart
|
||||
|
||||
# A watcher event must not undo an intentional administrative stop.
|
||||
container processes stop openldap
|
||||
for name in tls.crt tls.key ca.crt; do
|
||||
cp "/run/container/certs1/${name}" "/run/container/projected/${name}"
|
||||
done
|
||||
sleep 3
|
||||
[ -e /run/container/processes/openldap.down ] || fail 'intentional stop was undone'
|
||||
[ "$(restart_count)" = 3 ] || fail 'stopped slapd was restarted'
|
||||
kill -0 "${runner}"
|
||||
echo 'TLS reload checks: OK'
|
||||
@@ -0,0 +1,37 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
// TLSReload verifies certificate rotation on new TLS connections by restarting only after file contents change.
|
||||
func TLSReload(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c := withEnv(image, merge(passwords, env{
|
||||
"OPENLDAP_BOOTSTRAP_TLS": "true",
|
||||
"OPENLDAP_BOOTSTRAP_TLS_CERT": "/run/container/projected/tls.crt",
|
||||
"OPENLDAP_BOOTSTRAP_TLS_CERT_KEY": "/run/container/projected/tls.key",
|
||||
"OPENLDAP_BOOTSTRAP_TLS_CA_CERT": "/run/container/projected/ca.crt",
|
||||
})).WithUser("0").
|
||||
WithDirectory("/run/container/certs1", certificates(client, []string{"localhost"}), dagger.ContainerWithDirectoryOpts{Owner: ldapUser}).
|
||||
WithDirectory("/run/container/certs2", certificates(client, []string{"localhost"}), dagger.ContainerWithDirectoryOpts{Owner: ldapUser}).
|
||||
WithNewFile("/run/container/tls-reload-test.sh", tlsReloadCheck).
|
||||
WithExec([]string{"container", "packages", "install", "openssl"}).
|
||||
WithMountedDirectory("/tmp", client.Directory(), dagger.ContainerWithMountedDirectoryOpts{ReadOnly: true}).
|
||||
WithUser(ldapUser)
|
||||
_, err = expect(ctx, c, true, []string{"TLS reload checks: OK"}, "container", "run", "--", "bash", "/run/container/tls-reload-test.sh")
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS reload: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testdata/tls-reload.sh
|
||||
var tlsReloadCheck string
|
||||
@@ -16,7 +16,7 @@ fi
|
||||
|
||||
container log info "Bootstrapping OpenLDAP ..."
|
||||
|
||||
BOOTSTRAP_TMP_DIR="$(mktemp -d)"
|
||||
BOOTSTRAP_TMP_DIR="$(mktemp -d /run/container/tmp/openldap-bootstrap.XXXXXX)"
|
||||
trap 'rm -rf "${BOOTSTRAP_TMP_DIR}"' EXIT
|
||||
|
||||
bootstrap_env() {
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#!/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
|
||||
|
||||
# Group file events from the same renewal, then compare contents, not timestamps
|
||||
# or symlink targets. The baseimage watcher runs callbacks sequentially.
|
||||
sleep 1
|
||||
if ! fingerprint=$(sha256sum -- "${OPENLDAP_BOOTSTRAP_TLS_CERT}" "${OPENLDAP_BOOTSTRAP_TLS_CERT_KEY}" \
|
||||
"${OPENLDAP_BOOTSTRAP_TLS_CA_CERT}" | sha256sum); then
|
||||
container log error "Cannot read TLS files; OpenLDAP was not restarted"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${fingerprint}" = "$(cat "${OPENLDAP_TLS_RELOAD_STATE_DIR:?}/fingerprint")" ]; then
|
||||
container log debug "TLS file contents unchanged; skipping restart"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Preserve an intentional stop requested through the baseimage CLI.
|
||||
if [ -e /run/container/processes/openldap.down ] || [ ! -e /run/container/processes/openldap.pid ]; then
|
||||
container log warning "TLS files changed but OpenLDAP is not running; skipping restart"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
container log info "TLS file contents changed; restarting OpenLDAP ..."
|
||||
if container processes stop openldap && container processes start openldap; then
|
||||
printf '%s\n' "${fingerprint}" > "${OPENLDAP_TLS_RELOAD_STATE_DIR}/fingerprint"
|
||||
else
|
||||
container log error "Failed to restart OpenLDAP after TLS files changed"
|
||||
fi
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/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
|
||||
|
||||
OPENLDAP_TLS_RELOAD_STATE_DIR=$(mktemp -d /run/container/tmp/openldap-tls-reload.XXXXXX)
|
||||
export OPENLDAP_TLS_RELOAD_STATE_DIR
|
||||
trap 'rm -rf "${OPENLDAP_TLS_RELOAD_STATE_DIR:?}"' EXIT
|
||||
|
||||
sha256sum -- "${OPENLDAP_BOOTSTRAP_TLS_CERT}" "${OPENLDAP_BOOTSTRAP_TLS_CERT_KEY}" \
|
||||
"${OPENLDAP_BOOTSTRAP_TLS_CA_CERT}" | sha256sum > "${OPENLDAP_TLS_RELOAD_STATE_DIR}/fingerprint"
|
||||
|
||||
# Keep this shell to clean up its private state when the watcher exits.
|
||||
container watch "${OPENLDAP_BOOTSTRAP_TLS_CERT}" "${OPENLDAP_BOOTSTRAP_TLS_CERT_KEY}" \
|
||||
"${OPENLDAP_BOOTSTRAP_TLS_CA_CERT}" --exec /container/services/openldap-tls-reload/assets/reload.sh
|
||||
@@ -18,8 +18,8 @@ fi
|
||||
SCHEMAS=$1
|
||||
DEPENDENCIES=${2:-${OPENLDAP_CTL_SCHEMA2LDIF_DEPENDENCIES}}
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
TMP_FILE=$(mktemp)
|
||||
TMP_DIR=$(mktemp -d /run/container/tmp/openldap-schema.XXXXXX)
|
||||
TMP_FILE=$(mktemp /run/container/tmp/openldap-schema-config.XXXXXX)
|
||||
|
||||
cleanup() {
|
||||
rm -rf "${TMP_DIR}"
|
||||
|
||||
Reference in New Issue
Block a user