#!/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