Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions start
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ service_runs_migrations() {
}

NGINX_PID_FILE=/run/nginx.pid
CUSTOM_CA_CERTS_DIR=/app/data/ssl/ca-certificates
SYSTEM_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
CUSTOM_CA_BUNDLE=/tmp/custom-ca-certificates.crt

fail_dep() {
>&2 echo "$1 not running!"
Expand Down Expand Up @@ -274,6 +277,46 @@ ensure_saml_certificate() {
fi
}

install_custom_ca_certificates() {
# Build a combined CA bundle (system certificates plus user-provided
# ones from the data volume) in a runtime-writable location, then point
# Python (requests), OpenSSL, libcurl and git at it. The system trust
# store is not writable for the container's unprivileged user, and the
# bundle is rebuilt from scratch on every start, so stale certificates
# never linger.
local certs=()
local cert
if [[ -d $CUSTOM_CA_CERTS_DIR ]]; then
shopt -s nullglob
certs=("$CUSTOM_CA_CERTS_DIR"/*.crt)
shopt -u nullglob
fi
if ((${#certs[@]} == 0)); then
return 0
Comment thread
nijel marked this conversation as resolved.
fi
echo "Adding ${#certs[@]} custom CA certificate(s) from $CUSTOM_CA_CERTS_DIR to the runtime CA bundle..."
# Separate inputs with newlines: a source file without a trailing newline
# would otherwise glue PEM blocks together (e.g. "-----END CERTIFICATE----------BEGIN CERTIFICATE-----"),
# producing a bundle that OpenSSL/Python reject.
{
cat "$SYSTEM_CA_BUNDLE"
printf '\n'
for cert in "${certs[@]}"; do
cat "$cert"
printf '\n'
done
} > "$CUSTOM_CA_BUNDLE"
chmod 0644 "$CUSTOM_CA_BUNDLE"
export REQUESTS_CA_BUNDLE="$CUSTOM_CA_BUNDLE"
export SSL_CERT_FILE="$CUSTOM_CA_BUNDLE"
export CURL_CA_BUNDLE="$CUSTOM_CA_BUNDLE"
export GIT_SSL_CAINFO="$CUSTOM_CA_BUNDLE"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Export the bundle through PGSSLROOTCERT

When POSTGRES_SSL_MODE is verify-ca or verify-full, the database-version check is enabled, and PostgreSQL is signed only by a CA mounted here, this export list does not configure libpq. The libpq environment-variable documentation states that PGSSLROOTCERT behaves like the sslrootcert parameter, whose documented default is ~/.postgresql/root.crt; SSL_CERT_FILE is not its CA setting. The psql invocation in check_postgres_version therefore rejects the server before startup despite the mounted CA, so export this bundle as PGSSLROOTCERT as well.

Useful? React with 👍 / 👎.

# libpq (psql) ignores SSL_CERT_FILE; without PGSSLROOTCERT the
# check_postgres_version probe rejects servers signed by a custom CA
# when POSTGRES_SSL_MODE=verify-ca/verify-full.
export PGSSLROOTCERT="$CUSTOM_CA_BUNDLE"
}

prepare_runtime_files() {
prepare_ssh_keys
ensure_data_volume_writable
Expand All @@ -283,6 +326,7 @@ prepare_runtime_files() {
wait_for_django_secret
fi
ensure_saml_certificate
install_custom_ca_certificates
}

ensure_runtime_user() {
Expand Down
3 changes: 3 additions & 0 deletions tests/run
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ echo 'Running Docker build arguments tests'

echo 'Running image signing tests'
./tests/test-sign-image-tags

echo 'Running custom CA certificate tests'
./tests/test-custom-ca-certificates
85 changes: 85 additions & 0 deletions tests/test-custom-ca-certificates
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash
# Tests for install_custom_ca_certificates in the start script: the function
# is extracted verbatim and exercised against stub directories.

set -eu

test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT HUP INT TERM

# Extract the path variables and the function itself from start.
awk '/^CUSTOM_CA_CERTS_DIR=|^SYSTEM_CA_BUNDLE=|^CUSTOM_CA_BUNDLE=/' start > "$test_dir/vars.sh"
awk '/^install_custom_ca_certificates\(\) \{/,/^\}/' start > "$test_dir/function.sh"
grep -q 'GIT_SSL_CAINFO' "$test_dir/function.sh"
grep -q 'PGSSLROOTCERT' "$test_dir/function.sh"

# Stub "system" bundle used as the base of the combined bundle.
printf 'system-ca\n' > "$test_dir/system.crt"

# Evaluate vars + function in a subshell per case. env -u strips inherited
# CA variables so a proxy or custom CA configured on the host cannot leak
# into the assertions.
run_case() {
env -u REQUESTS_CA_BUNDLE -u SSL_CERT_FILE -u CURL_CA_BUNDLE -u GIT_SSL_CAINFO -u PGSSLROOTCERT bash -c "
set -eu
. '$test_dir/vars.sh'
. '$test_dir/function.sh'
CUSTOM_CA_CERTS_DIR='$1'
SYSTEM_CA_BUNDLE='$test_dir/system.crt'
CUSTOM_CA_BUNDLE='$test_dir/bundle.crt'
install_custom_ca_certificates
echo \"exports: \${REQUESTS_CA_BUNDLE:-unset} \${SSL_CERT_FILE:-unset} \${CURL_CA_BUNDLE:-unset} \${GIT_SSL_CAINFO:-unset} \${PGSSLROOTCERT:-unset}\"
" | tail -n 1
}

fail=0
check() { # $1 = label, $2 = expected, $3 = actual
if [ "$2" = "$3" ]; then
echo "ok: $1"
else
echo "FAIL: $1 (expected [$2], got [$3])"
fail=1
fi
}

# Case 1: no source directory -> no-op, no exports, no bundle.
rm -f "$test_dir/bundle.crt"
out=$(run_case "$test_dir/no-such-dir")
check 'no dir: no exports' 'exports: unset unset unset unset unset' "$out"
check 'no dir: no bundle written' 'missing' "$(cat "$test_dir/bundle.crt" 2> /dev/null || echo missing)"

# Case 2: two certificates -> combined bundle written, exports set.
mkdir -p "$test_dir/src2"
printf 'cert-a\n' > "$test_dir/src2/internal-a.crt"
printf 'cert-b\n' > "$test_dir/src2/internal-b.crt"
touch "$test_dir/src2/ignored.pem"
rm -f "$test_dir/bundle.crt"
out=$(run_case "$test_dir/src2")
check 'two certs: exports point at bundle' "exports: $test_dir/bundle.crt $test_dir/bundle.crt $test_dir/bundle.crt $test_dir/bundle.crt $test_dir/bundle.crt" "$out"
check 'two certs: bundle = system + custom' 'system-ca cert-a cert-b' "$(tr '\n' ' ' < "$test_dir/bundle.crt" | tr -s ' ' | sed 's/ $//')"
check 'two certs: bundle is world-readable' '644' "$(stat -c '%a' "$test_dir/bundle.crt")"

# Case 4: a certificate without a trailing newline must not glue PEM blocks.
mkdir -p "$test_dir/src4"
printf 'cert-no-newline' > "$test_dir/src4/internal-a.crt"
printf 'cert-after\n' > "$test_dir/src4/internal-b.crt"
rm -f "$test_dir/bundle.crt"
run_case "$test_dir/src4" > /dev/null
check 'no trailing newline: first cert on its own line' '1' "$(
grep -xc 'cert-no-newline' "$test_dir/bundle.crt"
true
)"
check 'no trailing newline: second cert on its own line' '1' "$(
grep -xc 'cert-after' "$test_dir/bundle.crt"
true
)"
check 'no trailing newline: no glued content' '0' "$(grep -c 'cert-no-newlinecert-after' "$test_dir/bundle.crt" || true)"

# Case 3: empty source directory -> no-op.
mkdir -p "$test_dir/src3"
rm -f "$test_dir/bundle.crt"
out=$(run_case "$test_dir/src3")
check 'empty dir: no exports' 'exports: unset unset unset unset unset' "$out"
check 'empty dir: no bundle written' 'missing' "$(cat "$test_dir/bundle.crt" 2> /dev/null || echo missing)"

exit "$fail"
Loading