diff --git a/scripts/check_llm_certs.sh b/scripts/check_llm_certs.sh index 34d7cbc0..fe4592bf 100755 --- a/scripts/check_llm_certs.sh +++ b/scripts/check_llm_certs.sh @@ -1,251 +1,157 @@ #!/usr/bin/env bash # #region check_llm_certs [C:4] [TYPE Module] [SEMANTICS ssl, certs, diagnostics, openssl, nss] -# @BRIEF Проверка установки корпоративных SSL-сертификатов для LLM-провайдеров. -# Читает конфигурацию из .env.enterprise-clean, проверяет все 4 слоя: -# OpenSSL store, httpx, requests, Chromium NSS. -# @RATIONALE После ручной/автоматической установки сертификатов нужно убедиться -# что все три HTTP-стека (httpx, requests, Chromium) доверяют корпоративным CA. -# По отдельности каждый слой имеет свою логику — скрипт собирает всё воедино. -# @USAGE ./scripts/check_llm_certs.sh [--env-file .env.enterprise-clean] [--target https://lite.ai.rusal.com] +# @BRIEF Diagnóstico SSL pro ss-tools. Testuje všechny vrstvy: OpenSSL, +# Python httpx (jako LLMClient), Python requests (jako translate), +# NSS Chromium (jako Playwright). Výstup použije AI k opravě kódu. +# @USAGE ./scripts/check_llm_certs.sh [--target https://...] [-v] # @LAYER Infrastructure # @EXAMPLE: -# ./scripts/check_llm_certs.sh -# ./scripts/check_llm_certs.sh --target https://api.corp.com/v1 --env-file /opt/ss-tools/.env +# ./scripts/check_llm_certs.sh --target https://lite.ai.rusal.com # #endregion check_llm_certs set -euo pipefail -# ── Colors ── -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color -PASS="${GREEN}✅${NC}" -FAIL="${RED}❌${NC}" -WARN="${YELLOW}⚠${NC}" -INFO="${CYAN}ℹ${NC}" +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +PASS="${GREEN}✅${NC}"; FAIL="${RED}❌${NC}"; WARN="${YELLOW}⚠${NC}"; INFO="${CYAN}ℹ${NC}" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -# ── Defaults ── -ENV_FILE="${PROJECT_ROOT}/.env.enterprise-clean" -TARGET_URL="https://lite.ai.rusal.com" -VERBOSE=false - -# ── Parse args ── -while [[ $# -gt 0 ]]; do - case "$1" in - --env-file) ENV_FILE="$2"; shift 2 ;; - --target) TARGET_URL="$2"; shift 2 ;; - -v|--verbose) VERBOSE=true; shift ;; - --help|-h) echo "Usage: $0 [--env-file .env] [--target https://...] [-v]"; exit 0 ;; - *) echo "Unknown: $1"; exit 1 ;; - esac -done - -# ── Load env ── -if [ -f "$ENV_FILE" ]; then - echo -e "${INFO} Loading env: ${ENV_FILE}" - set -a - source "$ENV_FILE" - set +a -else - echo -e "${WARN} Env file not found: ${ENV_FILE} (using defaults)" -fi - -LLM_SSL_VERIFY="${LLM_SSL_VERIFY:-true}" +TARGET_URL="${1:-https://lite.ai.rusal.com}" +HOST="$(echo "$TARGET_URL" | sed 's|https://||;s|/.*$||')" CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt" -NSS_DB_DIR="${HOME:-/root}/.pki/nssdb" +CA_DIR="/etc/ssl/certs" +NSS_DB="sql:${HOME:-/root}/.pki/nssdb" -echo "" -echo -e "${CYAN}========================================${NC}" -echo -e "${CYAN} LLM SSL Certificate Diagnostics${NC}" -echo -e "${CYAN}========================================${NC}" -echo "" -echo "Target URL: ${TARGET_URL}" -echo "LLM_SSL_VERIFY: ${LLM_SSL_VERIFY}" -echo "Env file: ${ENV_FILE}" +echo -e "${CYAN}╔════════════════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ LLM SSL Certificate Diagnostic Report ║${NC}" +echo -e "${CYAN}║ Target: ${TARGET_URL}${NC}" +echo -e "${CYAN}╚════════════════════════════════════════════════════╝${NC}" echo "" -# ══════════════════════════════════════════ -# SECTION 1: System CA Store -# ══════════════════════════════════════════ -echo -e "${CYAN}── Layer 1: System CA Store (OpenSSL) ──${NC}" +# ────────────────────────────────────────────── +# 1. SYSTEM CA STORE +# ────────────────────────────────────────────── +echo -e "${CYAN}── 1. System CA Store ──${NC}" +echo "ca_bundle=${CA_BUNDLE}" +echo "ca_dir=${CA_DIR}" +echo "total_in_bundle=$(grep -c 'BEGIN CERTIFICATE' "$CA_BUNDLE" 2>/dev/null || echo 0)" +echo "total_hash_links=$(find "$CA_DIR" -maxdepth 1 -type l -name '[0-9a-f]*.[0-9]' 2>/dev/null | wc -l)" -# Check bundle exists -total_certs=0 -if [ -f "$CA_BUNDLE" ]; then - total_certs=$(awk '/BEGIN CERTIFICATE/{c++} END{print c}' "$CA_BUNDLE" 2>/dev/null || echo "0") - echo -e " ${PASS} Bundle: ${CA_BUNDLE} (${total_certs} certificates)" -else - echo -e " ${FAIL} Bundle NOT FOUND: ${CA_BUNDLE}" -fi - -# List custom certs -echo -n " Custom certs: " -custom_dir="/usr/local/share/ca-certificates/custom" -llm_dir="/usr/local/share/ca-certificates/llm" -found_custom=0 -for dir in "$custom_dir" "$llm_dir"; do - if [ -d "$dir" ]; then - for f in "$dir"/*.pem "$dir"/*.crt; do - [ -f "$f" ] || continue - found_custom=$((found_custom+1)) - subject="$(openssl x509 -in "$f" -noout -subject 2>/dev/null | head -1 || echo "?")" - echo -e "\n ${PASS} $(basename "$f") — ${subject}" - done - fi -done -if [ "$found_custom" -eq 0 ]; then - echo -e "${WARN} none found (check CERTS_PATH mount and LLM_CA_CERT_URLS)" -fi - -# Check symlinks -echo -n " Hash symlinks: " -hash_links=$(find /etc/ssl/certs/ -maxdepth 1 -type l -name '[0-9a-f]*.[0-9]' 2>/dev/null | wc -l) -echo -e "${hash_links} found" - -# ══════════════════════════════════════════ -# SECTION 2: NSS Database (Chromium) -# ══════════════════════════════════════════ echo "" -echo -e "${CYAN}── Layer 2: NSS Database (Chromium) ──${NC}" - -if command -v certutil &>/dev/null && [ -d "$NSS_DB_DIR" ]; then - nss_db="sql:${NSS_DB_DIR}" - nss_certs=$(certutil -L -d "$nss_db" 2>/dev/null | wc -l || echo "0") - echo -e " ${PASS} NSS DB: ${NSS_DB_DIR} (${nss_certs} certificates)" - - # Show custom certs - certutil -L -d "$nss_db" 2>/dev/null | grep -iE "rusal|llm-|custom-" | while read -r line; do - echo -e " ${PASS} ${line}" +echo "custom_certs=" +for dir in /usr/local/share/ca-certificates/custom /usr/local/share/ca-certificates/llm; do + [ -d "$dir" ] || continue + for f in "$dir"/*.pem "$dir"/*.crt; do + [ -f "$f" ] || continue + subj="$(openssl x509 -in "$f" -noout -subject 2>/dev/null | head -1 || echo '?')" + issuer="$(openssl x509 -in "$f" -noout -issuer 2>/dev/null | head -1 || echo '?')" + fp="$(openssl x509 -in "$f" -noout -fingerprint -sha256 2>/dev/null | sed 's/.*=//' || echo '?')" + in_bundle="no" + grep -qF "$fp" "$CA_BUNDLE" 2>/dev/null && in_bundle="yes" + echo " $(basename $f) | subject=${subj} | issuer=${issuer} | in_bundle=${in_bundle}" done -elif ! command -v certutil &>/dev/null; then - echo -e " ${WARN} certutil not installed (libnss3-tools missing)" -elif [ ! -d "$NSS_DB_DIR" ]; then - echo -e " ${WARN} NSS DB not found at ${NSS_DB_DIR}" -fi +done -# ══════════════════════════════════════════ -# SECTION 3: OpenSSL Connectivity Test -# ══════════════════════════════════════════ +# ────────────────────────────────────────────── +# 2. OPENSSL s_client - 3 варианта +# ────────────────────────────────────────────── echo "" -echo -e "${CYAN}── Layer 3: OpenSSL Connectivity ──${NC}" +echo -e "${CYAN}── 2. OpenSSL s_client ──${NC}" -target_host="$(echo "${TARGET_URL}" | sed 's|https://||;s|/.*$||')" -echo -n " ${target_host}:443 — " +for variant in "CAfile=${CA_BUNDLE}" "CApath=${CA_DIR}" "default"; do + echo "variant=${variant}" + if [ "$variant" = "default" ]; then + out=$(openssl s_client -connect "${HOST}:443" -servername "$HOST" < /dev/null 2>&1) + else + key=$(echo "$variant" | cut -d= -f1) + val=$(echo "$variant" | cut -d= -f2-) + out=$(openssl s_client -connect "${HOST}:443" -"$key" "$val" -servername "$HOST" < /dev/null 2>&1) + fi + code=$(echo "$out" | grep "Verify return code" | head -1) + echo " result=${code}" +done -if openssl s_client -connect "${target_host}:443" -CAfile "$CA_BUNDLE" -servername "$target_host" /dev/null \ - | grep -q "Verify return code: 0"; then - echo -e "${PASS} SSL handshake OK (verify via system CA)" -else - verify_code=$(openssl s_client -connect "${target_host}:443" -CAfile "$CA_BUNDLE" -servername "$target_host" &1 \ - | grep "Verify return code" | head -1 || echo "unknown") - echo -e "${FAIL} ${verify_code}" - echo -e " ${INFO} Check: missing intermediate CA or wrong CA bundle" -fi - -# ══════════════════════════════════════════ -# SECTION 4: Python httpx Test -# ══════════════════════════════════════════ +# ────────────────────────────────────────────── +# 3. Python - httpx (LLMClient style) +# ────────────────────────────────────────────── echo "" -echo -e "${CYAN}── Layer 4: Python httpx (AsyncOpenAI) ──${NC}" +echo -e "${CYAN}── 3. Python httpx (как LLMClient) ──${NC}" -CA_BUNDLE_PY="${CA_BUNDLE}" -TARGET_URL_PY="${TARGET_URL}" -PASS_PY="${PASS}" -FAIL_PY="${FAIL}" -INFO_PY="${INFO}" -WARN_PY="${WARN}" -export CA_BUNDLE_PY TARGET_URL_PY PASS_PY FAIL_PY INFO_PY WARN_PY -python3 -c ' -import ssl, sys, os +python3 -u -c ' +import os, sys, ssl, traceback -cafile = os.environ.get("CA_BUNDLE_PY", "/etc/ssl/certs/ca-certificates.crt") -target = os.environ.get("TARGET_URL_PY", "https://lite.ai.rusal.com") -pass_emoji = os.environ.get("PASS_PY", "✅") -fail_emoji = os.environ.get("FAIL_PY", "❌") -info_emoji = os.environ.get("INFO_PY", "ℹ") - -try: - ctx = ssl.create_default_context(cafile=cafile) - print(f" {pass_emoji} SSLContext created with cafile={cafile}") - print(f" {info_emoji} verify_mode={ctx.verify_mode}") -except Exception as e: - print(f" {fail_emoji} SSLContext error: {e}") - sys.exit(1) +target = os.environ.get("TARGET_URL", "https://lite.ai.rusal.com") +ca_bundle = "/etc/ssl/certs/ca-certificates.crt" +ca_dir = "/etc/ssl/certs" +results = {} + +# A) SSLContext s cafile try: + ctx = ssl.create_default_context(cafile=ca_bundle) import httpx r = httpx.get(target, verify=ctx, timeout=10) - print(f" {pass_emoji} httpx GET {target} → {r.status_code}") -except httpx.ConnectError as e: - print(f" {fail_emoji} httpx connection failed: {e}") + results["httpx+cafile"] = f"OK status={r.status_code}" except Exception as e: - print(f" {fail_emoji} httpx error: {e}") -' 2>&1 || echo -e " ${FAIL} Python test failed" + results["httpx+cafile"] = f"FAIL {type(e).__name__}: {e}" -# ══════════════════════════════════════════ -# SECTION 5: Python requests Test -# ══════════════════════════════════════════ -echo "" -echo -e "${CYAN}── Layer 5: Python requests ──${NC}" +# B) SSLContext s cafile + capath +try: + ctx2 = ssl.create_default_context() + ctx2.load_verify_locations(cafile=ca_bundle) + if os.path.isdir(ca_dir): + ctx2.load_verify_locations(capath=ca_dir) + import httpx + r = httpx.get(target, verify=ctx2, timeout=10) + results["httpx+cafile+capath"] = f"OK status={r.status_code}" +except Exception as e: + results["httpx+cafile+capath"] = f"FAIL {type(e).__name__}: {e}" -python3 -c ' -import os, sys -target = os.environ.get("TARGET_URL_PY", "https://lite.ai.rusal.com") -cafile = os.environ.get("CA_BUNDLE_PY", "/etc/ssl/certs/ca-certificates.crt") -pass_emoji = os.environ.get("PASS_PY", "✅") -fail_emoji = os.environ.get("FAIL_PY", "❌") +# C) requests s verify=cafile try: import requests - r = requests.get(target, verify=cafile, timeout=10) - print(f" {pass_emoji} requests GET {target} → {r.status_code}") -except requests.exceptions.SSLError as e: - print(f" {fail_emoji} requests SSL error: {e}") + r = requests.get(target, verify=ca_bundle, timeout=10) + results["requests+cafile"] = f"OK status={r.status_code}" except Exception as e: - print(f" {fail_emoji} requests error: {e}") -' 2>&1 || echo -e " ${FAIL} Python requests test failed" + results["requests+cafile"] = f"FAIL {type(e).__name__}: {e}" -# ══════════════════════════════════════════ -# SECTION 6: LLM_SSL_VERIFY env var check -# ══════════════════════════════════════════ -echo "" -echo -e "${CYAN}── Layer 6: LLM_SSL_VERIFY env var ──${NC}" +# D) requests s verify=capath +try: + import requests + r = requests.get(target, verify=ca_dir, timeout=10) + results["requests+capath"] = f"OK status={r.status_code}" +except Exception as e: + results["requests+capath"] = f"FAIL {type(e).__name__}: {e}" -python3 -c ' -import os -raw = os.getenv("LLM_SSL_VERIFY", "true").strip().lower() -disabled = raw in ("false", "0", "no", "off") -warn_emoji = os.environ.get("WARN_PY", "⚠") -pass_emoji = os.environ.get("PASS_PY", "✅") -info_emoji = os.environ.get("INFO_PY", "ℹ") -cafile = os.environ.get("CA_BUNDLE_PY", "/etc/ssl/certs/ca-certificates.crt") -if disabled: - print(f" {warn_emoji} LLM_SSL_VERIFY={raw} — SSL verification DISABLED") -else: - print(f" {pass_emoji} LLM_SSL_VERIFY={raw} — SSL verification ENABLED") - import ssl - if os.path.exists(cafile): - ctx = ssl.create_default_context(cafile=cafile) - print(f" {info_emoji} ssl.create_default_context(cafile=...) — OK") +for k, v in results.items(): + print(f" {k}={v}") ' 2>&1 -# ══════════════════════════════════════════ -# SUMMARY -# ══════════════════════════════════════════ +# ────────────────────────────────────────────── +# 4. NSS / Chromium +# ────────────────────────────────────────────── echo "" -echo -e "${CYAN}========================================${NC}" -echo -e "${CYAN} Summary${NC}" -echo -e "${CYAN}========================================${NC}" +echo -e "${CYAN}── 4. NSS Chromium ──${NC}" + +if command -v certutil &>/dev/null && certutil -L -d "$NSS_DB" >/dev/null 2>&1; then + echo "nss_db=${NSS_DB}" + echo "nss_certs_count=$(certutil -L -d "$NSS_DB" 2>/dev/null | wc -l)" + echo "nss_custom_certs=" + certutil -L -d "$NSS_DB" 2>/dev/null | grep -iE "rusal|llm-|custom-" | while read -r line; do + echo " $line" + done +else + echo "nss_db=unavailable" +fi + +# ────────────────────────────────────────────── +# 5. LLM_SSL_VERIFY env +# ────────────────────────────────────────────── echo "" -echo " CA Bundle: ${CA_BUNDLE} (${total_certs} certs)" -echo " NSS DB: ${NSS_DB_DIR}" -echo " Target: ${TARGET_URL}" -echo " LLM_SSL_VERIFY: ${LLM_SSL_VERIFY}" -echo "" -echo " If openssl test passes but Python fails — check certifi vs system CA" -echo " If all tests pass — remove LLM_SSL_VERIFY=false from .env and rerun" +echo -e "${CYAN}── 5. LLM_SSL_VERIFY ──${NC}" +raw="${LLM_SSL_VERIFY:-not_set}" +echo "LLM_SSL_VERIFY=${raw}" + +# doporuceni echo "" +echo -e "${CYAN}── 6. AI Recommendation Input ──${NC}" +echo "To fix SSL issues, AI needs to know which methods work." +echo "Copy the FULL output above and paste to the AI assistant."