Repository navigation
feat: support custom CA certificates from the data volume #4841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
365062f
3f00080
a0e038a
10518dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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!" | ||
|
|
@@ -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 | ||
| 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" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When 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 | ||
|
|
@@ -283,6 +326,7 @@ prepare_runtime_files() { | |
| wait_for_django_secret | ||
| fi | ||
| ensure_saml_certificate | ||
| install_custom_ca_certificates | ||
| } | ||
|
|
||
| ensure_runtime_user() { | ||
|
|
||
| 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" |
Uh oh!
There was an error while loading. Please reload this page.