Files
ss-tools/docker/certs.sh
busya 18b9f2e94e fix(certs): restore HTTP CA auto-download and test edge matrix
Restore LLM_CA_CERT_URLS support for corporate PKI HTTP cert delivery.
Downloaded certificates are installed into the llm/ CA category and share
the same system trust + NSS import path as CERTS_PATH-mounted certs.

Changes:
- certs.sh: add download_llm_ca_certs with HTTP-only curl policy,
  PEM/DER handling, retries, llm/ storage, hash symlinks for llm+custom
- backend/agent entrypoints: run download before install_all_certs
- compose/env/docs: expose LLM_CA_CERT_URLS again and update ADR-0009
- integration tests: cover PEM/DER/CER, invalid certs, non-HTTP skips,
  query-string filenames, private/server skips, empty inputs, combined
  LLM_CA_CERT_URLS + CERTS_PATH channels, and NSS imports

Verified:
- 194 passed, 1 skipped integration tests
- full backend container smoke passed with testcontainers PostgreSQL
- docker bundle rebuilt for 0.5.0
2026-07-07 10:23:49 +03:00

252 lines
8.8 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# #region docker.certs [C:3] [TYPE Module] [SEMANTICS docker,certs,ssl,ca,trust]
# @BRIEF Shared certificate installation functions for all containers.
# @LAYER Infrastructure
# @PRE CERTS_PATH points to mounted /opt/certs directory (may be empty or absent).
# @POST Corporate CA certificates installed into system trust store and NSS DB.
# @INVARIANT server.crt, server.key, server.p12, *.key, *.p12, *.pfx are never imported as CA.
set -euo pipefail
# ── normalize_cert PEM/DER detection + conversion ──────────────────
normalize_cert() {
local input="$1"
local output="$2"
if openssl x509 -in "$input" -inform PEM -noout 2>/dev/null; then
cp "$input" "$output"
return 0
fi
if openssl x509 -in "$input" -inform DER -noout 2>/dev/null; then
openssl x509 -in "$input" -inform DER -out "$output" -outform PEM 2>/dev/null
return 0
fi
return 1
}
# ── download_llm_ca_certs download certs from HTTP URLs ────────────
# @RATIONALE HTTP download from corporate PKI (e.g. pki.rusal.com) is the
# primary delivery mechanism for CA certificates. No chicken-and-egg TLS
# problem because URLs use plain HTTP, not HTTPS.
# @REJECTED HTTP-only HTTPS redirect was rejected — PKI servers may redirect
# HTTP→HTTPS using the same CA chain we are downloading (true chicken-and-egg).
download_llm_ca_certs() {
local urls="${LLM_CA_CERT_URLS:-}"
if [ -z "$urls" ]; then
return 0
fi
local work_dir="/tmp/llm-ca-certs"
local target_dir="/usr/local/share/ca-certificates/llm"
mkdir -p "$work_dir" "$target_dir"
echo "[certs] === Скачивание CA-сертификатов из LLM_CA_CERT_URLS ==="
local index=0 downloaded=0
for url in $urls; do
url="$(echo "$url" | xargs)" # trim
[ -z "$url" ] && continue
index=$((index + 1))
case "$url" in
http://*) ;;
*)
echo "[certs] ⚠️ пропускаем не-HTTP URL: ${url}"
continue
;;
esac
local filename
filename="$(basename "$url" | sed 's/[?#].*//')"
if [ -z "$filename" ] || [ "$filename" = "." ] || [ "$filename" = ".." ]; then
filename="llm-ca-${index}.crt"
fi
local raw_file="${work_dir}/${filename}"
local pem_file="${target_dir}/$(echo "$filename" | sed 's/\.[^.]*$//').crt"
echo "[certs] [${index}] Скачивание: ${url}"
local attempt=0 success=0
while [ $attempt -lt 3 ]; do
attempt=$((attempt + 1))
if curl -fsSL --proto '=http' --proto-redir '=http' --connect-timeout 10 --max-time 30 -o "$raw_file" "$url" 2>/dev/null; then
success=1
break
fi
echo "[certs] ⚠ попытка ${attempt}/3 не удалась, повтор через 2с..."
sleep 2
done
if [ "$success" -eq 0 ]; then
echo "[certs] ❌ Не удалось скачать: ${url} — пропускаем"
continue
fi
# Detect format: PEM or DER
if openssl x509 -in "$raw_file" -inform PEM -noout 2>/dev/null; then
echo "[certs] ✓ Формат: PEM"
cp "$raw_file" "$pem_file"
elif openssl x509 -in "$raw_file" -inform DER -noout 2>/dev/null; then
echo "[certs] ↻ Формат: DER — конвертируем в PEM"
if ! openssl x509 -in "$raw_file" -inform DER -out "$pem_file" -outform PEM 2>&1; then
echo "[certs] ❌ Ошибка конвертации DER→PEM — пропускаем"
rm -f "$raw_file" "${pem_file}"
continue
fi
else
echo "[certs] ❌ Неизвестный формат (не PEM и не DER) — пропускаем"
rm -f "$raw_file"
continue
fi
chmod 644 "$pem_file"
echo "[certs] ✅ Установлен: $(basename "$pem_file")"
downloaded=$((downloaded + 1))
rm -f "$raw_file"
done
if [ "$downloaded" -gt 0 ]; then
echo "[certs] Скачано и установлено: ${downloaded}"
fi
rm -rf "$work_dir"
}
# ── install_all_certs single unified entry point ───────────────────
install_all_certs() {
local certs_dir="${CERTS_PATH:-/opt/certs}"
if [ ! -d "$certs_dir" ]; then
echo "[certs] CERTS_PATH=${certs_dir} не существует пропускаем"
fi
local target="/usr/local/share/ca-certificates/custom"
mkdir -p "$target"
if [ -d "$certs_dir" ]; then
echo "[certs] Устанавливаем корпоративные сертификаты из ${certs_dir}..."
local installed=0 skipped=0 invalid=0
for f in "$certs_dir"/*; do
[ -f "$f" ] || continue
local name
name="$(basename "$f")"
# Skip non-CA server/private files
case "$(echo "$name" | tr '[:upper:]' '[:lower:]')" in
server.crt|server.key|server.pem|*.key|*.p12|*.pfx)
echo "[certs] ⏭️ пропускаем: ${name} (server/private файл)"
skipped=$((skipped + 1))
continue
;;
esac
# Accept .crt, .pem, .cer, .der
case "$(echo "$name" | tr '[:upper:]' '[:lower:]')" in
*.crt|*.pem|*.cer|*.der)
;;
*)
echo "[certs] ⚠️ неизвестный формат: ${name} пропускаем"
skipped=$((skipped + 1))
continue
;;
esac
local out_name="${name%.*}.crt"
local out_file="${target}/${out_name}"
if normalize_cert "$f" "$out_file"; then
echo "[certs] ✅ ${name} (→ ${out_name})"
installed=$((installed + 1))
else
echo "[certs] ❌ невалидный сертификат: ${name}"
invalid=$((invalid + 1))
fi
done
echo "[certs] Установлено: ${installed}, пропущено: ${skipped}, невалидно: ${invalid}"
fi
# ]]] SED_END_MARKER — do not remove, used by sed in update script
# Update system CA store (picks up both custom/ and llm/ dirs)
if command -v update-ca-certificates &>/dev/null; then
echo "[certs] Обновляем системное хранилище CA-сертификатов..."
update-ca-certificates --fresh 2>&1 | sed 's/^/[certs] /'
fi
# Create hash symlinks for OpenSSL capath (scan both custom/ and llm/)
echo "[certs] Создаём хеш-симлинки..."
local sym_count=0
for ca_dir in /usr/local/share/ca-certificates/custom /usr/local/share/ca-certificates/llm; do
[ -d "$ca_dir" ] || continue
for cert_file in "$ca_dir"/*.crt; do
[ -f "$cert_file" ] || continue
local cert_name
cert_name="$(basename "$cert_file" .crt)"
local hash_val
hash_val="$(openssl x509 -in "$cert_file" -noout -hash 2>/dev/null || true)"
if [ -n "$hash_val" ]; then
local suffix=0
local link_path="/etc/ssl/certs/${hash_val}.${suffix}"
local already_linked=0
while [ -L "$link_path" ]; do
if [ "$(readlink "$link_path")" = "$cert_file" ]; then
already_linked=1
break
fi
suffix=$((suffix + 1))
link_path="/etc/ssl/certs/${hash_val}.${suffix}"
done
if [ "$already_linked" -eq 0 ] && [ ! -L "$link_path" ]; then
ln -sf "$cert_file" "$link_path"
sym_count=$((sym_count + 1))
fi
fi
done
done
echo "[certs] Создано хеш-симлинок: ${sym_count}"
}
# ── install_ca_to_nss NSS DB for Chromium/Playwright ──────────────
install_ca_to_nss() {
if ! command -v certutil &>/dev/null; then
return 0
fi
local nssdb="${HOME}/.pki/nssdb"
mkdir -p "$nssdb"
if [ ! -f "${nssdb}/cert9.db" ]; then
certutil -N -d "sql:${nssdb}" --empty-password >/dev/null 2>&1 || true
fi
local nss_count=0
echo "[certs] Импортируем сертификаты в NSS DB..."
for ca_dir in /usr/local/share/ca-certificates/custom /usr/local/share/ca-certificates/llm; do
[ -d "$ca_dir" ] || continue
local prefix
prefix="$(basename "$ca_dir")"
for cert_file in "$ca_dir"/*.crt; do
[ -f "$cert_file" ] || continue
local cert_name
cert_name="$(basename "$cert_file" .crt)"
certutil -A -d "sql:${nssdb}" -t "C,," -n "${prefix}-${cert_name}" -i "$cert_file" 2>/dev/null && nss_count=$((nss_count + 1)) || true
done
done
echo "[certs] Импортировано в NSS: ${nss_count}"
}
# ── skip_server_files remove server certs from CA dir ──────────────
skip_server_files_filter() {
# Used by frontend to skip server.crt from being installed as CA
local certs_dir="${1:-/opt/certs}"
[ -d "$certs_dir" ] || return 0
for f in "$certs_dir"/*.crt "$certs_dir"/*.pem; do
[ -f "$f" ] || continue
case "$(basename "$f" | tr '[:upper:]' '[:lower:]')" in
server.crt|server.key) continue ;;
esac
echo "$f"
done
}
# #endregion docker.certs