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:
@@ -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"
|
||||
|
||||
@@ -263,6 +263,7 @@ except Exception as exc:
|
||||
|
||||
source "$(dirname "$0")/certs.sh"
|
||||
|
||||
download_llm_ca_certs
|
||||
install_all_certs
|
||||
install_ca_to_nss
|
||||
|
||||
|
||||
235
docker/certs.sh
235
docker/certs.sh
@@ -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}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user