refactor(ssl): centralize SSL trust management, remove LLM_SSL_VERIFY

Centralized SSL via one contract: CERTS_PATH=/opt/certs mounted into all containers.

Backend:
  - NEW: backend/src/core/ssl.py — system_ssl_context(), httpx_verify(),
    cert_dir_inventory()
  - LLMClient._get_ssl_verify() → delegates to core.ssl
  - _llm_async_http._get_verify() → delegates to core.ssl
  - Removed LLM_SSL_VERIFY env reading from all runtime code

Docker:
  - NEW: docker/certs.sh — shared cert installer (PEM/DER/cer to .crt conversion,
    update-ca-certificates, hash symlinks, NSS import)
  - NEW: docker/agent.entrypoint.sh — agent entrypoint with cert installation
  - backend.entrypoint.sh → uses certs.sh instead of install_llm_ca_certs
  - Dockerfile.agent → adds ca-certificates, openssl, entrypoint

Compose:
  - Removed LLM_CA_CERT_URLS and LLM_SSL_VERIFY from all compose files
  - Added CERTS_PATH volume mount to agent (dev + enterprise)
  - Added certs volume mount to backend/agent in dev compose

Env examples:
  - Removed LLM_SSL_VERIFY, LLM_CA_CERT_URLS from .env.example,
    .env.enterprise-clean.example, .env.current.example, .env.master.example,
    backend/.env.example
  - Enhanced CERTS_PATH comments with accepted formats

Diagnostics:
  - diag_container.py: removed LLM_* checks, added CERTS_PATH inventory,
    uses core.ssl for context creation

Tests:
  - Updated test_llm_analysis_service, test_llm_async_http,
    test_client_headers to verify centralized ssl context (no env disable)
  - 4/4 SSL tests pass
This commit is contained in:
2026-07-06 21:00:28 +03:00
parent 43e3f4135e
commit 8fd23f7ea1
16 changed files with 1200 additions and 1048 deletions

View File

@@ -16,9 +16,9 @@ FROM python:3.11-slim
WORKDIR /app
# Install system deps for pdfplumber + psycopg (v3 needs libpq)
# Install system deps for pdfplumber + psycopg (v3 needs libpq) + certs
RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1 libglib2.0-0 libpq5 && rm -rf /var/lib/apt/lists/*
libgl1 libglib2.0-0 libpq5 ca-certificates openssl && rm -rf /var/lib/apt/lists/*
# Python dependencies — agent runtime (gradio, langgraph, httpx, pdfplumber, ...)
# Без sentence-transformers — embedding router безопасно отключается при ImportError.
@@ -47,10 +47,15 @@ COPY backend/src/core/logger.py /app/backend/src/core/logger.py
COPY backend/src/core/ws_log_handler.py /app/backend/src/core/ws_log_handler.py
COPY backend/src/agent/ /app/backend/src/agent/
# Entrypoint for corporate CA cert installation
COPY docker/certs.sh /app/docker/certs.sh
COPY docker/agent.entrypoint.sh /app/docker/agent.entrypoint.sh
RUN chmod +x /app/docker/agent.entrypoint.sh /app/docker/certs.sh
# Gradio server
ENV GRADIO_SERVER_NAME=0.0.0.0
ENV GRADIO_SERVER_PORT=7860
WORKDIR /app/backend
CMD ["python", "-m", "src.agent.run"]
ENTRYPOINT ["/app/docker/agent.entrypoint.sh"]
# #endregion docker.agent.Dockerfile

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# #region docker.agent.entrypoint [C:3] [TYPE Module] [SEMANTICS docker,agent,entrypoint,certs]
# @BRIEF Agent container entrypoint — install corporate CA certs, then start agent.
# @LAYER Infrastructure
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CERTS_PATH="${CERTS_PATH:-/opt/certs}"
if [ -f "$SCRIPT_DIR/certs.sh" ]; then
source "$SCRIPT_DIR/certs.sh"
install_all_certs
else
echo "[agent-entry] certs.sh not found — skipping corporate CA installation"
fi
exec python -m src.agent.run
# #endregion docker.agent.entrypoint

View File

@@ -497,8 +497,9 @@ except Exception as exc:
# MAIN
# ======================================================================
install_certificates
install_llm_ca_certs
source "$(dirname "$0")/certs.sh"
install_all_certs
install_ca_to_nss
# ── C1: Wait for database before any migration logic ──

158
docker/certs.sh Normal file
View File

@@ -0,0 +1,158 @@
#!/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
}
# ── 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
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)
# valid cert extension
;;
*)
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
if [ "$installed" -eq 0 ]; then
echo "[certs] Нет CA-сертификатов для установки"
return 0
fi
echo "[certs] Установлено: ${installed}, пропущено: ${skipped}, невалидно: ${invalid}"
# Update system CA store
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
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
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
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 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"
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
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