#!/bin/bash # === Конфигурация скрипта === UPDATE_DIR="/opt/scripts/update" SCRIPT_NAME="ssh-monitor" REPO_URL="${REPO_URL:-}" GIT_BRANCH="${GIT_BRANCH:-main}" GIT_REF="${GIT_REF:-}" GIT_VERIFY_MODE="${GIT_VERIFY_MODE:-off}" GIT_ALLOW_RESET="${GIT_ALLOW_RESET:-0}" MANIFEST_VERIFIED=0 MANIFEST_STRICT=0 LOCAL_SCRIPT_PATH="/usr/local/bin/ssh-monitor" LOG_FILE="/var/log/update_script.log" # Режим отладки: 1 = включён, 0 = выключен DEBUG=0 DEPLOY_MODE=0 QUIET=0 # Итог последнего прогона (для лога и консоли) SUMMARY_SCRIPT="—" SUMMARY_SCRIPT_REASON="—" SUMMARY_SAC_CLIENT="—" SUMMARY_SAC_REASON="—" SUMMARY_CONFIG="—" SUMMARY_GIT="—" SUMMARY_SERVICE_RESTART="—" SUMMARY_UPDATER="—" SUMMARY_WATCHDOG="—" SUMMARY_HARDENING="—" SUMMARY_MANIFEST="—" CONFIG_FILE="/etc/ssh-monitor.conf" DEPLOY_INSTALL_PATH="/opt/scripts/update_ssh_monitor.sh" EXPECTED_HEARTBEAT_INTERVAL_MINUTES=240 EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE=$((EXPECTED_HEARTBEAT_INTERVAL_MINUTES * 60 + 3600)) SAC_SPOOL_DIR_DEFAULT="/var/lib/ssh-monitor/sac-spool" SYSTEMD_UNIT_PATH="/etc/systemd/system/ssh-monitor.service" LOCAL_WATCHDOG_PATH="/usr/local/bin/ssh-monitor-watchdog" WATCHDOG_SERVICE_PATH="/etc/systemd/system/ssh-monitor-watchdog.service" WATCHDOG_TIMER_PATH="/etc/systemd/system/ssh-monitor-watchdog.timer" # === Список переменных, объявленных в скрипте (для логирования при DEBUG=1) === SCRIPT_VARS=("UPDATE_DIR" "SCRIPT_NAME" "REPO_URL" "GIT_BRANCH" "LOCAL_SCRIPT_PATH" "LOG_FILE" "DEBUG" "REMOTE_SCRIPT_PATH" "LOCAL_CHECKSUM" "REMOTE_CHECKSUM") # === Функции === say_step() { printf '\n==> %s\n' "$1" } say_ok() { printf '[OK] %s\n' "$1" log_message "INFO" "$1" } say_warn() { printf '[WARN] %s\n' "$1" log_message "INFO" "WARNING: $1" } say_info() { printf ' %s\n' "$1" } show_help() { cat <<'EOF' update_ssh_monitor.sh — обновление или первичная установка ssh-monitor Использование: sudo ./update_ssh_monitor.sh обновление (git pull + checksum) sudo ./update_ssh_monitor.sh --deploy первичная установка / полный deploy sudo ./first_deploy.sh то же, что --deploy Опции: --deploy установить ssh-monitor, sac-client, пример конфига, systemd unit --debug подробный лог в /var/log/update_script.log --quiet не печатать шаги в stdout (только в $LOG_FILE) -h, --help эта справка Лог действий: /var/log/update_script.log (в конце каждого прогона — блок «Итог обновления»). При первой установке создаётся /etc/ssh-monitor.conf из example. При обновлении в существующий конфиг дописываются отсутствующие SAC-переменные и обязательные ключи безопасности (CONFIG_STRICT_PERMS, SAC_TLS_INSECURE и т.д.); выставляется UseSAC=fallback (Telegram/SMTP не перезаписываются). На каждом прогоне автоматически выставляются chmod/chown на конфиг, spool и state-файлы (основной сценарий — кнопка «Обновить» в SAC; cron/timer — опционально). Переменная REPO_URL обязательна (export, SAC или Environment= в systemd/cron). EOF } require_repo_url() { if [ -n "${REPO_URL:-}" ]; then return 0 fi local msg="ERROR: REPO_URL не задан. Задайте export REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git или Environment=REPO_URL=... в systemd unit / cron." echo "$msg" >&2 mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: REPO_URL не задан" >>"$LOG_FILE" 2>/dev/null || true exit 1 } default_required_config_value() { case "$1" in CONFIG_STRICT_PERMS) printf '0' ;; SAC_TLS_INSECURE) printf '0' ;; *) printf '' ;; esac } # Ключи SAC: дописываются из ssh-monitor.conf.example, если отсутствуют в /etc. SAC_CONFIG_KEYS=( UseSAC SAC_URL SAC_API_KEY SAC_SPOOL_DIR SAC_FAIL_COUNT_FILE SAC_FALLBACK_FAILURES SAC_TIMEOUT_SEC SAC_SPOOL_FLUSH_MAX_FILES SAC_SPOOL_MAX_AGE_HOURS ) SAC_USE_MODE_DEFAULT="fallback" # Обязательные ключи конфига (добавляются updater'ом, если отсутствуют). REQUIRED_CONFIG_KEYS=( CONFIG_STRICT_PERMS SAC_TLS_INSECURE ) find_ssh_monitor_conf_example() { local example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor.conf.example" if [ -f "$example" ]; then printf '%s\n' "$example" return 0 fi return 1 } config_conf_value() { local key="$1" file="$2" line val [ -f "$file" ] || return 1 line=$(grep -E "^[[:space:]]*${key}=" "$file" 2>/dev/null | tail -1) || return 1 val=${line#*=} val=${val#"${val%%[![:space:]]*}"} val=${val%"${val##*[![:space:]]}"} val=${val#\"} val=${val%\"} val=${val#\'} val=${val%\'} printf '%s\n' "$val" } config_example_value() { local key="$1" example="$2" config_conf_value "$key" "$example" 2>/dev/null || true } backup_config_if_needed() { local file="$1" flag_var="$2" if [ -z "${!flag_var:-}" ] || [ "${!flag_var}" -eq 0 ]; then cp "$file" "${file}.bak.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true printf -v "$flag_var" '%s' 1 fi } ensure_ssh_monitor_config_file() { local example CONFIG_CHANGED=0 example=$(find_ssh_monitor_conf_example) || { log_message "INFO" "WARNING: ssh-monitor.conf.example not found in clone" return 0 } if [ -f "$CONFIG_FILE" ]; then if [ "$DEPLOY_MODE" -eq 1 ]; then say_info "Уже существует: $CONFIG_FILE (дополним SAC-переменные при необходимости)" fi return 0 fi log_message "INFO" "Creating $CONFIG_FILE from ssh-monitor.conf.example" if [ "$DEPLOY_MODE" -eq 1 ]; then say_ok "Создан $CONFIG_FILE из ssh-monitor.conf.example" fi if ! cp "$example" "$CONFIG_FILE"; then log_message "INFO" "ERROR: Failed to copy ssh-monitor.conf.example to $CONFIG_FILE" return 1 fi chmod 600 "$CONFIG_FILE" 2>/dev/null || true chown root:root "$CONFIG_FILE" 2>/dev/null || true CONFIG_CHANGED=1 return 0 } sync_sac_config_keys() { local file="$CONFIG_FILE" local example backed_up=0 local key val current changed=0 added=0 [ -f "$file" ] || return 1 example=$(find_ssh_monitor_conf_example) || { log_message "INFO" "WARNING: ssh-monitor.conf.example not found; SAC merge skipped" return 0 } if ! config_key_exists "UseSAC" "$file"; then backup_config_if_needed "$file" backed_up { printf '\n# --- Security Alert Center (SAC) ---\n' printf '# off | exclusive | dual | fallback — см. security-alert-center docs/agent-integration.md\n' } >>"$file" log_message "INFO" "Config $file: добавлен блок SAC (ключи отсутствовали)." changed=1 fi for key in "${SAC_CONFIG_KEYS[@]}"; do if config_key_exists "$key" "$file"; then continue fi val=$(config_example_value "$key" "$example") if [ -z "$val" ] && [ "$key" != "SAC_API_KEY" ]; then case "$key" in UseSAC) val="$SAC_USE_MODE_DEFAULT" ;; SAC_SPOOL_DIR) val="$SAC_SPOOL_DIR_DEFAULT" ;; SAC_FAIL_COUNT_FILE) val="/var/lib/ssh-monitor/sac-fail.count" ;; SAC_FALLBACK_FAILURES) val="5" ;; SAC_TIMEOUT_SEC) val="45" ;; SAC_SPOOL_FLUSH_MAX_FILES) val="50" ;; SAC_SPOOL_MAX_AGE_HOURS) val="72" ;; SAC_URL) val="https://sac.kalinamall.ru" ;; esac fi backup_config_if_needed "$file" backed_up set_config_kv "$key" "$val" "$file" log_message "INFO" "Config $file: добавлен ${key}=${val:-<пусто>}" changed=1 added=$((added + 1)) done current=$(config_conf_value "UseSAC" "$file" 2>/dev/null || true) if [ "$current" != "$SAC_USE_MODE_DEFAULT" ]; then backup_config_if_needed "$file" backed_up set_config_kv "UseSAC" "$SAC_USE_MODE_DEFAULT" "$file" log_message "INFO" "Config $file: UseSAC ${current:-<нет>} → $SAC_USE_MODE_DEFAULT (deploy policy)." changed=1 fi current=$(config_conf_value "SAC_TIMEOUT_SEC" "$file" 2>/dev/null || true) if [ "$current" = "12" ]; then backup_config_if_needed "$file" backed_up set_config_kv "SAC_TIMEOUT_SEC" "45" "$file" log_message "INFO" "Config $file: SAC_TIMEOUT_SEC 12 → 45 (ingest timeout)." changed=1 fi if [ "$changed" -eq 0 ]; then return 1 fi if [ "$DEPLOY_MODE" -eq 1 ]; then if [ "$added" -gt 0 ]; then say_info "SAC: добавлено ключей — $added; UseSAC=$SAC_USE_MODE_DEFAULT." else say_info "SAC: UseSAC=$SAC_USE_MODE_DEFAULT." fi fi CONFIG_CHANGED=1 return 0 } sync_required_config_keys() { local file="$CONFIG_FILE" local example backed_up=0 local key val changed=0 [ -f "$file" ] || return 1 example=$(find_ssh_monitor_conf_example) || example="" for key in "${REQUIRED_CONFIG_KEYS[@]}"; do if config_key_exists "$key" "$file"; then continue fi val="$(config_example_value "$key" "$example")" if [ -z "$val" ]; then val="$(default_required_config_value "$key")" fi backup_config_if_needed "$file" backed_up if ! grep -qE '^[[:space:]]*# --- Security / updater ---' "$file" 2>/dev/null; then printf '\n# --- Security / updater (добавлено update_ssh_monitor.sh) ---\n' >>"$file" fi set_config_kv "$key" "$val" "$file" log_message "INFO" "Config $file: добавлен обязательный ${key}=${val}" changed=1 done if [ "$changed" -eq 0 ]; then return 1 fi CONFIG_CHANGED=1 return 0 } harden_ssh_monitor_config_perms() { [ -f "$CONFIG_FILE" ] || return 0 chmod 600 "$CONFIG_FILE" 2>/dev/null || true chown root:root "$CONFIG_FILE" 2>/dev/null || true } load_ssh_monitor_perms_lib() { local p candidates=() candidates+=("${LOCAL_SCRIPT_PATH%/*}/ssh-monitor-perms.sh") candidates+=("$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-perms.sh") for p in "${candidates[@]}"; do if [ -f "$p" ]; then # shellcheck source=/dev/null source "$p" return 0 fi done return 1 } # SAC-first: при каждом обновлении (SAC, cron или вручную) выставляет chmod/chown на конфиг, # spool, счётчики SAC и state-файлы из /etc/ssh-monitor.conf — без участия админа. apply_runtime_security_hardening() { local spool fail_count agent_id path key local -a state_keys defaults local i=0 log_action "security: применение прав (конфиг, spool, state) — основной путь через SAC/update" harden_ssh_monitor_config_perms chmod 750 "$DEPLOY_INSTALL_PATH" 2>/dev/null || true chown root:root "$DEPLOY_INSTALL_PATH" 2>/dev/null || true if ! load_ssh_monitor_perms_lib; then log_action "WARNING: ssh-monitor-perms.sh недоступен — только chmod конфига и базовых каталогов" mkdir -p /var/lib/ssh-monitor "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true chmod 700 /var/lib/ssh-monitor "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true chown root:root /var/lib/ssh-monitor "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true SUMMARY_HARDENING="частично (нет perms lib)" return 0 fi ssh_monitor_secure_dir "/var/lib/ssh-monitor" spool="$(config_conf_value SAC_SPOOL_DIR "$CONFIG_FILE" 2>/dev/null || echo "$SAC_SPOOL_DIR_DEFAULT")" fail_count="$(config_conf_value SAC_FAIL_COUNT_FILE "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/ssh-monitor/sac-fail.count")" agent_id="$(config_conf_value SAC_AGENT_ID_FILE "$CONFIG_FILE" 2>/dev/null || echo "/var/lib/ssh-monitor/agent_instance_id")" ssh_monitor_harden_path "$spool" ssh_monitor_harden_path "$fail_count" ssh_monitor_harden_path "$agent_id" state_keys=( LAST_HEARTBEAT_FILE LAST_REPORT_FILE LAST_SSH_CHECK_FILE LAST_SUDO_CHECK_FILE LAST_SECURITY_EVENTS_FILE LAST_LOGIND_CHECK_FILE BAN_LIST_FILE HEALTHCHECK_STATUS_FILE ) defaults=( /var/log/last_heartbeat.txt /var/log/last_daily_report.txt /var/log/last_ssh_check /var/log/last_sudo_check /var/log/last_security_events_check /var/log/last_logind_check /var/log/ssh_bans.txt "" ) for key in "${state_keys[@]}"; do path="$(config_conf_value "$key" "$CONFIG_FILE" 2>/dev/null || true)" if [ -z "$path" ]; then path="${defaults[$i]}" fi if [ -n "$path" ]; then ssh_monitor_harden_path "$path" fi i=$((i + 1)) done if [ -f /etc/ssh_monitor_whitelist.txt ]; then ssh_monitor_harden_path /etc/ssh_monitor_whitelist.txt fi SUMMARY_HARDENING="права применены" log_action "security: runtime paths hardened (spool, state, config)" } maybe_enable_deploy_for_missing_install() { if [ "$DEPLOY_MODE" -eq 1 ]; then return 0 fi if [ -f "$LOCAL_SCRIPT_PATH" ]; then return 0 fi log_action "ssh-monitor не найден в $LOCAL_SCRIPT_PATH — автоматический режим --deploy" DEPLOY_MODE=1 } set_config_kv() { local key="$1" val="$2" file="$3" esc esc=$(printf '%s' "$val" | sed -e 's/[\\&/]/\\&/g' -e 's/"/\\"/g') if grep -qE "^[[:space:]]*${key}=" "$file" 2>/dev/null; then sed -i -E "s/^[[:space:]]*${key}=.*/${key}=\"${esc}\"/" "$file" else printf '%s="%s"\n' "$key" "$esc" >>"$file" fi } config_key_exists() { local key="$1" file="$2" grep -qE "^[[:space:]]*${key}=" "$file" 2>/dev/null } config_key_mentioned() { local key="$1" file="$2" grep -qE "^[[:space:]]*(#.*)?${key}=" "$file" 2>/dev/null } sync_server_ipv4_hint_config() { local file="$CONFIG_FILE" backed_up=0 [ -f "$file" ] || return 1 if config_key_mentioned "SERVER_IPV4" "$file"; then return 1 fi backup_config_if_needed "$file" backed_up { printf '\n# --- Явный IPv4 для SAC host.ipv4 (раскомментируйте при необходимости) ---\n' printf '# SERVER_IPV4="10.0.0.10"\n' } >>"$file" log_message "INFO" "Config $file: добавлена подсказка # SERVER_IPV4." CONFIG_CHANGED=1 return 0 } sync_daily_report_enabled_config() { local file="$CONFIG_FILE" backed_up=0 current [ -f "$file" ] || return 1 if config_key_exists "DAILY_REPORT_ENABLED" "$file"; then current=$(config_conf_value "DAILY_REPORT_ENABLED" "$file" 2>/dev/null || true) if [ "$current" = "1" ]; then return 1 fi backup_config_if_needed "$file" backed_up set_config_kv "DAILY_REPORT_ENABLED" "1" "$file" log_message "INFO" "Config $file: DAILY_REPORT_ENABLED ${current:-<пусто>} → 1 (отчёт с агента)." else backup_config_if_needed "$file" backed_up { printf '\n# --- Суточный отчёт report.daily.ssh (1 = с агента; 0 = только SAC) ---\n' printf 'DAILY_REPORT_ENABLED="1"\n' } >>"$file" log_message "INFO" "Config $file: добавлен DAILY_REPORT_ENABLED=1." fi CONFIG_CHANGED=1 return 0 } append_heartbeat_interval_block() { local file="$1" { printf '\n# --- SAC agent.heartbeat (интервал в минутах; %s = 4 ч) ---\n' "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" printf 'HEARTBEAT_INTERVAL_MINUTES="%s"\n' "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" printf '# Порог watchdog: интервал + 1 ч запас (%s с)\n' "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" printf 'WATCHDOG_MAX_HEARTBEAT_AGE="%s"\n' "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" } >>"$file" } sync_heartbeat_interval_config() { local file="$CONFIG_FILE" local current_interval current_watchdog local changed=0 local backed_up=0 [ -f "$file" ] || return 1 backup_config_once() { if [ "$backed_up" -eq 0 ]; then cp "$file" "${file}.bak.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true backed_up=1 fi } if ! config_key_exists "HEARTBEAT_INTERVAL_MINUTES" "$file"; then backup_config_once if ! config_key_exists "WATCHDOG_MAX_HEARTBEAT_AGE" "$file"; then append_heartbeat_interval_block "$file" log_message "INFO" "Config $file: добавлены HEARTBEAT_INTERVAL_MINUTES=$EXPECTED_HEARTBEAT_INTERVAL_MINUTES и WATCHDOG_MAX_HEARTBEAT_AGE=$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE (отсутствовали в конфиге)." else set_config_kv "HEARTBEAT_INTERVAL_MINUTES" "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" "$file" log_message "INFO" "Config $file: добавлен HEARTBEAT_INTERVAL_MINUTES=$EXPECTED_HEARTBEAT_INTERVAL_MINUTES (отсутствовал в конфиге)." fi changed=1 else current_interval=$(config_conf_value "HEARTBEAT_INTERVAL_MINUTES" "$file" 2>/dev/null || true) if [ "$current_interval" != "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" ]; then backup_config_once set_config_kv "HEARTBEAT_INTERVAL_MINUTES" "$EXPECTED_HEARTBEAT_INTERVAL_MINUTES" "$file" log_message "INFO" "Config $file: HEARTBEAT_INTERVAL_MINUTES ${current_interval:-?} → $EXPECTED_HEARTBEAT_INTERVAL_MINUTES (deploy policy)." changed=1 fi fi if ! config_key_exists "WATCHDOG_MAX_HEARTBEAT_AGE" "$file"; then backup_config_once set_config_kv "WATCHDOG_MAX_HEARTBEAT_AGE" "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" "$file" log_message "INFO" "Config $file: добавлен WATCHDOG_MAX_HEARTBEAT_AGE=$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE (отсутствовал в конфиге)." changed=1 else current_watchdog=$(config_conf_value "WATCHDOG_MAX_HEARTBEAT_AGE" "$file" 2>/dev/null || true) if [ "$current_watchdog" != "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" ]; then backup_config_once set_config_kv "WATCHDOG_MAX_HEARTBEAT_AGE" "$EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE" "$file" log_message "INFO" "Config $file: WATCHDOG_MAX_HEARTBEAT_AGE ${current_watchdog:-?} → $EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE (deploy policy)." changed=1 fi fi if [ "$changed" -eq 0 ]; then return 1 fi if [ "$DEPLOY_MODE" -eq 1 ]; then say_info "Heartbeat: ${EXPECTED_HEARTBEAT_INTERVAL_MINUTES} мин, watchdog ${EXPECTED_WATCHDOG_MAX_HEARTBEAT_AGE} с." fi CONFIG_CHANGED=1 return 0 } parse_command_line() { DEPLOY_MODE=0 while [ $# -gt 0 ]; do case "$1" in --deploy) DEPLOY_MODE=1 shift ;; --debug) DEBUG=1 shift ;; --quiet) QUIET=1 shift ;; -h | --help) show_help exit 0 ;; *) printf 'Неизвестная опция: %s\n' "$1" >&2 show_help >&2 exit 2 ;; esac done } deploy_require_root() { if [ "$DEPLOY_MODE" -ne 1 ]; then return 0 fi if [ "$(id -u)" -ne 0 ]; then printf 'ERROR: режим --deploy нужно запускать от root (sudo)\n' >&2 exit 1 fi } # Основная функция логирования log_message() { local level="$1" local message="$2" local timestamp timestamp="$(date '+%Y-%m-%d %H:%M:%S')" # Создаём лог-файл и директорию при необходимости if [ ! -f "$LOG_FILE" ]; then mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null touch "$LOG_FILE" 2>/dev/null fi if [ "$level" = "INFO" ]; then echo "$timestamp INFO: $message" >> "$LOG_FILE" elif [ "$level" = "DEBUG" ] && [ "$DEBUG" -eq 1 ]; then echo "$timestamp DEBUG: $message" >> "$LOG_FILE" elif [ "$level" = "ERROR" ]; then echo "$timestamp ERROR: $message" >> "$LOG_FILE" fi } # Всегда в лог; в stderr — если не --quiet (stdout не трогаем: его используют $(find_*)) log_action() { log_message "INFO" "$1" if [ "$QUIET" -eq 0 ]; then printf '[update_ssh_monitor] %s\n' "$1" >&2 fi } get_git_short_rev() { local repo_dir="$1" git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || printf 'unknown' } # Remote для fetch/pull: всегда kalinamall с URL из REPO_URL (SAC передаёт REPO_URL при SSH-обновлении). resolve_git_remote_name() { local repo="$1" local name=kalinamall if git -C "$repo" remote get-url "$name" &>/dev/null; then git -C "$repo" remote set-url "$name" "$REPO_URL" >> "$LOG_FILE" 2>&1 else git -C "$repo" remote add "$name" "$REPO_URL" >> "$LOG_FILE" 2>&1 \ || git -C "$repo" remote set-url "$name" "$REPO_URL" >> "$LOG_FILE" 2>&1 fi printf '%s' "$name" } load_git_verify_settings() { local v if [ -f "$CONFIG_FILE" ]; then if [ -z "${GIT_REF:-}" ]; then v=$(config_conf_value GIT_REF "$CONFIG_FILE" 2>/dev/null || true) [ -n "$v" ] && GIT_REF="$v" fi if [ -z "${GIT_VERIFY_MODE:-}" ] || [ "$GIT_VERIFY_MODE" = "off" ]; then v=$(config_conf_value GIT_VERIFY_MODE "$CONFIG_FILE" 2>/dev/null || true) [ -n "$v" ] && GIT_VERIFY_MODE="$v" fi if [ "${GIT_ALLOW_RESET:-0}" = "0" ]; then v=$(config_conf_value GIT_ALLOW_RESET "$CONFIG_FILE" 2>/dev/null || true) [ -n "$v" ] && GIT_ALLOW_RESET="$v" fi fi GIT_VERIFY_MODE="$(printf '%s' "${GIT_VERIFY_MODE:-off}" | tr '[:upper:]' '[:lower:]')" case "$GIT_VERIFY_MODE" in off | tag | commit) ;; *) log_action "WARN: GIT_VERIFY_MODE=$GIT_VERIFY_MODE неизвестен — off" GIT_VERIFY_MODE=off ;; esac case "${GIT_ALLOW_RESET:-0}" in 1 | yes | true | on) GIT_ALLOW_RESET=1 ;; *) GIT_ALLOW_RESET=0 ;; esac case "${RELEASE_MANIFEST_STRICT:-0}" in 1 | yes | true | on) MANIFEST_STRICT=1 ;; *) MANIFEST_STRICT=0 ;; esac } verify_git_ref() { local repo="$1" ref="$2" mode="$3" local obj_type full [ "$mode" = "off" ] && return 0 [ -n "$ref" ] || { log_action "ERROR: GIT_VERIFY_MODE=$mode требует GIT_REF" return 1 } case "$mode" in tag) if ! git -C "$repo" rev-parse "refs/tags/$ref" >/dev/null 2>&1; then log_action "ERROR: GIT_REF=$ref не найден как тег в клоне" return 1 fi obj_type=$(git -C "$repo" cat-file -t "refs/tags/$ref" 2>/dev/null || true) if [ "$obj_type" != "tag" ] && [ "$obj_type" != "commit" ]; then log_action "ERROR: GIT_REF=$ref: неожиданный тип объекта ($obj_type)" return 1 fi log_action "git verify: тег $ref OK (type=$obj_type)" ;; commit) full=$(git -C "$repo" rev-parse "$ref^{commit}" 2>/dev/null || true) if [ -z "$full" ] || [ "${#full}" -ne 40 ]; then log_action "ERROR: GIT_REF=$ref не является полным commit SHA" return 1 fi log_action "git verify: commit ${full:0:12}… OK" ;; esac return 0 } find_release_manifest_path() { local repo="$1" version="$2" local p="$repo/release/manifest-${version}.json" if [ -f "$p" ]; then printf '%s\n' "$p" return 0 fi return 1 } read_repo_release_version() { local repo="$1" ver ver=$(read_ssh_monitor_version "$repo/ssh-monitor") if [ -z "$ver" ] && [ -f "$repo/version.txt" ]; then ver=$(tr -d '\r\n' <"$repo/version.txt") fi printf '%s' "$ver" } verify_release_manifest() { local repo="$1" version manifest_path rc MANIFEST_VERIFIED=0 version=$(read_repo_release_version "$repo") if [ -z "$version" ]; then SUMMARY_MANIFEST="версия не определена" log_action "WARN: не удалось определить версию релиза для manifest" [ "$MANIFEST_STRICT" -eq 1 ] && return 1 return 0 fi manifest_path=$(find_release_manifest_path "$repo" "$version" 2>/dev/null || true) if [ -z "$manifest_path" ]; then SUMMARY_MANIFEST="manifest-${version}.json не найден (skip)" log_action "WARN: release/manifest-${version}.json не найден — проверка SHA пропущена" if [ "$MANIFEST_STRICT" -eq 1 ]; then log_action "ERROR: RELEASE_MANIFEST_STRICT=1 — manifest обязателен" return 1 fi return 0 fi log_action "manifest: проверка $manifest_path" if ! MANIFEST_PATH="$manifest_path" REPO_DIR="$repo" python3 <<'PY' >>"$LOG_FILE" 2>&1 import hashlib import json import os import subprocess import sys manifest_path = os.environ["MANIFEST_PATH"] repo = os.environ["REPO_DIR"] with open(manifest_path, encoding="utf-8") as f: manifest = json.load(f) expected_commit = (manifest.get("git_commit") or "").strip() if expected_commit: actual = subprocess.check_output(["git", "-C", repo, "rev-parse", "HEAD"], text=True).strip() if actual != expected_commit: print(f"commit mismatch: HEAD={actual[:12]} manifest={expected_commit[:12]}", file=sys.stderr) sys.exit(2) files = manifest.get("files") or {} for rel, spec in files.items(): path = os.path.join(repo, rel) if not os.path.isfile(path): print(f"missing file: {rel}", file=sys.stderr) sys.exit(3) want = str(spec).removeprefix("sha256:") h = hashlib.sha256() with open(path, "rb") as fh: for chunk in iter(lambda: fh.read(1024 * 1024), b""): h.update(chunk) got = h.hexdigest() if got != want: print(f"sha256 mismatch: {rel}", file=sys.stderr) sys.exit(4) print("OK") PY then SUMMARY_MANIFEST="ОШИБКА verify" log_action "ERROR: manifest verify failed (см. $LOG_FILE)" return 1 fi MANIFEST_VERIFIED=1 SUMMARY_MANIFEST="OK ($version)" log_action "manifest: verify OK ($version)" return 0 } # fetch + checkout GIT_REF или fast-forward по GIT_BRANCH; без reset --hard (кроме GIT_ALLOW_RESET=1). sync_git_repository() { local repo="$1" branch="${2:-main}" local remote upstream git_before git_after ref="${GIT_REF:-}" remote="$(resolve_git_remote_name "$repo")" upstream="${remote}/${branch}" if [ -f "$repo/.git/MERGE_HEAD" ]; then log_action "git: незавершённый merge — abort" git -C "$repo" merge --abort >>"$LOG_FILE" 2>&1 || true fi git_before="$(get_git_short_rev "$repo")" if [ -n "$ref" ]; then log_action "git: fetch $remote (ref=$ref, verify=$GIT_VERIFY_MODE) → $REPO_URL" if ! git -C "$repo" fetch --prune "$remote" \ "+refs/heads/*:refs/remotes/${remote}/*" \ "+refs/tags/*:refs/tags/*" >>"$LOG_FILE" 2>&1; then SUMMARY_GIT="fetch FAILED ($git_before)" log_action "ERROR: git fetch failed (см. $LOG_FILE)" return 1 fi if ! verify_git_ref "$repo" "$ref" "$GIT_VERIFY_MODE"; then SUMMARY_GIT="verify FAILED ($ref)" return 1 fi if ! git -C "$repo" checkout -f "$ref" >>"$LOG_FILE" 2>&1; then SUMMARY_GIT="checkout FAILED ($ref)" log_action "ERROR: git checkout $ref failed (см. $LOG_FILE)" return 1 fi git_after="$(get_git_short_rev "$repo")" SUMMARY_GIT="checkout $ref ok, $git_before → $git_after" log_action "git: checkout $ref OK ($git_before → $git_after)" return 0 fi log_action "git: fetch --prune $remote $branch (было $git_before) → $REPO_URL" if ! git -C "$repo" fetch --prune "$remote" "$branch" >>"$LOG_FILE" 2>&1; then SUMMARY_GIT="fetch FAILED ($git_before)" log_action "ERROR: git fetch failed (см. $LOG_FILE)" return 1 fi if git -C "$repo" pull --ff-only "$remote" "$branch" >>"$LOG_FILE" 2>&1; then git_after="$(get_git_short_rev "$repo")" SUMMARY_GIT="pull ok, $git_before → $git_after" log_action "git: fast-forward OK ($git_before → $git_after)" return 0 fi if [ "$GIT_ALLOW_RESET" -eq 1 ]; then log_action "WARN: fast-forward невозможен — GIT_ALLOW_RESET=1, reset --hard $upstream" if ! git -C "$repo" reset --hard "$upstream" >>"$LOG_FILE" 2>&1; then SUMMARY_GIT="reset FAILED ($git_before)" log_action "ERROR: git reset --hard $upstream failed (см. $LOG_FILE)" return 1 fi git_after="$(get_git_short_rev "$repo")" SUMMARY_GIT="reset ok, $git_before → $git_after" log_action "git reset --hard: $git_before → $git_after" return 0 fi SUMMARY_GIT="diverged ($git_before)" log_action "ERROR: git: история разошлась (ff-only невозможен). Ручной clone или GIT_ALLOW_RESET=1" echo "ERROR: git pull --ff-only failed; история разошлась. Исправьте клон в $repo или задайте GIT_ALLOW_RESET=1 (риск)." >&2 return 1 } read_ssh_monitor_version() { local f="$1" local line [ -f "$f" ] || return 0 line=$(grep -E '^[[:space:]]*SSH_MONITOR_VERSION=' "$f" 2>/dev/null | head -1) || return 0 line=${line#SSH_MONITOR_VERSION=} line=${line#\"} line=${line%\"} line=${line#\'} line=${line%\'} printf '%s' "$line" } # Версия SAC-модуля совпадает с SSH_MONITOR_VERSION основного скрипта в том же каталоге. read_sac_client_version() { local sac_file="$1" local dir="${sac_file%/*}" read_ssh_monitor_version "$dir/ssh-monitor" } warn_version_mismatch() { local repo_ssh="$1" local repo_ver vt repo_ver="$(read_ssh_monitor_version "$repo_ssh")" [ -f "$repo_ssh" ] || return 0 if [ -f "${repo_ssh%/*}/version.txt" ]; then vt="$(tr -d '\r\n' < "${repo_ssh%/*}/version.txt")" if [ -n "$repo_ver" ] && [ -n "$vt" ] && [ "$repo_ver" != "$vt" ]; then log_action "WARN: SSH_MONITOR_VERSION=$repo_ver != version.txt=$vt" fi fi } file_sha256() { sha256sum "$1" 2>/dev/null | awk '{print $1}' || true } log_update_summary() { log_action "===== Итог обновления =====" log_action "ssh-monitor: $SUMMARY_SCRIPT ($SUMMARY_SCRIPT_REASON)" log_action "sac-client.sh: $SUMMARY_SAC_CLIENT ($SUMMARY_SAC_REASON)" log_action "конфиг: $SUMMARY_CONFIG" log_action "git: $SUMMARY_GIT" log_action "updater ($DEPLOY_INSTALL_PATH): $SUMMARY_UPDATER" log_action "watchdog: $SUMMARY_WATCHDOG" log_action "security hardening: $SUMMARY_HARDENING" log_action "manifest: $SUMMARY_MANIFEST" log_action "перезапуск ssh-monitor.service: $SUMMARY_SERVICE_RESTART" log_action "полный лог: $LOG_FILE" log_action "===========================" } # Функция для логирования шагов с комментариями (работает только при DEBUG=1) debug_step() { if [ "$DEBUG" -eq 1 ]; then log_message "DEBUG" "# $1" fi } # Функция для логирования только объявленных в скрипте переменных log_script_vars() { if [ "$DEBUG" -eq 1 ]; then log_message "DEBUG" "=== Значения переменных скрипта ===" for var_name in "${SCRIPT_VARS[@]}"; do local var_value="${!var_name}" log_message "DEBUG" "$var_name='$var_value'" done log_message "DEBUG" "=== Конец списка переменных ===" fi } # === КРИТИЧЕСКАЯ ФУНКЦИЯ: Проверка и установка ipset === ensure_ipset_installed() { debug_step "Проверяем наличие ipset в системе" # Проверяем, установлен ли ipset if ! command -v ipset &>/dev/null; then log_message "INFO" "ipset not found. Attempting silent installation..." debug_step "ipset не обнаружен, начинаем установку в silent режиме" # Определяем пакетный менеджер и устанавливаем ipset if command -v apt-get &>/dev/null || command -v apt &>/dev/null; then debug_step "Обнаружен Debian/Ubuntu, используем apt" log_message "INFO" "Detected apt package manager, installing ipset..." # Тихая установка через apt if apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq ipset >/dev/null 2>&1; then log_message "INFO" "ipset installed successfully via apt" debug_step "ipset успешно установлен через apt" else log_message "ERROR" "Failed to install ipset via apt" log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset" debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через apt" return 1 fi elif command -v yum &>/dev/null; then debug_step "Обнаружен RHEL/CentOS, используем yum" log_message "INFO" "Detected yum package manager, installing ipset..." if yum install -y -q ipset >/dev/null 2>&1; then log_message "INFO" "ipset installed successfully via yum" debug_step "ipset успешно установлен через yum" else log_message "ERROR" "Failed to install ipset via yum" log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset" debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через yum" return 1 fi elif command -v dnf &>/dev/null; then debug_step "Обнаружен Fedora/RHEL 8+, используем dnf" log_message "INFO" "Detected dnf package manager, installing ipset..." if dnf install -y -q ipset >/dev/null 2>&1; then log_message "INFO" "ipset installed successfully via dnf" debug_step "ipset успешно установлен через dnf" else log_message "ERROR" "Failed to install ipset via dnf" log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset" debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через dnf" return 1 fi elif command -v pacman &>/dev/null; then debug_step "Обнаружен Arch Linux, используем pacman" log_message "INFO" "Detected pacman package manager, installing ipset..." if pacman -S --noconfirm --quiet ipset >/dev/null 2>&1; then log_message "INFO" "ipset installed successfully via pacman" debug_step "ipset успешно установлен через pacman" else log_message "ERROR" "Failed to install ipset via pacman" log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset" debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через pacman" return 1 fi elif command -v zypper &>/dev/null; then debug_step "Обнаружен openSUSE, используем zypper" log_message "INFO" "Detected zypper package manager, installing ipset..." if zypper --quiet --non-interactive install ipset >/dev/null 2>&1; then log_message "INFO" "ipset installed successfully via zypper" debug_step "ipset успешно установлен через zypper" else log_message "ERROR" "Failed to install ipset via zypper" log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset" debug_step "КРИТИЧЕСКАЯ ОШИБКА: Не удалось установить ipset через zypper" return 1 fi else log_message "ERROR" "Unsupported package manager. Cannot install ipset" log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset" debug_step "КРИТИЧЕСКАЯ ОШИБКА: Пакетный менеджер не распознан" return 1 fi # Проверяем, что ipset действительно установился if ! command -v ipset &>/dev/null; then log_message "ERROR" "ipset installation verification failed" log_message "ERROR" "Работа обновленного скрипта невозможна без установленного ipset" debug_step "КРИТИЧЕСКАЯ ОШИБКА: ipset не обнаружен после установки" return 1 fi else debug_step "ipset уже установлен в системе" log_message "INFO" "ipset is already installed" # Проверяем версию ipset (информационно) if [ "$DEBUG" -eq 1 ]; then local ipset_version=$(ipset --version 2>&1) log_message "DEBUG" "ipset version: $ipset_version" fi fi log_message "INFO" "ipset check/installation completed successfully" return 0 } # === Гарантированное создание директорий === ensure_directories() { debug_step "Проверяем и создаём необходимые директории" # Создаём UPDATE_DIR если не существует if [ ! -d "$UPDATE_DIR" ]; then log_message "INFO" "Creating directory: $UPDATE_DIR" debug_step "Директория $UPDATE_DIR не существует, создаём" if mkdir -p "$UPDATE_DIR" 2>/dev/null; then log_message "INFO" "Directory created successfully" else log_message "INFO" "ERROR: Cannot create directory $UPDATE_DIR" return 1 fi else debug_step "Директория $UPDATE_DIR уже существует" fi # Проверяем права на запись if [ ! -w "$UPDATE_DIR" ]; then log_message "INFO" "WARNING: Directory $UPDATE_DIR is not writable" debug_step "ПРЕДУПРЕЖДЕНИЕ: Нет прав на запись в $UPDATE_DIR" return 1 fi return 0 } # Путь к sac-client.sh в клоне репозитория (корень git) find_sac_client_remote() { local candidates=( "$UPDATE_DIR/$SCRIPT_NAME/sac-client.sh" ) local p for p in "${candidates[@]}"; do if [ -f "$p" ]; then printf '%s\n' "$p" return 0 fi done return 1 } # Копирует sac-client.sh рядом с LOCAL_SCRIPT_PATH (всегда после git pull, по checksum). # Устанавливает SAC_CLIENT_CHANGED=1 при фактическом обновлении файла. sync_sac_client() { SAC_CLIENT_CHANGED=0 local remote local_dst local_sum remote_sum remote=$(find_sac_client_remote) || { SUMMARY_SAC_CLIENT="не найден в клоне" SUMMARY_SAC_REASON="путь $UPDATE_DIR/$SCRIPT_NAME/sac-client.sh" log_action "WARNING: sac-client.sh не найден в клоне git" debug_step "sac-client.sh не найден в клоне репозитория" return 0 } local_dst="${LOCAL_SCRIPT_PATH%/*}/sac-client.sh" local remote_ver local_ver remote_ver="$(read_sac_client_version "$remote")" local_ver="$(read_sac_client_version "$local_dst")" log_action "sac-client: remote=$remote ver=${remote_ver:-?} local=$local_dst ver=${local_ver:-?}" remote_sum=$(file_sha256 "$remote") if [ -z "$remote_sum" ]; then SUMMARY_SAC_CLIENT="ошибка" SUMMARY_SAC_REASON="не удалось sha256 remote" log_action "ERROR: не удалось посчитать sha256 для $remote" return 1 fi if [ "$DEPLOY_MODE" -ne 1 ] && [ -f "$local_dst" ]; then local_sum=$(file_sha256 "$local_dst") if [ "$local_sum" = "$remote_sum" ]; then SUMMARY_SAC_CLIENT="без изменений" SUMMARY_SAC_REASON="sha256 совпадает; ver=${remote_ver:-?} (${local_sum:0:16}…)" log_action "sac-client.sh: checksum совпадает, копирование не требуется" debug_step "sac-client.sh без изменений" return 0 fi log_action "sac-client.sh: checksum отличается (local ${local_sum:0:16}… != remote ${remote_sum:0:16}…)" elif [ ! -f "$local_dst" ]; then log_action "sac-client.sh: локальный файл отсутствует, будет установлен" debug_step "Локальный sac-client.sh отсутствует, будет установлен" fi debug_step "Копируем sac-client.sh в $local_dst" if ! cp "$remote" "$local_dst"; then SUMMARY_SAC_CLIENT="ошибка копирования" SUMMARY_SAC_REASON="cp $remote -> $local_dst" log_action "ERROR: не удалось скопировать sac-client.sh" return 1 fi chmod 644 "$local_dst" SAC_CLIENT_CHANGED=1 SUMMARY_SAC_CLIENT="обновлён" SUMMARY_SAC_REASON="ver ${local_ver:-?} → ${remote_ver:-?}, sha256 ${remote_sum:0:16}…" log_action "sac-client.sh обновлён → $local_dst" if [ "$DEPLOY_MODE" -eq 1 ]; then say_ok "sac-client.sh → $local_dst" fi return 0 } find_ssh_monitor_perms_remote() { local p="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-perms.sh" if [ -f "$p" ]; then printf '%s\n' "$p" return 0 fi return 1 } sync_ssh_monitor_perms_lib() { local remote local_dst remote_sum local_sum="" remote=$(find_ssh_monitor_perms_remote) || { log_action "WARNING: ssh-monitor-perms.sh не найден в клоне git" return 0 } local_dst="${LOCAL_SCRIPT_PATH%/*}/ssh-monitor-perms.sh" remote_sum=$(file_sha256 "$remote") if [ -f "$local_dst" ]; then local_sum=$(file_sha256 "$local_dst") if [ "$local_sum" = "$remote_sum" ]; then return 0 fi fi if ! cp "$remote" "$local_dst"; then log_action "ERROR: не удалось скопировать ssh-monitor-perms.sh" return 1 fi chmod 644 "$local_dst" log_action "ssh-monitor-perms.sh обновлён → $local_dst" return 0 } find_remote_ssh_monitor_path() { local repo="$UPDATE_DIR/$SCRIPT_NAME" local candidates=() git_path="" found="" p REMOTE_SCRIPT_PATH="$repo/ssh-monitor" ALT_REMOTE_PATH="$repo/src/branch/main/ssh-monitor" candidates=( "$REMOTE_SCRIPT_PATH" "$ALT_REMOTE_PATH" ) for p in "${candidates[@]}"; do if [ -f "$p" ]; then log_action "ssh-monitor в клоне: $p" printf '%s\n' "$p" return 0 fi done if [ -d "$repo/.git" ]; then if git -C "$repo" cat-file -e "HEAD:ssh-monitor" 2>/dev/null; then if [ ! -f "$repo/ssh-monitor" ]; then log_action "ssh-monitor есть в git HEAD, но отсутствует на диске — git checkout HEAD -- ssh-monitor" git -C "$repo" checkout HEAD -- ssh-monitor >> "$LOG_FILE" 2>&1 || true fi if [ -f "$repo/ssh-monitor" ]; then log_action "ssh-monitor восстановлен: $repo/ssh-monitor" printf '%s\n' "$repo/ssh-monitor" return 0 fi fi git_path=$(git -C "$repo" ls-files 'ssh-monitor' 2>/dev/null | grep -v '^contrib/' | head -1) if [ -z "$git_path" ]; then git_path=$(git -C "$repo" ls-files 2>/dev/null | grep -E '/ssh-monitor$' | grep -v contrib | grep -v watchdog | head -1) fi if [ -n "$git_path" ] && [ -f "$repo/$git_path" ]; then log_action "ssh-monitor в клоне (git ls-files): $repo/$git_path" printf '%s\n' "$repo/$git_path" return 0 fi fi found=$(find "$repo" -maxdepth 4 -type f -name 'ssh-monitor' ! -path '*/contrib/*' 2>/dev/null | head -1) if [ -n "$found" ] && [ -f "$found" ]; then log_action "ssh-monitor найден поиском: $found" printf '%s\n' "$found" return 0 fi log_action "WARNING: ssh-monitor не найден в $repo" if [ -d "$repo" ]; then log_action "корень клона: $(ls -1 "$repo" 2>/dev/null | tr '\n' ' ')" fi return 1 } install_ssh_monitor_from() { local actual_script_path="$1" local backup_path="" if [ -f "$LOCAL_SCRIPT_PATH" ]; then backup_path="${LOCAL_SCRIPT_PATH}.backup.$(date +%Y%m%d_%H%M%S)" cp "$LOCAL_SCRIPT_PATH" "$backup_path" 2>/dev/null || true log_action "резервная копия: $backup_path" fi if ! cp "$actual_script_path" "$LOCAL_SCRIPT_PATH"; then log_action "ERROR: не удалось скопировать $actual_script_path → $LOCAL_SCRIPT_PATH" return 1 fi chmod +x "$LOCAL_SCRIPT_PATH" log_action "ssh-monitor установлен: $LOCAL_SCRIPT_PATH (версия $(read_ssh_monitor_version "$LOCAL_SCRIPT_PATH"))" if [ "$DEPLOY_MODE" -eq 1 ]; then say_ok "ssh-monitor → $LOCAL_SCRIPT_PATH" fi return 0 } deploy_prepare_dirs() { say_step "Каталоги" mkdir -p "$UPDATE_DIR" /opt/scripts /var/log /var/lib/ssh-monitor 2>/dev/null || true mkdir -p "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true chmod 700 "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true chown root:root "$SAC_SPOOL_DIR_DEFAULT" 2>/dev/null || true say_ok "UPDATE_DIR=$UPDATE_DIR, spool=$SAC_SPOOL_DIR_DEFAULT" } deploy_install_config() { say_step "Конфигурация" ensure_ssh_monitor_config_file || return 1 sync_sac_config_keys || true sync_required_config_keys || true sync_heartbeat_interval_config || true sync_server_ipv4_hint_config || true sync_daily_report_enabled_config || true apply_runtime_security_hardening } deploy_install_systemd() { local example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor.service.example" say_step "systemd" if [ -f "$SYSTEMD_UNIT_PATH" ]; then say_info "Уже существует: $SYSTEMD_UNIT_PATH" elif [ ! -f "$example" ]; then say_warn "Unit example не найден: $example" else cp "$example" "$SYSTEMD_UNIT_PATH" if command -v systemctl &>/dev/null; then systemctl daemon-reload 2>/dev/null || true systemctl enable ssh-monitor.service 2>/dev/null || true say_ok "Установлен $SYSTEMD_UNIT_PATH (enable ssh-monitor.service)" else say_ok "Скопирован $SYSTEMD_UNIT_PATH (systemctl не найден)" fi fi ensure_watchdog_installed || true } find_watchdog_remote() { local p="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-watchdog" if [ -f "$p" ]; then printf '%s\n' "$p" return 0 fi return 1 } sync_watchdog_script() { WATCHDOG_CHANGED=0 local remote local_sum remote_sum remote=$(find_watchdog_remote) || { SUMMARY_WATCHDOG="скрипт не найден в клоне" log_action "WARNING: ssh-monitor-watchdog не найден в клоне git" return 0 } remote_sum=$(file_sha256 "$remote") if [ -z "$remote_sum" ]; then SUMMARY_WATCHDOG="ошибка sha256 remote" return 1 fi if [ -f "$LOCAL_WATCHDOG_PATH" ]; then local_sum=$(file_sha256 "$LOCAL_WATCHDOG_PATH") if [ "$local_sum" = "$remote_sum" ]; then log_action "ssh-monitor-watchdog: checksum совпадает, копирование не требуется" return 0 fi log_action "ssh-monitor-watchdog: обновление (sha ${local_sum:0:16}… → ${remote_sum:0:16}…)" else log_action "ssh-monitor-watchdog: локальный файл отсутствует, будет установлен" fi if ! cp "$remote" "$LOCAL_WATCHDOG_PATH"; then SUMMARY_WATCHDOG="ошибка копирования" log_action "ERROR: не удалось скопировать ssh-monitor-watchdog → $LOCAL_WATCHDOG_PATH" return 1 fi chmod 750 "$LOCAL_WATCHDOG_PATH" WATCHDOG_CHANGED=1 SUMMARY_WATCHDOG="скрипт обновлён" if [ "$DEPLOY_MODE" -eq 1 ]; then say_ok "ssh-monitor-watchdog → $LOCAL_WATCHDOG_PATH" fi return 0 } install_watchdog_systemd_units() { local svc_example timer_example units_changed=0 svc_example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-watchdog.service.example" timer_example="$UPDATE_DIR/$SCRIPT_NAME/ssh-monitor-watchdog.timer.example" if [ ! -f "$svc_example" ] || [ ! -f "$timer_example" ]; then log_action "WARNING: watchdog unit examples не найдены в клоне" return 0 fi if [ ! -f "$WATCHDOG_SERVICE_PATH" ] \ || ! cmp -s "$svc_example" "$WATCHDOG_SERVICE_PATH" 2>/dev/null; then cp "$svc_example" "$WATCHDOG_SERVICE_PATH" units_changed=1 log_action "watchdog: установлен/обновлён $WATCHDOG_SERVICE_PATH" fi if [ ! -f "$WATCHDOG_TIMER_PATH" ] \ || ! cmp -s "$timer_example" "$WATCHDOG_TIMER_PATH" 2>/dev/null; then cp "$timer_example" "$WATCHDOG_TIMER_PATH" units_changed=1 log_action "watchdog: установлен/обновлён $WATCHDOG_TIMER_PATH" fi if [ "$units_changed" -eq 1 ]; then WATCHDOG_CHANGED=1 fi if ! command -v systemctl &>/dev/null; then log_action "watchdog: systemctl не найден, units только скопированы" return 0 fi systemctl daemon-reload 2>/dev/null || true if systemctl enable ssh-monitor-watchdog.timer 2>/dev/null; then log_action "watchdog: enable ssh-monitor-watchdog.timer" fi if systemctl start ssh-monitor-watchdog.timer 2>/dev/null; then log_action "watchdog: start ssh-monitor-watchdog.timer" if [ -z "$SUMMARY_WATCHDOG" ] || [ "$SUMMARY_WATCHDOG" = "—" ]; then SUMMARY_WATCHDOG="timer активен" elif [ "${WATCHDOG_CHANGED:-0}" -eq 1 ]; then SUMMARY_WATCHDOG="${SUMMARY_WATCHDOG}; timer активен" fi else log_action "WARNING: systemctl start ssh-monitor-watchdog.timer не выполнен" fi if [ "$DEPLOY_MODE" -eq 1 ] && [ "${WATCHDOG_CHANGED:-0}" -eq 1 ]; then say_ok "watchdog: units + timer (ssh-monitor-watchdog.timer)" fi return 0 } ensure_watchdog_installed() { sync_watchdog_script || return 1 install_watchdog_systemd_units || return 1 if [ -z "$SUMMARY_WATCHDOG" ] || [ "$SUMMARY_WATCHDOG" = "—" ]; then if [ -f "$LOCAL_WATCHDOG_PATH" ] && systemctl is-enabled ssh-monitor-watchdog.timer &>/dev/null; then SUMMARY_WATCHDOG="без изменений (timer enabled)" elif [ -f "$LOCAL_WATCHDOG_PATH" ]; then SUMMARY_WATCHDOG="скрипт на месте, timer настроен" else SUMMARY_WATCHDOG="не установлен" fi fi return 0 } deploy_install_self() { local self_path self_path="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")" say_step "Скрипт автообновления" if [ "$self_path" = "$DEPLOY_INSTALL_PATH" ]; then say_info "Уже запущен из $DEPLOY_INSTALL_PATH" return 0 fi mkdir -p "$(dirname "$DEPLOY_INSTALL_PATH")" cp "$self_path" "$DEPLOY_INSTALL_PATH" chmod 750 "$DEPLOY_INSTALL_PATH" say_ok "update_ssh_monitor.sh → $DEPLOY_INSTALL_PATH" } print_config_variables_help() { say_step "Заполните в $CONFIG_FILE" cat <<'EOF' Обязательно хотя бы один канал уведомлений ИЛИ SAC: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID — или MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO Security Alert Center: UseSAC=off | dual | exclusive | fallback Пилот 2.1 (только SAC): UseSAC=exclusive SAC_URL=https://sac.kalinamall.ru SAC_API_KEY=<ключ из SAC sac-api.env / bootstrap> Проверка после правок: /usr/local/bin/ssh-monitor --check-config /usr/local/bin/ssh-monitor --check-sac systemctl restart ssh-monitor.service systemctl status ssh-monitor.service systemctl status ssh-monitor-watchdog.timer systemctl list-timers | grep ssh-monitor-watchdog EOF } print_deploy_summary() { say_step "Итог установки" say_info "Монитор: $LOCAL_SCRIPT_PATH" say_info "SAC client: ${LOCAL_SCRIPT_PATH%/*}/sac-client.sh" say_info "Конфиг: $CONFIG_FILE" say_info "Git-клон: $UPDATE_DIR/$SCRIPT_NAME" say_info "Updater: $DEPLOY_INSTALL_PATH" say_info "Лог updater: $LOG_FILE" } deploy_post_install() { deploy_prepare_dirs deploy_install_config deploy_install_systemd deploy_install_self print_deploy_summary print_config_variables_help } # Копирует update_ssh_monitor.sh из git-клона в /opt/scripts/ и при необходимости re-exec. sync_installed_updater_from_clone() { local remote="$UPDATE_DIR/$SCRIPT_NAME/update_ssh_monitor.sh" local self installed_path remote_sum installed_sum="" local repo="$UPDATE_DIR/$SCRIPT_NAME" ver manifest_path="" if [ ! -f "$remote" ]; then SUMMARY_UPDATER="не найден в клоне" log_action "updater: в клоне нет $remote" return 0 fi ver=$(read_repo_release_version "$repo") if [ -n "$ver" ]; then manifest_path=$(find_release_manifest_path "$repo" "$ver" 2>/dev/null || true) if [ -n "$manifest_path" ] && [ "${MANIFEST_VERIFIED:-0}" != "1" ]; then SUMMARY_UPDATER="manifest не verified" log_action "ERROR: updater: $manifest_path не verified — копирование пропущено" return 1 fi fi self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")" installed_path="$(readlink -f "$DEPLOY_INSTALL_PATH" 2>/dev/null || realpath "$DEPLOY_INSTALL_PATH" 2>/dev/null || printf '%s' "$DEPLOY_INSTALL_PATH")" remote_sum=$(file_sha256 "$remote") if [ -f "$DEPLOY_INSTALL_PATH" ]; then installed_sum=$(file_sha256 "$DEPLOY_INSTALL_PATH") fi if [ "$remote_sum" = "$installed_sum" ]; then SUMMARY_UPDATER="актуален (sha ${remote_sum:0:16}…)" log_action "updater: $DEPLOY_INSTALL_PATH совпадает с git (commit $(get_git_short_rev "$UPDATE_DIR/$SCRIPT_NAME"))" return 0 fi mkdir -p "$(dirname "$DEPLOY_INSTALL_PATH")" if ! cp "$remote" "$DEPLOY_INSTALL_PATH"; then SUMMARY_UPDATER="ошибка копирования" log_action "ERROR: не удалось обновить $DEPLOY_INSTALL_PATH из git" return 1 fi chmod 750 "$DEPLOY_INSTALL_PATH" SUMMARY_UPDATER="обновлён из git (${installed_sum:0:8}…→${remote_sum:0:8}…)" log_action "updater: записан $DEPLOY_INSTALL_PATH из клона (новый sha ${remote_sum:0:16}…)" if [ "$self" = "$installed_path" ] && [ "${UPDATER_REEXEC_GUARD:-0}" != "1" ]; then log_action "updater: перезапуск с новой версией (расширенный лог)…" UPDATER_REEXEC_GUARD=1 exec /bin/bash "$DEPLOY_INSTALL_PATH" "$@" fi if [ "$self" != "$installed_path" ]; then log_action "updater: сейчас запущен $self — в cron используйте $DEPLOY_INSTALL_PATH" fi return 0 } restart_ssh_monitor_service() { debug_step "Перезапускаем сервис ssh-monitor.service" if command -v systemctl &>/dev/null; then if systemctl restart ssh-monitor.service 2>/dev/null; then SUMMARY_SERVICE_RESTART="выполнен" log_action "ssh-monitor.service перезапущен" debug_step "Сервис успешно перезапущен" local i for i in $(seq 1 30); do if systemctl is-active --quiet ssh-monitor.service 2>/dev/null; then sleep 2 break fi sleep 1 done else SUMMARY_SERVICE_RESTART="не удался" log_action "WARNING: systemctl restart ssh-monitor.service не выполнен" debug_step "ПРЕДУПРЕЖДЕНИЕ: Не удалось перезапустить сервис" fi else SUMMARY_SERVICE_RESTART="пропущен (нет systemctl)" log_action "systemctl не найден, перезапуск сервиса пропущен" debug_step "systemctl не найден, сервис не перезапущен" fi } # Функция для обновления скрипта (принимает "$@" для re-exec updater) update_script() { local git_repo="$UPDATE_DIR/$SCRIPT_NAME" local git_before="" git_after="" pull_ok=1 local actual_script_path="" script_changed=0 local local_ver="" remote_ver="" local_sum="" remote_sum="" SUMMARY_SCRIPT="—" SUMMARY_SCRIPT_REASON="—" SUMMARY_SAC_CLIENT="—" SUMMARY_SAC_REASON="—" SUMMARY_CONFIG="—" SUMMARY_GIT="—" SUMMARY_HARDENING="—" SUMMARY_MANIFEST="—" SUMMARY_SERVICE_RESTART="не требовался" load_git_verify_settings load_ssh_monitor_perms_lib || true log_action "Старт обновления (deploy=$DEPLOY_MODE, debug=$DEBUG, git_ref=${GIT_REF:-$GIT_BRANCH})" debug_step "Запуск функции обновления скрипта" debug_step "Гарантируем наличие директории для работы" if ! ensure_directories; then SUMMARY_SCRIPT="ошибка" SUMMARY_SCRIPT_REASON="нет каталога $UPDATE_DIR или нет прав на запись" log_update_summary return 1 fi debug_step "Переходим в директорию для клона репозитория: $UPDATE_DIR" if ! cd "$UPDATE_DIR" 2>/dev/null; then SUMMARY_SCRIPT="ошибка" SUMMARY_SCRIPT_REASON="cannot cd $UPDATE_DIR" log_action "ERROR: cannot cd $UPDATE_DIR" log_update_summary return 1 fi debug_step "Проверяем наличие репозитория и обновляем/клонируем" if [ -d "$SCRIPT_NAME" ]; then if sync_git_repository "$git_repo" "$GIT_BRANCH"; then pull_ok=1 else pull_ok=0 log_action "WARNING: git sync не удался — remote в клоне может быть устаревшим" fi else local clone_ref="${GIT_REF:-$GIT_BRANCH}" log_action "git: clone -b $clone_ref $REPO_URL → $UPDATE_DIR/$SCRIPT_NAME" debug_step "Выполняем git clone -b $clone_ref $REPO_URL $SCRIPT_NAME" if ! git clone -b "$clone_ref" "$REPO_URL" "$SCRIPT_NAME" >>"$LOG_FILE" 2>&1; then SUMMARY_GIT="clone FAILED" SUMMARY_SCRIPT="ошибка" SUMMARY_SCRIPT_REASON="git clone failed" log_action "ERROR: git clone failed (см. $LOG_FILE)" log_update_summary return 1 fi if [ -n "${GIT_REF:-}" ] && ! verify_git_ref "$git_repo" "$GIT_REF" "$GIT_VERIFY_MODE"; then SUMMARY_GIT="verify FAILED ($GIT_REF)" SUMMARY_SCRIPT="ошибка" SUMMARY_SCRIPT_REASON="GIT_REF verify failed" log_update_summary return 1 fi git_after="$(get_git_short_rev "$git_repo")" SUMMARY_GIT="clone ok, commit $git_after" log_action "git clone: успех (commit $git_after)" fi if ! verify_release_manifest "$git_repo"; then SUMMARY_SCRIPT="ошибка" SUMMARY_SCRIPT_REASON="manifest verify failed" log_update_summary return 1 fi sync_installed_updater_from_clone "$@" actual_script_path="$(find_remote_ssh_monitor_path 2>/dev/null || true)" actual_script_path="${actual_script_path//$'\n'/}" actual_script_path="${actual_script_path//$'\r'/}" debug_step "Путь к ssh-monitor в клоне: ${actual_script_path:-не найден}" if [ -n "$actual_script_path" ] && [ -f "$actual_script_path" ]; then warn_version_mismatch "$actual_script_path" fi SAC_CLIENT_CHANGED=0 if ! sync_sac_client; then log_update_summary return 1 fi sync_ssh_monitor_perms_lib || true WATCHDOG_CHANGED=0 if ! ensure_watchdog_installed; then log_action "WARNING: установка watchdog завершилась с ошибкой" fi CONFIG_CHANGED=0 if ! ensure_ssh_monitor_config_file; then log_update_summary return 1 fi if sync_sac_config_keys; then SUMMARY_CONFIG="SAC-ключи дополнены, UseSAC=fallback" else SUMMARY_CONFIG="SAC без изменений" fi sync_required_config_keys && SUMMARY_CONFIG="${SUMMARY_CONFIG}; security keys" sync_heartbeat_interval_config || true sync_server_ipv4_hint_config || true sync_daily_report_enabled_config || true harden_ssh_monitor_config_perms if [ "${CONFIG_CHANGED:-0}" -eq 1 ] && [ "$SUMMARY_CONFIG" = "SAC без изменений" ]; then SUMMARY_CONFIG="heartbeat/watchdog выровнены" elif [ "${CONFIG_CHANGED:-0}" -eq 1 ] && [ "$SUMMARY_CONFIG" != "SAC без изменений" ]; then SUMMARY_CONFIG="${SUMMARY_CONFIG}; heartbeat при необходимости" fi if [ -n "$actual_script_path" ] && [ -f "$actual_script_path" ]; then if [ "$DEPLOY_MODE" -eq 1 ]; then log_action "режим --deploy: установка ssh-monitor → $LOCAL_SCRIPT_PATH" if install_ssh_monitor_from "$actual_script_path"; then script_changed=1 SUMMARY_SCRIPT="установлен (--deploy)" SUMMARY_SCRIPT_REASON="$(read_ssh_monitor_version "$LOCAL_SCRIPT_PATH")" else SUMMARY_SCRIPT="ошибка установки" log_update_summary return 1 fi else if [ ! -f "$LOCAL_SCRIPT_PATH" ]; then LOCAL_CHECKSUM="" local_ver="(нет файла)" else LOCAL_CHECKSUM=$(file_sha256 "$LOCAL_SCRIPT_PATH") local_ver="$(read_ssh_monitor_version "$LOCAL_SCRIPT_PATH")" fi REMOTE_CHECKSUM=$(file_sha256 "$actual_script_path") remote_ver="$(read_ssh_monitor_version "$actual_script_path")" log_action "сравнение: local=$LOCAL_SCRIPT_PATH ver=$local_ver sha=${LOCAL_CHECKSUM:0:16}…" log_action "сравнение: remote=$actual_script_path ver=$remote_ver sha=${REMOTE_CHECKSUM:0:16}…" if [ "$pull_ok" -eq 0 ]; then log_action "примечание: git pull не удался — remote может быть устаревшим" fi if [ "$LOCAL_CHECKSUM" != "$REMOTE_CHECKSUM" ]; then log_action "checksum различается — обновляем $LOCAL_SCRIPT_PATH" if install_ssh_monitor_from "$actual_script_path"; then script_changed=1 SUMMARY_SCRIPT="обновлён" SUMMARY_SCRIPT_REASON="${local_ver} → ${remote_ver}, sha изменился" else SUMMARY_SCRIPT="ошибка копирования" log_update_summary return 1 fi else SUMMARY_SCRIPT="без изменений" SUMMARY_SCRIPT_REASON="sha256 совпадает; версия local=$local_ver remote=$remote_ver" log_action "ssh-monitor: файлы идентичны, копирование не требуется" fi fi else SUMMARY_SCRIPT="не найден в клоне" SUMMARY_SCRIPT_REASON="проверьте структуру репозитория после git pull" log_action "ERROR: ssh-monitor не найден в клоне $UPDATE_DIR/$SCRIPT_NAME" if [ "$DEPLOY_MODE" -eq 1 ]; then say_warn "ssh-monitor не найден в клоне $UPDATE_DIR/$SCRIPT_NAME" fi if [ "$DEBUG" -eq 1 ] && [ -d "$UPDATE_DIR/$SCRIPT_NAME" ]; then find "$UPDATE_DIR/$SCRIPT_NAME" -type f -name "$SCRIPT_NAME" 2>/dev/null | while read -r found_file; do log_message "DEBUG" "Найден файл: $found_file" done fi fi if [ "$DEPLOY_MODE" -eq 1 ]; then script_changed=1 fi apply_runtime_security_hardening if [ "$script_changed" -eq 1 ] || [ "${SAC_CLIENT_CHANGED:-0}" -eq 1 ] || [ "${CONFIG_CHANGED:-0}" -eq 1 ]; then restart_ssh_monitor_service else log_action "перезапуск сервиса не нужен (ни один файл не изменился)" fi log_script_vars log_update_summary return 0 } # === Основной блок выполнения === parse_command_line "$@" deploy_require_root require_repo_url load_ssh_monitor_perms_lib || true if declare -F ssh_monitor_update_mark_begin >/dev/null 2>&1; then ssh_monitor_update_mark_begin fi if [ "$DEPLOY_MODE" -eq 1 ]; then printf '\n=== ssh-monitor: первичная установка (--deploy) ===\n' say_step "Репозиторий и файлы" fi _updater_self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || printf '%s' "$0")" _updater_sha="$(file_sha256 "$_updater_self")" log_message "INFO" "=== Script update started (deploy=$DEPLOY_MODE) ===" log_action "запуск updater: $_updater_self (sha ${_updater_sha:0:16}…)" log_action "хост=$(hostname 2>/dev/null || echo '?') user=$(id -un 2>/dev/null) uid=$(id -u 2>/dev/null)" log_action "пути: LOCAL=$LOCAL_SCRIPT_PATH UPDATE_DIR=$UPDATE_DIR REPO=$REPO_URL" debug_step "Инициализация скрипта обновления" if ! ensure_directories; then log_message "INFO" "ERROR: Failed to ensure directories. Exiting." exit 1 fi maybe_enable_deploy_for_missing_install cleanup_update_state() { if declare -F ssh_monitor_update_mark_end >/dev/null 2>&1; then ssh_monitor_update_mark_end fi } trap cleanup_update_state EXIT debug_step "Начинаем проверку и установку ipset (только --deploy / first_deploy)" if [ "$DEPLOY_MODE" -eq 1 ]; then if ! ensure_ipset_installed; then log_message "ERROR" "CRITICAL: ipset installation or verification failed" echo "ERROR: Работа deploy невозможна без установленного ipset" >&2 exit 1 fi fi update_script "$@" exit_code=$? if [ "$exit_code" -eq 0 ] && [ "$DEPLOY_MODE" -eq 1 ]; then deploy_post_install restart_ssh_monitor_service if command -v systemctl &>/dev/null; then if systemctl start ssh-monitor.service 2>/dev/null; then say_ok "systemctl start ssh-monitor.service" else say_warn "systemctl start не выполнен (проверьте --check-config и конфиг)" fi fi fi if [ $exit_code -eq 0 ]; then log_message "INFO" "=== Script update completed successfully ===" log_action "завершено успешно (код 0). Итог см. выше или: tail -30 $LOG_FILE" if [ "$DEPLOY_MODE" -eq 1 ]; then printf '\n=== Установка завершена. Отредактируйте %s и перезапустите сервис. ===\n' "$CONFIG_FILE" fi else log_message "INFO" "=== Script update completed with errors (exit code: $exit_code) ===" log_action "завершено с ошибкой (код $exit_code). См. $LOG_FILE" fi exit $exit_code