From 0f1b360684326437b4886b0a75cebe6d982e30f7 Mon Sep 17 00:00:00 2001 From: lendry Date: Fri, 26 Jun 2026 08:41:19 +0300 Subject: [PATCH] add custom cert install sh --- .env.example | 7 +- install.sh | 220 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 213 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 3e46655..4b3c39f 100644 --- a/.env.example +++ b/.env.example @@ -11,9 +11,14 @@ NGINX_MODE=auto NGINX_HTTP_PORT=80 NGINX_HTTPS_PORT=443 -# SSL: none (HTTP) | selfsigned (HTTPS локально) | letsencrypt (интернет) +# SSL: none (HTTP) | selfsigned (HTTPS локально) | custom (свои файлы) | letsencrypt (интернет) SSL_TYPE=none +# Папка со своими сертификатами (для SSL_TYPE=custom); install.sh копирует файлы в nginx/certs/custom/ +SSL_CERT_DIR= +SSL_CERT_FULLCHAIN= +SSL_CERT_KEY= + # Домены без протокола (install.sh подставит PUBLIC_* URL) DOMAIN_API= DOMAIN_FRONTEND= diff --git a/install.sh b/install.sh index 866d80c..5505639 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.2.4" +SCRIPT_VERSION="2.2.5" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -20,6 +20,8 @@ NGINX_ENABLED="/etc/nginx/sites-enabled" CERTBOT_WEBROOT="/var/www/certbot" LOCAL_CONF_DIR="${ROOT_DIR}/nginx/conf.d" LOCAL_CERTS_DIR="${ROOT_DIR}/nginx/certs" +CUSTOM_CERT_TARGET_DIR="${LOCAL_CERTS_DIR}/custom" +DEFAULT_CERT_UPLOAD_DIR="${LOCAL_CERTS_DIR}/upload" COMPOSE_OVERRIDE="${ROOT_DIR}/docker-compose.override.yml" STATE_FILE="${ROOT_DIR}/.idp-install.json" INFRA_SECRETS_FILE="${ROOT_DIR}/.idp-infra.secrets" @@ -42,6 +44,9 @@ CLI_DOMAIN_MINIO="" CLI_DOMAIN_MINIO_CONSOLE="" CLI_EMAIL="" CLI_SSL_TYPE="" +CLI_CERT_DIR="" +CLI_CERT_FULLCHAIN="" +CLI_CERT_KEY="" CLI_NGINX_HTTP_PORT="" CLI_NGINX_HTTPS_PORT="" @@ -64,7 +69,7 @@ ${BOLD}Lendry ID v${SCRIPT_VERSION}${NC} — установка и управл ./install.sh Интерактивное меню ./install.sh --install Полная установка ./install.sh --configure-domains Изменить домены / Nginx / SSL - ./install.sh --renew-ssl Обновить сертификаты Let's Encrypt + ./install.sh --renew-ssl Обновить сертификаты (Let's Encrypt / custom / selfsigned) ./install.sh --restart Перезапуск контейнеров ./install.sh --docker-prune Только очистка Docker (без пересборки) ./install.sh --fix-proxy Исправить 502: host Nginx + порты 3000/3002/3003 @@ -74,7 +79,10 @@ ${BOLD}Lendry ID v${SCRIPT_VERSION}${NC} — установка и управл ${BOLD}Режим локальной сети (без интернета / AD-домены .local .lpr):${NC} --intranet Локальная сеть: HTTP или самоподписанный HTTPS - --ssl TYPE none | selfsigned | letsencrypt (по умолчанию в --intranet: none) + --ssl TYPE none | selfsigned | custom | letsencrypt + --cert-dir PATH Папка со своими сертификатами (для --ssl custom) + --cert-fullchain FILE Файл сертификата в --cert-dir (по умолчанию: fullchain.pem / cert.pem) + --cert-key FILE Файл ключа в --cert-dir (по умолчанию: privkey.pem / key.pem) --nginx-http-port PORT HTTP-порт Nginx (если 80 занят, например 8080) --nginx-https-port PORT HTTPS-порт Nginx (если 443 занят, например 8443) --offline Не скачивать Docker из интернета (только если уже установлен) @@ -101,6 +109,12 @@ ${BOLD}Переменные окружения:${NC} COMPOSE_PARALLEL_LIMIT=4 Параллельная сборка сервисов (по умолчанию 4) ${BOLD}Примеры:${NC} + ./install.sh --intranet --install --yes \\ + --api-domain api.idpmvk.lpr \\ + --frontend-domain id.idpmvk.lpr \\ + --docs-domain docs.idpmvk.lpr \\ + --ssl custom --cert-dir /opt/idp-certs + ./install.sh --intranet --install --yes \\ --api-domain api.idpmvk.lpr \\ --frontend-domain id.idpmvk.lpr \\ @@ -138,6 +152,9 @@ parse_args() { --intranet) INSTALL_INTRANET=1; shift ;; --offline) OFFLINE=1; shift ;; --ssl) CLI_SSL_TYPE="${2:-}"; shift 2 ;; + --cert-dir) CLI_CERT_DIR="${2:-}"; shift 2 ;; + --cert-fullchain) CLI_CERT_FULLCHAIN="${2:-}"; shift 2 ;; + --cert-key) CLI_CERT_KEY="${2:-}"; shift 2 ;; --nginx-mode) NGINX_MODE="${2:-auto}"; shift 2 ;; --nginx-http-port) CLI_NGINX_HTTP_PORT="${2:-}"; shift 2 ;; --nginx-https-port) CLI_NGINX_HTTPS_PORT="${2:-}"; shift 2 ;; @@ -192,7 +209,7 @@ preflight_checks() { NGINX_MODE="docker" fi [[ -f docker-compose.yml ]] || fail "Запустите скрипт из корня репозитория (рядом с docker-compose.yml)" - mkdir -p "$LOCAL_CONF_DIR" "$LOCAL_CERTS_DIR" + mkdir -p "$LOCAL_CONF_DIR" "$LOCAL_CERTS_DIR" "$DEFAULT_CERT_UPLOAD_DIR" } detect_nginx_mode() { @@ -726,7 +743,7 @@ derive_public_urls() { fi local scheme="http" - [[ "$ssl_type" == "letsencrypt" || "$ssl_type" == "selfsigned" ]] && scheme="https" + [[ "$ssl_type" == "letsencrypt" || "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]] && scheme="https" PUBLIC_API_URL="${scheme}://${DOMAIN_API}" PUBLIC_FRONTEND_URL="${scheme}://${DOMAIN_FRONTEND}" @@ -760,7 +777,7 @@ derive_public_urls() { if [[ "$INSTALL_LOCAL" -eq 1 ]]; then INSTALL_MODE="local" - elif [[ "$INSTALL_INTRANET" -eq 1 || "$ssl_type" == "none" || "$ssl_type" == "selfsigned" ]]; then + elif [[ "$INSTALL_INTRANET" -eq 1 || "$ssl_type" == "none" || "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]]; then INSTALL_MODE="intranet" elif [[ -n "${DOMAIN_API:-}" ]]; then INSTALL_MODE="production" @@ -803,14 +820,15 @@ choose_ssl_type_interactive() { echo " 1) HTTP — локальная сеть, без сертификатов (http://api.idpmvk.lpr)" echo " 2) HTTPS самоподписанный — локальная сеть, браузер покажет предупреждение" echo " 3) HTTPS Let's Encrypt — нужен интернет и публичный DNS" + echo " 4) HTTPS свои сертификаты — загрузите fullchain.pem и privkey.pem в папку на сервере" echo "" local ssl_choice if [[ "$INSTALL_INTRANET" -eq 1 ]]; then - read -r -p "Выберите [1-3] [1]: " ssl_choice + read -r -p "Выберите [1-4] [1]: " ssl_choice ssl_choice="${ssl_choice:-1}" else - read -r -p "Выберите [1-3] [3]: " ssl_choice + read -r -p "Выберите [1-4] [3]: " ssl_choice ssl_choice="${ssl_choice:-3}" fi @@ -818,6 +836,7 @@ choose_ssl_type_interactive() { 1) SSL_TYPE="none" ;; 2) SSL_TYPE="selfsigned" ;; 3) SSL_TYPE="letsencrypt" ;; + 4) SSL_TYPE="custom" ;; *) SSL_TYPE="$default" ;; esac } @@ -899,10 +918,14 @@ configure_domains_interactive() { choose_ssl_type_interactive "$(env_get SSL_TYPE none)" case "$SSL_TYPE" in - none | selfsigned | letsencrypt) ;; - *) fail "Некорректный SSL_TYPE: $SSL_TYPE (none | selfsigned | letsencrypt)" ;; + none | selfsigned | custom | letsencrypt) ;; + *) fail "Некорректный SSL_TYPE: $SSL_TYPE (none | selfsigned | custom | letsencrypt)" ;; esac + if [[ "$SSL_TYPE" == "custom" ]]; then + prompt_custom_cert_settings + fi + if [[ "$SSL_TYPE" == "letsencrypt" ]]; then if [[ "$INSTALL_INTRANET" -eq 1 && "$NON_INTERACTIVE" -ne 1 ]]; then warn "Let's Encrypt требует доступ из интернета. Для чистой локальной сети выберите HTTP или selfsigned." @@ -1082,6 +1105,9 @@ persist_domain_env() { env_set PUBLIC_WS_URL "${PUBLIC_WS_URL}" env_set USE_NGINX_SSL "${USE_NGINX_SSL:-false}" env_set CERTBOT_EMAIL "${CERTBOT_EMAIL:-}" + env_set SSL_CERT_DIR "${SSL_CERT_DIR:-$(env_get SSL_CERT_DIR "")}" + env_set SSL_CERT_FULLCHAIN "${SSL_CERT_FULLCHAIN:-$(env_get SSL_CERT_FULLCHAIN "")}" + env_set SSL_CERT_KEY "${SSL_CERT_KEY:-$(env_get SSL_CERT_KEY "")}" env_set MINIO_PUBLIC_ENDPOINT "${MINIO_PUBLIC_ENDPOINT:-localhost:9000}" env_set MINIO_USE_SSL "${MINIO_USE_SSL:-false}" @@ -1335,6 +1361,12 @@ nginx_cert_base_path() { else echo "${LOCAL_CERTS_DIR}/shared-intranet" fi + elif [[ "$ssl_type" == "custom" ]]; then + if [[ "$NGINX_MODE" == "docker" ]]; then + echo "/etc/nginx/certs/custom" + else + echo "${LOCAL_CERTS_DIR}/custom" + fi elif [[ "$NGINX_MODE" == "docker" ]]; then echo "/etc/nginx/certs/${domain}" else @@ -1371,6 +1403,150 @@ collect_self_signed_sans() { (IFS=,; echo "${sans[*]}") } +find_custom_cert_file() { + local dir="$1" + local preferred="${2:-}" + shift 2 || true + local -a fallbacks=("$@") + local name + + if [[ -n "$preferred" ]]; then + [[ -f "${dir}/${preferred}" ]] || fail "Файл не найден: ${dir}/${preferred}" + echo "${dir}/${preferred}" + return 0 + fi + + for name in "${fallbacks[@]}"; do + if [[ -f "${dir}/${name}" ]]; then + echo "${dir}/${name}" + return 0 + fi + done + + return 1 +} + +resolve_custom_cert_source_dir() { + if [[ -n "$CLI_CERT_DIR" ]]; then + SSL_CERT_DIR="$CLI_CERT_DIR" + elif [[ -n "${SSL_CERT_DIR:-}" ]]; then + : + elif [[ -n "$(env_get SSL_CERT_DIR "")" ]]; then + SSL_CERT_DIR="$(env_get SSL_CERT_DIR "")" + elif [[ -d "$DEFAULT_CERT_UPLOAD_DIR" ]]; then + SSL_CERT_DIR="$DEFAULT_CERT_UPLOAD_DIR" + fi +} + +prompt_custom_cert_settings() { + resolve_custom_cert_source_dir + local default_dir="${SSL_CERT_DIR:-$DEFAULT_CERT_UPLOAD_DIR}" + local default_fullchain="${CLI_CERT_FULLCHAIN:-$(env_get SSL_CERT_FULLCHAIN "")}" + local default_key="${CLI_CERT_KEY:-$(env_get SSL_CERT_KEY "")}" + + if [[ "$NON_INTERACTIVE" -eq 1 ]]; then + SSL_CERT_DIR="${SSL_CERT_DIR:-$default_dir}" + SSL_CERT_FULLCHAIN="${SSL_CERT_FULLCHAIN:-$default_fullchain}" + SSL_CERT_KEY="${SSL_CERT_KEY:-$default_key}" + [[ -d "$SSL_CERT_DIR" ]] || fail "Папка с сертификатами не найдена: $SSL_CERT_DIR (укажите --cert-dir PATH)" + return 0 + fi + + echo "" + echo -e "${BOLD}Свои SSL-сертификаты:${NC}" + echo " Положите на сервер файлы сертификата и ключа, затем укажите папку." + echo " Поддерживаются имена: fullchain.pem / cert.pem и privkey.pem / key.pem" + echo " Рекомендуемая папка: ${DEFAULT_CERT_UPLOAD_DIR}/" + echo "" + + prompt SSL_CERT_DIR "Папка с сертификатами на сервере" "$default_dir" + [[ -d "$SSL_CERT_DIR" ]] || fail "Папка не найдена: $SSL_CERT_DIR" + + prompt_optional SSL_CERT_FULLCHAIN "Имя файла сертификата (полная цепочка, пусто = авто)" "$default_fullchain" + prompt_optional SSL_CERT_KEY "Имя файла приватного ключа (пусто = авто)" "$default_key" +} + +validate_custom_certificate_pair() { + local fullchain="$1" + local key="$2" + + [[ -f "$fullchain" ]] || fail "Файл сертификата не найден: $fullchain" + [[ -f "$key" ]] || fail "Файл ключа не найден: $key" + [[ -r "$fullchain" ]] || fail "Нет прав на чтение сертификата: $fullchain" + [[ -r "$key" ]] || fail "Нет прав на чтение ключа: $key" + + has_cmd openssl || return 0 + + openssl x509 -in "$fullchain" -noout >/dev/null 2>&1 \ + || fail "Некорректный файл сертификата: $fullchain" + + if openssl rsa -in "$key" -check -noout >/dev/null 2>&1 \ + || openssl ec -in "$key" -check -noout >/dev/null 2>&1 \ + || openssl pkey -in "$key" -noout >/dev/null 2>&1; then + : + else + fail "Некорректный файл приватного ключа: $key" + fi + + if ! openssl x509 -noout -modulus -in "$fullchain" 2>/dev/null \ + | cmp -s <(openssl rsa -noout -modulus -in "$key" 2>/dev/null) - 2>/dev/null \ + && ! openssl x509 -noout -modulus -in "$fullchain" 2>/dev/null \ + | cmp -s <(openssl ec -noout -modulus -in "$key" 2>/dev/null) - 2>/dev/null \ + && ! openssl x509 -noout -pubkey -in "$fullchain" 2>/dev/null \ + | cmp -s <(openssl pkey -pubout -in "$key" 2>/dev/null) - 2>/dev/null; then + warn "Не удалось проверить соответствие сертификата и ключа — продолжаем установку" + fi +} + +install_custom_certificates() { + local force="${1:-}" + load_env + + resolve_custom_cert_source_dir + SSL_CERT_DIR="${SSL_CERT_DIR:-$(env_get SSL_CERT_DIR "")}" + SSL_CERT_FULLCHAIN="${SSL_CERT_FULLCHAIN:-$(env_get SSL_CERT_FULLCHAIN "")}" + SSL_CERT_KEY="${SSL_CERT_KEY:-$(env_get SSL_CERT_KEY "")}" + + [[ -n "$SSL_CERT_DIR" ]] || fail "Не задана папка с сертификатами (SSL_CERT_DIR или --cert-dir)" + [[ -d "$SSL_CERT_DIR" ]] || fail "Папка с сертификатами не найдена: $SSL_CERT_DIR" + + local src_fullchain src_key + src_fullchain="$(find_custom_cert_file "$SSL_CERT_DIR" "$SSL_CERT_FULLCHAIN" \ + fullchain.pem cert.pem certificate.pem server.crt)" \ + || fail "Не найден файл сертификата в ${SSL_CERT_DIR}. Положите fullchain.pem или cert.pem, либо укажите --cert-fullchain" + src_key="$(find_custom_cert_file "$SSL_CERT_DIR" "$SSL_CERT_KEY" \ + privkey.pem key.pem server.key)" \ + || fail "Не найден файл ключа в ${SSL_CERT_DIR}. Положите privkey.pem или key.pem, либо укажите --cert-key" + + validate_custom_certificate_pair "$src_fullchain" "$src_key" + + if [[ "$force" != "--force" \ + && -f "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" \ + && -f "${CUSTOM_CERT_TARGET_DIR}/privkey.pem" \ + && cmp -s "$src_fullchain" "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" \ + && cmp -s "$src_key" "${CUSTOM_CERT_TARGET_DIR}/privkey.pem" ]]; then + log "Сертификаты уже установлены: ${CUSTOM_CERT_TARGET_DIR}/" + return 0 + fi + + log "Установка своих сертификатов из ${SSL_CERT_DIR}..." + mkdir -p "$CUSTOM_CERT_TARGET_DIR" + cp "$src_fullchain" "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" + cp "$src_key" "${CUSTOM_CERT_TARGET_DIR}/privkey.pem" + chmod 644 "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" 2>/dev/null || true + chmod 600 "${CUSTOM_CERT_TARGET_DIR}/privkey.pem" 2>/dev/null || true + + env_set SSL_CERT_DIR "$SSL_CERT_DIR" + env_set SSL_CERT_FULLCHAIN "$SSL_CERT_FULLCHAIN" + env_set SSL_CERT_KEY "$SSL_CERT_KEY" + + ok "Сертификаты установлены: ${CUSTOM_CERT_TARGET_DIR}/" + if has_cmd openssl; then + log "Срок действия: $(openssl x509 -in "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" -noout -enddate 2>/dev/null | cut -d= -f2-)" + log "Домены (SAN): $(openssl x509 -in "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" -noout -ext subjectAltName 2>/dev/null | sed 's/ *//g' || echo "не указаны")" + fi +} + generate_shared_self_signed_cert() { local cert_dir="${LOCAL_CERTS_DIR}/shared-intranet" local base sans cn @@ -1489,7 +1665,7 @@ server { } EOF - elif [[ "$ssl_type" == "selfsigned" ]]; then + elif [[ "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]]; then cat <