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
This commit is contained in:
2026-07-07 10:23:49 +03:00
parent 1e61f098bd
commit 18b9f2e94e
8 changed files with 487 additions and 86 deletions

View File

@@ -9,6 +9,7 @@ CERTS_PATH="${CERTS_PATH:-/opt/certs}"
if [ -f "$SCRIPT_DIR/certs.sh" ]; then
source "$SCRIPT_DIR/certs.sh"
download_llm_ca_certs
install_all_certs
else
echo "[agent-entry] certs.sh not found — skipping corporate CA installation"

View File

@@ -263,6 +263,7 @@ except Exception as exc:
source "$(dirname "$0")/certs.sh"
download_llm_ca_certs
install_all_certs
install_ca_to_nss

View File

@@ -24,96 +24,185 @@ normalize_cert() {
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} не существует пропускаем"
return 0
fi
local target="/usr/local/share/ca-certificates/custom"
mkdir -p "$target"
echo "[certs] Устанавливаем корпоративные сертификаты из ${certs_dir}..."
local installed=0 skipped=0 invalid=0
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")"
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
# 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)
# valid cert extension
;;
*)
echo "[certs] ⚠️ неизвестный формат: ${name} пропускаем"
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}"
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
if normalize_cert "$f" "$out_file"; then
echo "[certs] ✅ ${name} (→ ${out_name})"
installed=$((installed + 1))
else
echo "[certs] ❌ невалидный сертификат: ${name}"
invalid=$((invalid + 1))
fi
done
if [ "$installed" -eq 0 ]; then
echo "[certs] Нет CA-сертификатов для установки"
return 0
echo "[certs] Установлено: ${installed}, пропущено: ${skipped}, невалидно: ${invalid}"
fi
# ]]] SED_END_MARKER — do not remove, used by sed in update script
echo "[certs] Установлено: ${installed}, пропущено: ${skipped}, невалидно: ${invalid}"
# Update system CA store
# 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
# Create hash symlinks for OpenSSL capath (scan both custom/ and llm/)
echo "[certs] Создаём хеш-симлинки..."
local sym_count=0
for cert_file in "$target"/*.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}"
while [ -L "$link_path" ]; do
if [ "$(readlink "$link_path")" = "$cert_file" ]; then
break 2
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
suffix=$((suffix + 1))
link_path="/etc/ssl/certs/${hash_val}.${suffix}"
done
if [ ! -L "$link_path" ]; then
ln -sf "$cert_file" "$link_path"
sym_count=$((sym_count + 1))
fi
fi
done
done
echo "[certs] Создано хеш-симлинок: ${sym_count}"
}
@@ -124,19 +213,23 @@ install_ca_to_nss() {
if ! command -v certutil &>/dev/null; then
return 0
fi
local target="/usr/local/share/ca-certificates/custom"
if [ ! -d "$target" ] || [ -z "$(ls -A "$target" 2>/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 cert_file in "$target"/*.crt; do
[ -f "$cert_file" ] || continue
local cert_name
cert_name="$(basename "$cert_file" .crt)"
certutil -A -d "sql:${nssdb}" -t "C,," -n "custom-${cert_name}" -i "$cert_file" 2>/dev/null && nss_count=$((nss_count + 1)) || true
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}"
}