feat: release 2.1.8-SAC — security phase 1 and SAC-first update hardening
Updater applies chmod/chown on config, spool and state paths on every run. SAC is the primary update path; optional cron uses the same behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+70
-11
@@ -7,9 +7,13 @@ IFS=$'\n\t'
|
||||
# ============================================
|
||||
|
||||
CONFIG_FILE="/etc/ssh-monitor.conf"
|
||||
SSH_MONITOR_VERSION="2.1.6-SAC"
|
||||
SSH_MONITOR_VERSION="2.1.8-SAC"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "$SCRIPT_DIR/ssh-monitor-perms.sh" ]; then
|
||||
# shellcheck source=ssh-monitor-perms.sh
|
||||
source "$SCRIPT_DIR/ssh-monitor-perms.sh"
|
||||
fi
|
||||
if [ -f "$SCRIPT_DIR/sac-client.sh" ]; then
|
||||
# shellcheck source=sac-client.sh
|
||||
source "$SCRIPT_DIR/sac-client.sh"
|
||||
@@ -39,6 +43,8 @@ SAC_SPOOL_FLUSH_MAX_FILES="50"
|
||||
SAC_SPOOL_MAX_AGE_HOURS="72"
|
||||
SAC_SEND_HEARTBEAT="1"
|
||||
SAC_FALLBACK_FAILURES="5"
|
||||
SAC_TLS_INSECURE="0"
|
||||
CONFIG_STRICT_PERMS="0"
|
||||
|
||||
# Почта (SMTP через python3). Нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO.
|
||||
MAIL_SMTP_HOST=""
|
||||
@@ -229,6 +235,8 @@ load_config() {
|
||||
local cfg_sac_timeout_sec="${SAC_TIMEOUT_SEC:-45}"
|
||||
local cfg_sac_spool_flush_max="${SAC_SPOOL_FLUSH_MAX_FILES:-50}"
|
||||
local cfg_sac_spool_max_age_h="${SAC_SPOOL_MAX_AGE_HOURS:-72}"
|
||||
local cfg_sac_tls_insecure="${SAC_TLS_INSECURE:-0}"
|
||||
local cfg_config_strict_perms="${CONFIG_STRICT_PERMS:-0}"
|
||||
local cfg_mail_smtp_host="${MAIL_SMTP_HOST:-}"
|
||||
local cfg_mail_smtp_port="${MAIL_SMTP_PORT:-587}"
|
||||
local cfg_mail_smtp_user="${MAIL_SMTP_USER:-}"
|
||||
@@ -239,8 +247,13 @@ load_config() {
|
||||
local cfg_mail_smtp_ssl="${MAIL_SMTP_SSL:-0}"
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONFIG_FILE"
|
||||
local _strict_peek
|
||||
_strict_peek="$(ssh_monitor_config_peek CONFIG_STRICT_PERMS "$CONFIG_FILE" "0")"
|
||||
if ! ssh_monitor_check_config_file_perms "$CONFIG_FILE" "$_strict_peek"; then
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONFIG_FILE"
|
||||
else
|
||||
printf 'WARN: Конфиг %s не найден, использую значения по умолчанию\n' "$CONFIG_FILE" >&2
|
||||
fi
|
||||
@@ -303,6 +316,8 @@ load_config() {
|
||||
SAC_TIMEOUT_SEC="$(trim_value "${SAC_TIMEOUT_SEC:-$cfg_sac_timeout_sec}")"
|
||||
SAC_SPOOL_FLUSH_MAX_FILES="$(trim_value "${SAC_SPOOL_FLUSH_MAX_FILES:-$cfg_sac_spool_flush_max}")"
|
||||
SAC_SPOOL_MAX_AGE_HOURS="$(trim_value "${SAC_SPOOL_MAX_AGE_HOURS:-$cfg_sac_spool_max_age_h}")"
|
||||
SAC_TLS_INSECURE="$(trim_value "${SAC_TLS_INSECURE:-$cfg_sac_tls_insecure}")"
|
||||
CONFIG_STRICT_PERMS="$(trim_value "${CONFIG_STRICT_PERMS:-$cfg_config_strict_perms}")"
|
||||
MAIL_SMTP_HOST="$(trim_value "${MAIL_SMTP_HOST:-$cfg_mail_smtp_host}")"
|
||||
MAIL_SMTP_PORT="${MAIL_SMTP_PORT:-$cfg_mail_smtp_port}"
|
||||
MAIL_SMTP_USER="$(trim_value "${MAIL_SMTP_USER:-$cfg_mail_smtp_user}")"
|
||||
@@ -388,6 +403,35 @@ notify_chain_human() {
|
||||
(IFS=','; printf '%s' "${NOTIFY_CHAIN[*]}")
|
||||
}
|
||||
|
||||
write_state_file() {
|
||||
local path="$1" content="$2"
|
||||
ssh_monitor_secure_file "$path"
|
||||
printf '%s' "$content" >"$path"
|
||||
}
|
||||
|
||||
ssh_monitor_audit_state_paths() {
|
||||
local path
|
||||
for path in \
|
||||
"${SAC_SPOOL_DIR:-}" \
|
||||
"${SAC_FAIL_COUNT_FILE:-}" \
|
||||
"${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:-}" \
|
||||
"/etc/ssh_monitor_whitelist.txt"; do
|
||||
[ -n "$path" ] || continue
|
||||
if [ -d "$path" ]; then
|
||||
ssh_monitor_warn_path_perms "$path" "каталог state" 700
|
||||
elif [ -e "$path" ]; then
|
||||
ssh_monitor_warn_path_perms "$path" "файл state" 600
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
validate_config() {
|
||||
validate_report_hour
|
||||
validate_numeric_or_default "BAN_TIME" "3600"
|
||||
@@ -434,6 +478,12 @@ validate_config() {
|
||||
printf '%s\n' 'Не настроен ни один канал отправки оповещений' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "${BACKUP_WEBHOOK_URL:-}" ]; then
|
||||
printf 'WARN: BACKUP_WEBHOOK_URL deprecated (legacy fallback); будет удалён в 2.3.x\n' >&2
|
||||
fi
|
||||
if command -v sac_warn_tls_insecure >/dev/null 2>&1; then
|
||||
sac_warn_tls_insecure
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================
|
||||
@@ -1045,6 +1095,7 @@ block_ip() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
ssh_monitor_secure_file "$BAN_LIST_FILE" 2>/dev/null || true
|
||||
echo "$ip|$ban_until|$attempts|$(date '+%Y-%m-%d %H:%M:%S')" >>"$BAN_LIST_FILE"
|
||||
|
||||
save_firewall_state
|
||||
@@ -1214,6 +1265,7 @@ load_whitelist_from_file() {
|
||||
local whitelist_file="/etc/ssh_monitor_whitelist.txt"
|
||||
|
||||
if [ -f "$whitelist_file" ]; then
|
||||
ssh_monitor_warn_path_perms "$whitelist_file" "whitelist" 600
|
||||
write_log "Загрузка белого списка из $whitelist_file"
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
@@ -1369,7 +1421,7 @@ monitor_ssh() {
|
||||
done <<<"$ssh_logs"
|
||||
fi
|
||||
|
||||
echo "$current_time" >"$LAST_SSH_CHECK_FILE"
|
||||
write_state_file "$LAST_SSH_CHECK_FILE" "$current_time"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
@@ -1486,7 +1538,7 @@ monitor_sudo() {
|
||||
done <<<"$sudo_logs"
|
||||
fi
|
||||
|
||||
echo "$current_time" >"$LAST_SUDO_CHECK_FILE"
|
||||
write_state_file "$LAST_SUDO_CHECK_FILE" "$current_time"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
@@ -1551,7 +1603,7 @@ monitor_security_events() {
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$now" >"$LAST_SECURITY_EVENTS_FILE"
|
||||
write_state_file "$LAST_SECURITY_EVENTS_FILE" "$now"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
@@ -1709,7 +1761,7 @@ monitor_logind() {
|
||||
done <<<"$logind_lines"
|
||||
fi
|
||||
|
||||
echo "$current_time" >"$LAST_LOGIND_CHECK_FILE"
|
||||
write_state_file "$LAST_LOGIND_CHECK_FILE" "$current_time"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
@@ -1877,7 +1929,7 @@ PY
|
||||
notify_or_sac "report.daily.ssh" "info" "Ежедневный отчёт SSH" "$sac_summary" "$message" "$sac_details_json"
|
||||
write_log "Ежедневный отчет отправлен"
|
||||
|
||||
echo "$(report_date '+%F')" >"$LAST_REPORT_FILE"
|
||||
write_state_file "$LAST_REPORT_FILE" "$(report_date '+%F')"
|
||||
}
|
||||
|
||||
read_last_report_date() {
|
||||
@@ -1936,7 +1988,7 @@ send_heartbeat() {
|
||||
message="${message}🕐 Время: ${timestamp_human}"
|
||||
|
||||
notify_or_sac "agent.heartbeat" "info" "Heartbeat" "ssh-monitor alive" "$message"
|
||||
echo "$timestamp" >"$LAST_HEARTBEAT_FILE"
|
||||
write_state_file "$LAST_HEARTBEAT_FILE" "$timestamp"
|
||||
write_log "Heartbeat отправлен"
|
||||
}
|
||||
|
||||
@@ -1965,9 +2017,11 @@ write_health_and_metrics() {
|
||||
local ts
|
||||
ts=$(date '+%s')
|
||||
if [ -n "${HEALTHCHECK_STATUS_FILE:-}" ]; then
|
||||
mkdir -p "$(dirname "$HEALTHCHECK_STATUS_FILE")" 2>/dev/null || true
|
||||
ssh_monitor_secure_dir "$(dirname "$HEALTHCHECK_STATUS_FILE")" 2>/dev/null || mkdir -p "$(dirname "$HEALTHCHECK_STATUS_FILE")" 2>/dev/null || true
|
||||
printf '{"ts":%s,"hostname":"%s","dry_run":%s}\n' "$ts" "$(hostname | sed 's/"/\\"/g')" "$([[ "${DRY_RUN:-0}" = "1" ]] && echo true || echo false)" >"${HEALTHCHECK_STATUS_FILE}.tmp"
|
||||
ssh_monitor_secure_file "${HEALTHCHECK_STATUS_FILE}.tmp" 2>/dev/null || true
|
||||
mv "${HEALTHCHECK_STATUS_FILE}.tmp" "$HEALTHCHECK_STATUS_FILE" 2>/dev/null || true
|
||||
ssh_monitor_secure_file "$HEALTHCHECK_STATUS_FILE" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${PROMETHEUS_TEXTFILE_DIR:-}" ]; then
|
||||
mkdir -p "$PROMETHEUS_TEXTFILE_DIR" 2>/dev/null || true
|
||||
@@ -2097,6 +2151,11 @@ run_check_config() {
|
||||
printf 'MAIL_SMTP_PASSWORD: %s\n' "$([ -n "${MAIL_SMTP_PASSWORD:-}" ] && echo задан || echo не задан)"
|
||||
printf 'UseSAC=%s SAC_URL=%s SAC_API_KEY=%s\n' "${UseSAC:-off}" "${SAC_URL:-}" \
|
||||
"$([ -n "${SAC_API_KEY:-}" ] && echo задан || echo не задан)"
|
||||
printf 'CONFIG_STRICT_PERMS=%s SAC_TLS_INSECURE=%s\n' "${CONFIG_STRICT_PERMS:-0}" "${SAC_TLS_INSECURE:-0}"
|
||||
if [ -n "${BACKUP_WEBHOOK_URL:-}" ]; then
|
||||
printf 'WARN: BACKUP_WEBHOOK_URL deprecated (legacy fallback); будет удалён в 2.3.x\n'
|
||||
fi
|
||||
ssh_monitor_audit_state_paths
|
||||
if bash -n "$0" 2>/dev/null; then
|
||||
printf 'bash -n: OK\n'
|
||||
else
|
||||
@@ -2138,7 +2197,7 @@ fi
|
||||
trap send_shutdown_notification EXIT INT TERM
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
touch "$LOG_FILE"
|
||||
ssh_monitor_secure_file "$LOG_FILE" 2>/dev/null || touch "$LOG_FILE"
|
||||
|
||||
write_log "Конфигурация загружена из $CONFIG_FILE (или дефолты), версия ${SSH_MONITOR_VERSION:-?}, очередь оповещений: $(notify_chain_human)"
|
||||
if [ "${ENABLE_IP_BAN:-1}" = "1" ]; then
|
||||
|
||||
Reference in New Issue
Block a user