feat: release 2.2.1-SAC — phase 2 manifest verify and quiet SAC updates
Pinned GIT_REF/verify, release manifest, no blind reset --hard; suppress lifecycle and watchdog Telegram during updater; healthcheck via json.dumps.
This commit is contained in:
+271
-21
@@ -5,6 +5,11 @@ 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"
|
||||
|
||||
@@ -24,6 +29,7 @@ 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"
|
||||
@@ -401,11 +407,12 @@ maybe_enable_deploy_for_missing_install() {
|
||||
}
|
||||
|
||||
set_config_kv() {
|
||||
local key="$1" val="$2" file="$3"
|
||||
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}=\"${val}\"/" "$file"
|
||||
sed -i -E "s/^[[:space:]]*${key}=.*/${key}=\"${esc}\"/" "$file"
|
||||
else
|
||||
printf '%s="%s"\n' "$key" "$val" >>"$file"
|
||||
printf '%s="%s"\n' "$key" "$esc" >>"$file"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -619,44 +626,238 @@ resolve_git_remote_name() {
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
# fetch + ff-only pull; при расхождении истории (force-push) — reset --hard.
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
log_action "WARN: fast-forward невозможен (часто после force-push) — 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
|
||||
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
|
||||
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
|
||||
|
||||
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() {
|
||||
@@ -705,6 +906,7 @@ log_update_summary() {
|
||||
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 "==========================="
|
||||
@@ -1270,6 +1472,7 @@ deploy_post_install() {
|
||||
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="не найден в клоне"
|
||||
@@ -1277,6 +1480,16 @@ sync_installed_updater_from_clone() {
|
||||
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")
|
||||
@@ -1318,6 +1531,14 @@ restart_ssh_monitor_service() {
|
||||
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 не выполнен"
|
||||
@@ -1344,9 +1565,16 @@ update_script() {
|
||||
SUMMARY_CONFIG="—"
|
||||
SUMMARY_GIT="—"
|
||||
SUMMARY_HARDENING="—"
|
||||
SUMMARY_MANIFEST="—"
|
||||
SUMMARY_SERVICE_RESTART="не требовался"
|
||||
|
||||
log_action "Старт обновления (deploy=$DEPLOY_MODE, debug=$DEBUG)"
|
||||
load_git_verify_settings
|
||||
load_ssh_monitor_perms_lib || true
|
||||
if declare -F ssh_monitor_update_mark_begin >/dev/null 2>&1; then
|
||||
ssh_monitor_update_mark_begin
|
||||
log_action "agent-update: state file установлен (подавление lifecycle/watchdog)"
|
||||
fi
|
||||
log_action "Старт обновления (deploy=$DEPLOY_MODE, debug=$DEBUG, git_ref=${GIT_REF:-$GIT_BRANCH})"
|
||||
debug_step "Запуск функции обновления скрипта"
|
||||
|
||||
debug_step "Гарантируем наличие директории для работы"
|
||||
@@ -1375,9 +1603,10 @@ update_script() {
|
||||
log_action "WARNING: git sync не удался — remote в клоне может быть устаревшим"
|
||||
fi
|
||||
else
|
||||
log_action "git: clone $REPO_URL → $UPDATE_DIR/$SCRIPT_NAME"
|
||||
debug_step "Выполняем git clone $REPO_URL $SCRIPT_NAME"
|
||||
if ! git clone -b "$GIT_BRANCH" "$REPO_URL" "$SCRIPT_NAME" >> "$LOG_FILE" 2>&1; then
|
||||
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"
|
||||
@@ -1385,11 +1614,25 @@ update_script() {
|
||||
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)"
|
||||
@@ -1534,6 +1777,13 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user