diff --git a/README.md b/README.md index 84ea0fa..706a8a1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # ssh-monitor -**Версия:** `2.1.6-SAC` +**Версия:** `2.1.8-SAC` Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email, бан IP (ipset), ежедневный отчёт, heartbeat. -**Security Alert Center:** модуль **`sac-client.sh`**, режим `UseSAC` (`off` | `exclusive` | `dual` | `fallback`) — [контракт SAC](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-integration.md). Обновление с хоста: `update_ssh_monitor.sh` или кнопка **«Обновить ssh-monitor (SSH)»** в SAC — сервер передаёт `REPO_URL` / `GIT_BRANCH` из **Настройки → Обновления агентов** (по умолчанию `git.kalinamall.ru`, не GitHub). +**Security Alert Center:** модуль **`sac-client.sh`**, режим `UseSAC` (`off` | `exclusive` | `dual` | `fallback`) — [контракт SAC](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-integration.md). **Основной способ обновления на Linux** — кнопка **«Обновить ssh-monitor (SSH)»** в SAC: сервер передаёт `REPO_URL` / `GIT_BRANCH`, updater сам подтягивает git, выставляет права на конфиг/spool/state и перезапускает сервис. Cron/systemd timer — опционально. + +**Безопасность (2.1.7+):** агент работает от root; доверяйте только своему git-зеркалу (`REPO_URL`) и `/etc/ssh-monitor.conf` (`chmod 600`). Updater дописывает обязательные ключи (`CONFIG_STRICT_PERMS`, `SAC_TLS_INSECURE`, SAC-*) и **на каждом прогоне** применяет `chmod`/`chown` к конфигу, SAC-spool и state-файлам. Подробнее: [docs/security-roadmap.ru.md](docs/security-roadmap.ru.md). ## Состав репозитория @@ -37,7 +39,7 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email, - `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` - `NOTIFY_ORDER` — список каналов (`telegram`, `email` и сокращения **tg**, **mail**); при каждом оповещении скрипт **пытается отправить во все перечисленные** каналы по порядку (не «первый успешный — и стоп»). Пусто = в цепочку попадают только настроенные каналы (порядок по умолчанию см. в `ssh-monitor.conf.example`). Если итоговая цепочка пуста — скрипт не стартует (см. раздел **«Каналы оповещений и пустой NOTIFY_CHAIN»**). - `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — отправка почты через **python3** (см. таблицу каналов ниже) -- `BACKUP_WEBHOOK_URL` — резервная доставка JSON `{"text":"..."}` (например Slack Incoming Webhook), если **все** каналы из **`NOTIFY_CHAIN`** не смогли доставить сообщение; **не заменяет** обязательность хотя бы одного основного канала (см. шаг 3) +- `BACKUP_WEBHOOK_URL` — *(deprecated)* резервная доставка JSON, если **все** каналы из **`NOTIFY_CHAIN`** не смогли доставить; удаление в 2.3.x - `LOG_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` - `ENABLE_LOGIND_MONITOR`, `LOGIND_NOTIFY_NEW`, `LOGIND_NOTIFY_REMOVED`, `LOGIND_NOTIFY_FAILED`, `LOGIND_SKIP_REMOTE` — см. раздел «Мониторинг systemd-logind» ниже - `DAILY_REPORT_HOUR` (0..23), `DAILY_REPORT_ENABLED` (1/0 — отчёт с агента; 0 = только SAC), `DAILY_REPORT_TZ` (опционально), `NOTIFY_TZ` (опционально), `SERVER_DISPLAY_NAME` (опционально), `SSH_ACCEPT_NOTIFY_DEDUP_SEC`, `DAILY_REPORT_TOP_IPS` @@ -57,7 +59,7 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email, - `TELEGRAM_CHAT_ID` — ID чата/пользователя, куда отправляются уведомления. - `NOTIFY_ORDER` — CSV имён каналов (`telegram`, `email` или `tg`, `mail`). Для **каждого** оповещения выполняется попытка доставки **во все** перечисленные каналы (при ошибке одного остальные всё равно пробуются; неудачи пишутся в **`LOG_FILE`**). Пустая строка: автоматически собирается цепочка только из реально настроенных каналов (по умолчанию порядок telegram → email; критерии «настроен» — **таблица** в разделе **«Каналы оповещений и пустой NOTIFY_CHAIN»**). Если после сборки цепочки **нет ни одного** канала, скрипт **сразу завершается** с ошибкой: **«Не настроен ни один канал отправки оповещений»** (в том числе режим `--check-config` и `--dry-run`). - `MAIL_SMTP_HOST`, `MAIL_SMTP_PORT`, `MAIL_SMTP_USER`, `MAIL_SMTP_PASSWORD`, `MAIL_FROM`, `MAIL_TO`, `MAIL_SMTP_STARTTLS`, `MAIL_SMTP_SSL` — параметры SMTP для канала `email` (отправка через **python3**; см. таблицу «настроен»). -- `BACKUP_WEBHOOK_URL` — URL для резервной отправки (тело JSON `{"text":"..."}`); только fallback при сбое основных каналов, **не засчитывается** как «настроенный канал» (см. шаг 3). +- `BACKUP_WEBHOOK_URL` — *(deprecated)* URL резервной отправки; только fallback; **не засчитывается** как основной канал - `LOG_FILE` — путь к основному лог-файлу скрипта. - `LAST_HEARTBEAT_FILE` — файл с timestamp последнего heartbeat-сообщения. - `LAST_REPORT_FILE` — дата последнего ежедневного отчёта в формате `YYYY-MM-DD` (старый формат Unix-времени при первом запуске будет автоматически интерпретирован). @@ -127,9 +129,11 @@ Bash-мониторинг **SSH**, **sudo**, **systemd-logind**: Telegram/email, - Опционально **`SERVER_DISPLAY_NAME`** в конфиге — фиксированная подпись вместо hostname (удобно при нескольких серверах в одном Telegram-чате). - Типы сообщений и примеры: [docs/notifications.ru.md](docs/notifications.ru.md). -### Автообновление скрипта +### Обновление агента -Скрипт **`update_ssh_monitor.sh`** по расписанию (cron или systemd timer) выполняет `git pull` / `clone`, сравнивает SHA256 с `/usr/local/bin/ssh-monitor`, **всегда** подтягивает **`/usr/local/bin/sac-client.sh`** и при изменении любого из файлов перезапускает **`ssh-monitor.service`**. Конфиг **`/etc/ssh-monitor.conf`** не трогается. +**Рекомендуемый путь — SAC:** в карточке Linux-хоста кнопка **«Обновить ssh-monitor (SSH)»**. Участие админа на сервере не требуется: SAC передаёт `REPO_URL`/`GIT_BRANCH`, updater выполняет `git pull`, синхронизирует `sac-client.sh`, дописывает ключи конфига, **выставляет права** на конфиг/spool/state и перезапускает `ssh-monitor.service`. + +Скрипт **`update_ssh_monitor.sh`** также можно запускать вручную или по расписанию (cron/systemd timer) с тем же `REPO_URL` — поведение идентично. Пошаговая настройка: [docs/auto-update.ru.md](docs/auto-update.ru.md). diff --git a/docs/auto-update.ru.md b/docs/auto-update.ru.md index f7a1b9d..4821f51 100644 --- a/docs/auto-update.ru.md +++ b/docs/auto-update.ru.md @@ -1,174 +1,113 @@ -# Автообновление ssh-monitor - -Скрипт **`update_ssh_monitor.sh`** в корне репозитория обновляет установленный **`/usr/local/bin/ssh-monitor`** из git-репозитория и перезапускает systemd-сервис. Watchdog и таймеры **не изменяются**. - -**Конфиг `/etc/ssh-monitor.conf`:** при первой установке копируется из `ssh-monitor.conf.example`. При каждом deploy/обновлении в **существующий** конфиг **дописываются** отсутствующие SAC-переменные (`UseSAC`, `SAC_URL`, `SAC_API_KEY`, spool, timeout и т.д.) и выставляется **`UseSAC=fallback`**. Telegram/SMTP и прочие ключи **не перезаписываются**. - -Установка самого монитора и unit-файлов: [README.md](../README.md), зависимости: [install-prerequisites.ru.md](install-prerequisites.ru.md). - -## Что делает скрипт - -1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`). -2. Проверяет наличие **`ipset`**; при отсутствии пытается установить через пакетный менеджер (apt, yum, dnf, pacman, zypper). Без ipset скрипт завершается с ошибкой — новая версия монитора с **`ENABLE_IP_BAN=1`** требует ipset. -3. В **`UPDATE_DIR`** выполняет **`git clone`** или **`git pull origin main`** репозитория. -4. Ищет файл **`ssh-monitor`** в клоне: - - `$UPDATE_DIR/ssh-monitor/src/branch/main/ssh-monitor` (структура GitLab); - - `$UPDATE_DIR/ssh-monitor/ssh-monitor` (плоский репозиторий, как в этом проекте). -5. Сравнивает **SHA256** удалённого файла с **`LOCAL_SCRIPT_PATH`** (по умолчанию `/usr/local/bin/ssh-monitor`). -6. **Всегда** (независимо от шага 5) синхронизирует **`sac-client.sh`** в каталог рядом с монитором (по умолчанию **`/usr/local/bin/sac-client.sh`**) — нужен для **`UseSAC`** и **`--check-sac`**. -7. При отличии checksum **`ssh-monitor`** и/или **`sac-client.sh`**: - - резервная копия `ssh-monitor.backup.YYYYMMDD_HHMMSS` (только для основного скрипта); - - `cp` + `chmod +x` для `ssh-monitor`, `chmod 644` для `sac-client.sh`; - - **`systemctl restart ssh-monitor.service`** (если есть systemctl). -8. Пишет лог в **`LOG_FILE`** (по умолчанию `/var/log/update_script.log`). - -```mermaid -flowchart LR - A[cron/timer] --> B[update_ssh_monitor.sh] - B --> C[git pull/clone] - C --> D[sync sac-client.sh] - C --> E{SHA256 ssh-monitor?} - D --> F{что-то обновилось?} - E -->|да| G[backup + cp ssh-monitor] - E -->|нет| F - G --> F - F -->|да| H[restart ssh-monitor.service] - F -->|нет| I[без изменений] -``` - -## Настройка перед первым запуском - -Отредактируйте переменные в начале **`update_ssh_monitor.sh`**: - -| Переменная | По умолчанию | Назначение | -|------------|--------------|------------| -| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона | -| `REPO_URL` | `https://git.kalinamall.ru/PapaTramp/ssh-monitor.git` | URL репозитория (на закрытом зеркале — URL своего Gitea) | -| `LOCAL_SCRIPT_PATH` | `/usr/local/bin/ssh-monitor` | Куда копировать бинарник | -| `LOG_FILE` | `/var/log/update_script.log` | Лог обновлений | -| `DEBUG` | `0` | `1` — подробный DEBUG в лог | - -Требования на сервере: - -- **root** (или sudo) для `cp`, `systemctl`, установки ipset; -- **git** и сетевой доступ к репозиторию (SSH-ключ или HTTPS); -- ветка в скрипте жёстко **`main`** — при другой ветке измените `git pull` в файле. - -## Первичная установка (--deploy) - -Один сценарий для нового сервера (клон git, `ssh-monitor`, `sac-client.sh`, пример конфига, systemd unit): - -```bash -cd /path/to/ssh-monitor # или curl/clone репозитория -sudo ./first_deploy.sh -# то же самое: -sudo ./update_ssh_monitor.sh --deploy -``` - -Скрипт выводит в консоль, **куда что скопировано**, и список переменных для **`/etc/ssh-monitor.conf`**. Существующий конфиг **не перезаписывается**. - -После установки отредактируйте конфиг и проверьте: - -```bash -sudo nano /etc/ssh-monitor.conf -sudo /usr/local/bin/ssh-monitor --check-config -sudo /usr/local/bin/ssh-monitor --check-sac -sudo systemctl restart ssh-monitor.service -``` - -## Установка updater (для последующих обновлений) - -```bash -sudo mkdir -p /opt/scripts -sudo cp update_ssh_monitor.sh /opt/scripts/update_ssh_monitor.sh -sudo chmod 750 /opt/scripts/update_ssh_monitor.sh -``` - -Проверка вручную: - -```bash -sudo /opt/scripts/update_ssh_monitor.sh -sudo tail -20 /var/log/update_script.log -sudo systemctl status ssh-monitor -``` - -## Запуск по расписанию - -### Cron (пример: каждый день в 04:00) - -```bash -sudo crontab -e -``` - -Добавьте: - -``` -0 4 * * * /opt/scripts/update_ssh_monitor.sh -``` - -### systemd timer (альтернатива) - -Создайте `/etc/systemd/system/ssh-monitor-update.service`: - -```ini -[Unit] -Description=Update ssh-monitor from git - -[Service] -Type=oneshot -ExecStart=/opt/scripts/update_ssh_monitor.sh -``` - -И `/etc/systemd/system/ssh-monitor-update.timer`: - -```ini -[Unit] -Description=Daily ssh-monitor update - -[Timer] -OnCalendar=*-*-* 04:00:00 -Persistent=true - -[Install] -WantedBy=timers.target -``` - -```bash -sudo systemctl daemon-reload -sudo systemctl enable --now ssh-monitor-update.timer -``` - -## Откат после неудачного обновления - -Резервные копии лежат рядом с установленным скриптом: - -```bash -ls -la /usr/local/bin/ssh-monitor.backup.* -sudo cp /usr/local/bin/ssh-monitor.backup.YYYYMMDD_HHMMSS /usr/local/bin/ssh-monitor -sudo systemctl restart ssh-monitor.service -``` - -## Лог и диагностика «мгновенного» завершения - -Все шаги пишутся в **`/var/log/update_script.log`**. При обычном запуске те же строки дублируются в stdout (кроме **`--quiet`**). - -В конце каждого прогона — блок **«Итог обновления»**: статус `ssh-monitor` / `sac-client` / git / перезапуск и **причина**, если файлы не копировались (checksum совпал, git pull failed, скрипт не найден в клоне). - -```bash -sudo /opt/scripts/update_ssh_monitor.sh -sudo tail -40 /var/log/update_script.log -``` - -**Важно:** после `git pull` в клоне скрипт **сам обновляет** `/opt/scripts/update_ssh_monitor.sh` из репозитория и один раз перезапускает себя (если вызывали именно этот путь). Если в логе старые строки вроде `Checksums match` без блока «Итог обновления» — на сервере ещё старая копия updater; один раз: - -```bash -sudo cp /opt/scripts/update/ssh-monitor/update_ssh_monitor.sh /opt/scripts/update_ssh_monitor.sh -sudo chmod 750 /opt/scripts/update_ssh_monitor.sh -sudo /opt/scripts/update_ssh_monitor.sh -``` - -## Ограничения - -- Обновляется **только** файл **`ssh-monitor`**, не конфиг и не watchdog. -- При недоступном git **`git pull`** пишет предупреждение и продолжает с локальной копией. -- На **закрытом зеркале** в `update_ssh_monitor.sh` задайте **`REPO_URL`** своего Gitea; в публичном GitHub по умолчанию — `git.kalinamall.ru/PapaTramp/ssh-monitor`. +# Автообновление ssh-monitor + +Скрипт **`update_ssh_monitor.sh`** обновляет **`/usr/local/bin/ssh-monitor`** из git и перезапускает systemd-сервис. + +## Основной путь: Security Alert Center + +На продакшен-хостах обновление идёт **через SAC**, без cron: + +1. Админ нажимает **«Обновить ssh-monitor (SSH)»** в карточке хоста. +2. SAC по SSH запускает `/opt/scripts/update_ssh_monitor.sh` с `REPO_URL` / `GIT_BRANCH` из **Настройки → Обновления агентов**. +3. Updater сам выполняет `git pull`, синхронизирует `sac-client.sh` и watchdog, дописывает отсутствующие ключи в **`/etc/ssh-monitor.conf`**, **выставляет `chmod`/`chown`** на конфиг, SAC-spool, счётчики и state-файлы, перезапускает **`ssh-monitor.service`**. + +Участие админа на сервере **не требуется** (кроме первичной настройки `SAC_URL` / `SAC_API_KEY` в конфиге). + +При **первой установке** (нет updater на хосте) SAC выполняет bootstrap с **`--deploy`**: clone, ipset при необходимости, systemd unit, полный hardening. + +**Cron / systemd timer** — опциональная альтернатива с тем же скриптом и обязательным `REPO_URL` в окружении. + +Watchdog и таймеры watchdog **не изменяются** при обычном update (кроме синхронизации скрипта watchdog из git). + +**Конфиг `/etc/ssh-monitor.conf`:** при первой установке копируется из `ssh-monitor.conf.example`. При каждом update в **существующий** конфиг **дописываются** отсутствующие SAC-переменные и ключи безопасности; **`UseSAC=fallback`**. Telegram/SMTP и прочие ключи **не перезаписываются**. + +Установка монитора и unit-файлов: [README.md](../README.md), зависимости: [install-prerequisites.ru.md](install-prerequisites.ru.md). + +## Что делает скрипт + +1. Создаёт рабочий каталог **`UPDATE_DIR`** (по умолчанию `/opt/scripts/update`). +2. При **`--deploy`** / bootstrap SAC проверяет **`ipset`** и при отсутствии пытается установить через пакетный менеджер. Обычное обновление **не** вызывает `apt-get`. +3. В **`UPDATE_DIR`** выполняет **`git clone`** или **`git pull`** репозитория. +4. Ищет **`ssh-monitor`** в клоне (GitLab layout или плоский репозиторий). +5. Сравнивает **SHA256** с **`LOCAL_SCRIPT_PATH`** (`/usr/local/bin/ssh-monitor`). +6. **Всегда** синхронизирует **`sac-client.sh`**, **`ssh-monitor-perms.sh`**, watchdog. +7. Дописывает ключи конфига; **`apply_runtime_security_hardening`**: `chmod 600` конфиг, `700` spool, `600` state/SAC-файлы, whitelist. +8. При изменении **`ssh-monitor`**, **`sac-client.sh`** или конфига — **`systemctl restart ssh-monitor.service`**. +9. Лог: **`/var/log/update_script.log`** (блок «Итог обновления», строка `security hardening`). + +```mermaid +flowchart LR + A[SAC или cron] --> B[update_ssh_monitor.sh] + B --> C[git pull/clone] + C --> D[sync sac-client + perms lib] + C --> E{SHA256 ssh-monitor?} + D --> F[harden config/spool/state] + E -->|да| G[backup + cp ssh-monitor] + E -->|нет| F + G --> F + F --> H{что-то изменилось?} + H -->|да| I[restart ssh-monitor.service] + H -->|нет| J[без перезапуска] +``` + +## Настройка перед первым запуском + +| Переменная | По умолчанию | Назначение | +|------------|--------------|------------| +| `UPDATE_DIR` | `/opt/scripts/update` | Каталог для git-клона | +| `REPO_URL` | *(обязательно)* | URL git — задаётся SAC, export или `Environment=` в cron | +| `GIT_BRANCH` | `main` | Ветка для clone/pull | +| `LOCAL_SCRIPT_PATH` | `/usr/local/bin/ssh-monitor` | Куда копировать монитор | +| `LOG_FILE` | `/var/log/update_script.log` | Лог обновлений | +| `UPDATE_VIA_SAC` | *(опционально)* | SAC выставляет `1`; при отсутствии бинарника включается `--deploy` | + +Требования: **root**, **git**, доступ к репозиторию. + +## Первичная установка (--deploy) + +```bash +sudo REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git ./update_ssh_monitor.sh --deploy +# или first_deploy.sh с REPO_URL в окружении +``` + +Через SAC bootstrap выполняется автоматически с **`--deploy`**. + +## Установка updater (если не через SAC bootstrap) + +```bash +sudo mkdir -p /opt/scripts +sudo cp update_ssh_monitor.sh /opt/scripts/update_ssh_monitor.sh +sudo chmod 750 /opt/scripts/update_ssh_monitor.sh +``` + +Проверка: + +```bash +sudo REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git /opt/scripts/update_ssh_monitor.sh +sudo tail -30 /var/log/update_script.log +``` + +## Запуск по расписанию (опционально) + +### Cron + +``` +0 4 * * * REPO_URL=https://git.kalinamall.ru/PapaTramp/ssh-monitor.git /opt/scripts/update_ssh_monitor.sh +``` + +### systemd timer + +См. предыдущие примеры `ssh-monitor-update.service` / `.timer` с `Environment=REPO_URL=...`. + +## Откат + +```bash +ls -la /usr/local/bin/ssh-monitor.backup.* +sudo cp /usr/local/bin/ssh-monitor.backup.YYYYMMDD_HHMMSS /usr/local/bin/ssh-monitor +sudo systemctl restart ssh-monitor.service +``` + +## Ограничения + +- Секреты (`SAC_API_KEY`, Telegram) SAC **не** прописывает — только в `/etc/ssh-monitor.conf` на хосте. +- При недоступном git **`git pull`** пишет предупреждение и продолжает с локальной копией. +- **`REPO_URL`** без дефолта в скрипте — только доверенное зеркало (kalinamall / своё Gitea). diff --git a/docs/notifications.ru.md b/docs/notifications.ru.md index 91e8990..b6a5113 100644 --- a/docs/notifications.ru.md +++ b/docs/notifications.ru.md @@ -93,6 +93,6 @@ sudo ssh-monitor --check-config ## Каналы доставки - **`NOTIFY_CHAIN`**: `telegram`, `email` — при каждом событии попытка **во все** каналы списка. -- **`BACKUP_WEBHOOK_URL`**: только если **все** каналы цепочки не доставили сообщение. +- **`BACKUP_WEBHOOK_URL`** *(deprecated, legacy)*: только если **все** каналы цепочки не доставили сообщение. В типовом деплое не используется; удаление в 2.3.x. Режим **`--dry-run`**: уведомления не отправляются; в stderr печатается текст **уже с подписью сервера**. diff --git a/docs/security-roadmap.ru.md b/docs/security-roadmap.ru.md index 50aea00..5049379 100644 --- a/docs/security-roadmap.ru.md +++ b/docs/security-roadmap.ru.md @@ -22,7 +22,8 @@ | Версия | Фазы | Суть | |--------|------|------| -| **2.1.7-SAC** | Фаза 1 + выбранное из Фазы 4 | Права, `REPO_URL` обязателен, webhook, docs | +| **2.1.7-SAC** | Фаза 1 + выбранное из Фазы 4 | Права, `REPO_URL` обязателен, webhook, docs | ✅ реализовано | +| **2.1.8-SAC** | Дополнение к 1.1 | SAC-first update: hardening прав на каждом прогоне updater | ✅ реализовано | | **2.2.0-SAC** | Фаза 2 | Manifest, pinned ref, без слепого `reset --hard` | | **2.3.0-SAC** | Фаза 3 | Парсер конфига без `source` (minor breaking) | @@ -34,9 +35,10 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре ### 1.1 Права на state / spool (M4) -- [ ] При создании каталогов и файлов состояния: `chown root:root`, каталоги `chmod 700`, файлы `chmod 600` -- [ ] Затронуть: `SAC_SPOOL_DIR`, `SAC_FAIL_COUNT_FILE`, heartbeat / last_* (где создаёт `ssh-monitor` / `sac-client.sh` / deploy) -- [ ] В `--check-config`: предупреждение, если существующие пути с ослабленными правами +- [x] При создании каталогов и файлов состояния: `chown root:root`, каталоги `chmod 700`, файлы `chmod 600` +- [x] Затронуть: `SAC_SPOOL_DIR`, `SAC_FAIL_COUNT_FILE`, heartbeat / last_* (где создаёт `ssh-monitor` / `sac-client.sh` / deploy) +- [x] **2.1.8:** `update_ssh_monitor.sh` → `apply_runtime_security_hardening` на **каждом** update (SAC, cron, вручную): retrofit `chmod`/`chown` существующих путей из конфига +- [x] В `--check-config`: предупреждение, если существующие пути с ослабленными правами **Файлы:** `ssh-monitor`, `sac-client.sh`, при необходимости `update_ssh_monitor.sh` @@ -44,42 +46,30 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре ### 1.2 Проверка прав конфига при старте (C1, частично) -- [ ] Перед загрузкой конфига: если `/etc/ssh-monitor.conf` существует — проверить `root:root`, mode `600` или `400` -- [ ] **Поведение по умолчанию:** `WARN` в лог + stderr, работа продолжается (не ломать старые установки с `644`) -- [ ] Опционально в конфиге: `CONFIG_STRICT_PERMS=1` → **exit 1** при нарушении +- [x] Перед загрузкой конфига: если `/etc/ssh-monitor.conf` существует — проверить `root:root`, mode `600` или `400` +- [x] **Поведение по умолчанию:** `WARN` в лог + stderr, работа продолжается (не ломать старые установки с `644`) +- [x] Опционально в конфиге: `CONFIG_STRICT_PERMS=1` → **exit 1** при нарушении **Решение агента:** strict выключен по умолчанию; в README рекомендовать `chmod 600` и позже `400`. --- -### 1.3 `BACKUP_WEBHOOK_URL` (H3) — **нужно утвердить завтра** +### 1.3 `BACKUP_WEBHOOK_URL` (H3) — **вариант A (soft-deprecate)** -**Что есть сейчас:** fallback POST JSON `{"text":"..."}`, если **все** каналы `NOTIFY_CHAIN` не доставили сообщение; то же в watchdog при сбое Telegram. По умолчанию пусто. В проде, судя по обсуждению, **не используется**. - -| Вариант | Плюсы | Минусы | -|---------|-------|--------| -| **A. Soft-deprecate (рекомендация)** | Не ломает тех, у кого Slack webhook | Код остаётся | -| **B. Удалить в 2.1.7** | Меньше attack surface | Breaking, если кто-то использует | -| **C. Оставить как есть** | Без изменений | Риск H3 при компрометации конфига | - -**Рекомендация для 2.1.7 (вариант A):** - -- [ ] В `ssh-monitor.conf.example`: закомментировать / убрать из «активного» блока, комментарий `DEPRECATED: не используется в типовом деплое; будет удалён в 2.3.x` -- [ ] В README / `docs/notifications.ru.md`: пометить **legacy / необязательно** -- [ ] В `--check-config`: если задан — `WARN: BACKUP_WEBHOOK_URL deprecated` +- [x] В `ssh-monitor.conf.example`: закомментировать / убрать из «активного» блока, комментарий `DEPRECATED: не используется в типовом деплое; будет удалён в 2.3.x` +- [x] В README / `docs/notifications.ru.md`: пометить **legacy / необязательно** +- [x] В `--check-config`: если задан — `WARN: BACKUP_WEBHOOK_URL deprecated` - [ ] **Удаление кода** — отложить до **2.3.0** или позже, если подтвердим, что нигде не нужен -**Завтра решить:** A / B / C. - --- ### 1.4 `REPO_URL` — только из env, без дефолта в коде (C3) — **утверждено** -- [ ] Убрать захардкоженный default `https://git.kalinamall.ru/...` из `update_ssh_monitor.sh` -- [ ] При старте updater: если `REPO_URL` пуст — **exit 1**, сообщение в **stderr** и **`$LOG_FILE`** -- [ ] Текст ошибки: что задать (`export REPO_URL=...` или в systemd unit `Environment=REPO_URL=...`) -- [ ] SAC / cron / timer: документировать обязательную передачу `REPO_URL` (SAC уже может передавать при SSH-обновлении) -- [ ] **Не** делать allowlist доменов — оператор сам выбирает зеркало +- [x] Убрать захардкоженный default `https://git.kalinamall.ru/...` из `update_ssh_monitor.sh` +- [x] При старте updater: если `REPO_URL` пуст — **exit 1**, сообщение в **stderr** и **`$LOG_FILE`** +- [x] Текст ошибки: что задать (`export REPO_URL=...` или в systemd unit `Environment=REPO_URL=...`) +- [x] SAC: основной путь обновления (кнопка в UI); cron/timer — опционально. Bootstrap SAC → `--deploy` +- [x] **Не** делать allowlist доменов — оператор сам выбирает зеркало **Файлы:** `update_ssh_monitor.sh`, `docs/auto-update.ru.md`, пример unit/timer если есть @@ -89,8 +79,8 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре ### 1.5 Документация threat model -- [ ] `README.md` + этот файл: root-агент, доверие к `REPO_URL` и конфигу, санитайз GitHub -- [ ] Рекомендация: критичные хосты — только закрытое зеркало; автообновление осознанно +- [x] `README.md` + этот файл: root-агент, доверие к `REPO_URL` и конфигу, санитайз GitHub +- [x] Рекомендация: критичные хосты — только закрытое зеркало; автообновление осознанно --- @@ -220,8 +210,8 @@ Bump: `ssh-monitor` (`SSH_MONITOR_VERSION`) + `version.txt` в каждом ре | Пункт | Версия | Решение | |-------|--------|---------| -| **4.4** `ensure_ipset_installed` не на каждый update | 2.1.7 | **Делаем:** только `--deploy` / `first_deploy.sh`; обычный update не вызывает `apt-get` | -| **4.3** Права whitelist-файла | 2.1.7 | **Делаем:** при загрузке `/etc/ssh_monitor_whitelist.txt` — WARN если не root:root | +| **4.4** `ensure_ipset_installed` не на каждый update | 2.1.7 | **Делаем:** только `--deploy` / `first_deploy.sh`; обычный update не вызывает `apt-get` | ✅ | +| **4.3** Права whitelist-файла | 2.1.7 | **Делаем:** при загрузке `/etc/ssh_monitor_whitelist.txt` — WARN если не root:root | ✅ | | **4.5** JSON healthcheck через `json.dumps` | 2.2.0 | **Делаем:** мелкий fix L3 | | **4.1** Секреты не через environ в Python | — | **Не делаем** (мало выигрыша при root) | | **4.2** Telegram token в URL | — | **Не делаем** (ограничение Bot API) | diff --git a/sac-client.sh b/sac-client.sh index 32ecc2b..96a5199 100644 --- a/sac-client.sh +++ b/sac-client.sh @@ -2,6 +2,26 @@ # Версия для SAC = SSH_MONITOR_VERSION в ssh-monitor (отдельный номер здесь не задаётся). # shellcheck shell=bash +sac_tls_insecure_enabled() { + case "${SAC_TLS_INSECURE:-0}" in + 1 | yes | true | on) return 0 ;; + esac + return 1 +} + +sac_warn_tls_insecure() { + if sac_tls_insecure_enabled; then + write_log 'CRITICAL: SAC_TLS_INSECURE=1 — проверка TLS для SAC отключена (риск MITM). Только для краткой отладки в lab.' + fi +} + +sac_curl() { + if sac_tls_insecure_enabled; then + curl -k -sS "$@" + else + curl -sS "$@" + fi +} sac_normalize_mode() { printf '%s' "${1:-off}" | tr '[:upper:]' '[:lower:]' } @@ -31,7 +51,7 @@ sac_agent_instance_id() { local dir dir="$(dirname "$id_file")" if [ ! -d "$dir" ]; then - mkdir -p "$dir" 2>/dev/null || id_file="/tmp/ssh-monitor-agent_instance_id" + ssh_monitor_secure_dir "$dir" 2>/dev/null || mkdir -p "$dir" 2>/dev/null || id_file="/tmp/ssh-monitor-agent_instance_id" fi if [ -f "$id_file" ]; then cat "$id_file" @@ -39,6 +59,7 @@ sac_agent_instance_id() { fi local new_id new_id="$(python3 -c 'import uuid; print(uuid.uuid4())')" + ssh_monitor_secure_file "$id_file" 2>/dev/null || true printf '%s\n' "$new_id" >"$id_file" 2>/dev/null || true printf '%s' "$new_id" } @@ -50,7 +71,7 @@ sac_check_health() { if [ "${DRY_RUN:-0}" = "1" ]; then return 0 fi - code="$(curl -sS -o /dev/null -w '%{http_code}' \ + code="$(sac_curl -o /dev/null -w '%{http_code}' \ --connect-timeout "${SAC_TIMEOUT_SEC:-45}" \ --max-time "${SAC_TIMEOUT_SEC:-45}" \ "${base}/health" || echo 000)" @@ -61,7 +82,8 @@ sac_spool_write() { local event_id="$1" local body="$2" local dir="${SAC_SPOOL_DIR:-/var/lib/ssh-monitor/sac-spool}" - mkdir -p "$dir" 2>/dev/null || return 1 + ssh_monitor_secure_dir "$dir" 2>/dev/null || mkdir -p "$dir" 2>/dev/null || return 1 + ssh_monitor_secure_file "${dir}/${event_id}.json" 2>/dev/null || true printf '%s' "$body" >"${dir}/${event_id}.json" } @@ -89,7 +111,7 @@ sac_write_fail_count() { local n="$1" local f f="$(sac_fail_count_file)" - mkdir -p "$(dirname "$f")" 2>/dev/null || true + ssh_monitor_secure_file "$f" 2>/dev/null || mkdir -p "$(dirname "$f")" 2>/dev/null || true printf '%s' "$n" >"$f" } @@ -147,7 +169,7 @@ sac_post_payload() { tmp="$(mktemp)" resp="$(mktemp)" printf '%s' "$payload" >"$tmp" - http_code="$(curl -sS -o "$resp" -w '%{http_code}' \ + http_code="$(sac_curl -o "$resp" -w '%{http_code}' \ -X POST "$(sac_ingest_url)" \ -H "Authorization: Bearer ${SAC_API_KEY}" \ -H "Content-Type: application/json" \ diff --git a/ssh-monitor b/ssh-monitor index 5f0bf9c..6f65f8b 100644 --- a/ssh-monitor +++ b/ssh-monitor @@ -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 diff --git a/ssh-monitor-perms.sh b/ssh-monitor-perms.sh new file mode 100644 index 0000000..69573fb --- /dev/null +++ b/ssh-monitor-perms.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Path permission helpers for ssh-monitor, sac-client, watchdog, updater. +# shellcheck shell=bash + +ssh_monitor_config_peek() { + local key="$1" file="$2" default="${3:-}" + local line val + [ -f "$file" ] || { + printf '%s\n' "$default" + return 0 + } + line="$(grep -E "^[[:space:]]*${key}=" "$file" 2>/dev/null | tail -1)" || { + printf '%s\n' "$default" + return 0 + } + val="${line#*=}" + val="${val#"${val%%[![:space:]]*}"}" + val="${val%"${val##*[![:space:]]}"}" + val="${val#\"}" + val="${val%\"}" + val="${val#\'}" + val="${val%\'}" + printf '%s\n' "$val" +} + +ssh_monitor_truthy() { + case "${1,,}" in + 1 | yes | true | on) return 0 ;; + esac + return 1 +} + +ssh_monitor_path_mode() { + local path="$1" + [ -e "$path" ] || return 1 + stat -c '%a' "$path" 2>/dev/null || stat -f '%OLp' "$path" 2>/dev/null +} + +ssh_monitor_path_owner() { + local path="$1" + [ -e "$path" ] || return 1 + stat -c '%U:%G' "$path" 2>/dev/null +} + +ssh_monitor_is_root_owned() { + local path="$1" own + [ -e "$path" ] || return 0 + own="$(ssh_monitor_path_owner "$path" 2>/dev/null || true)" + [ "$own" = "root:root" ] +} + +ssh_monitor_secure_dir() { + local dir="$1" + [ -n "$dir" ] || return 1 + mkdir -p "$dir" 2>/dev/null || return 1 + chmod 700 "$dir" 2>/dev/null || true + chown root:root "$dir" 2>/dev/null || true +} + +ssh_monitor_secure_file() { + local file="$1" + local dir + [ -n "$file" ] || return 1 + dir="$(dirname "$file")" + ssh_monitor_secure_dir "$dir" || true + if [ ! -f "$file" ]; then + : >"$file" 2>/dev/null || touch "$file" 2>/dev/null || return 1 + fi + chmod 600 "$file" 2>/dev/null || true + chown root:root "$file" 2>/dev/null || true +} + +# Существующий файл или каталог: 600/700 root:root; для отсутствующего пути — только родительский каталог. +ssh_monitor_harden_path() { + local path="$1" + [ -n "$path" ] || return 0 + if [ -d "$path" ]; then + ssh_monitor_secure_dir "$path" + elif [ -f "$path" ] || [ -L "$path" ]; then + ssh_monitor_secure_file "$path" + else + local parent + parent="$(dirname "$path")" + [ -n "$parent" ] && [ "$parent" != "." ] && ssh_monitor_secure_dir "$parent" + fi +} + +ssh_monitor_warn_path_perms() { + local path="$1" kind="$2" max_mode="${3:-600}" + local mode own msg + [ -e "$path" ] || return 0 + if ! ssh_monitor_is_root_owned "$path"; then + own="$(ssh_monitor_path_owner "$path" 2>/dev/null || echo '?')" + msg="WARN: ${kind} ${path}: владелец ${own}, ожидается root:root" + printf '%s\n' "$msg" >&2 + [ -n "${write_log:-}" ] && write_log "$msg" || true + fi + mode="$(ssh_monitor_path_mode "$path" 2>/dev/null || echo "")" + if [ -n "$mode" ] && [ "$((10#$mode))" -gt "$((10#$max_mode))" ]; then + msg="WARN: ${kind} ${path}: mode ${mode}, рекомендуется не шире ${max_mode}" + printf '%s\n' "$msg" >&2 + [ -n "${write_log:-}" ] && write_log "$msg" || true + fi +} + +ssh_monitor_check_config_file_perms() { + local file="${1:-/etc/ssh-monitor.conf}" + local strict="${2:-0}" + local mode msg + [ -f "$file" ] || return 0 + if ! ssh_monitor_is_root_owned "$file"; then + msg="WARN: конфиг ${file}: владелец $(ssh_monitor_path_owner "$file" 2>/dev/null || echo '?'), ожидается root:root" + printf '%s\n' "$msg" >&2 + [ -n "${write_log:-}" ] && write_log "$msg" || true + if ssh_monitor_truthy "$strict"; then + printf '%s\n' "CONFIG_STRICT_PERMS=1: исправьте владельца конфига (chown root:root ${file})" >&2 + return 1 + fi + fi + mode="$(ssh_monitor_path_mode "$file" 2>/dev/null || echo "")" + case "$mode" in + 600 | 400) ;; + *) + msg="WARN: конфиг ${file}: mode ${mode:-?}, рекомендуется 600 или 400 (chmod 600 ${file})" + printf '%s\n' "$msg" >&2 + [ -n "${write_log:-}" ] && write_log "$msg" || true + if ssh_monitor_truthy "$strict"; then + printf '%s\n' "CONFIG_STRICT_PERMS=1: ослабленные права конфига недопустимы" >&2 + return 1 + fi + ;; + esac + return 0 +} diff --git a/ssh-monitor-watchdog b/ssh-monitor-watchdog index d07d472..416cef2 100644 --- a/ssh-monitor-watchdog +++ b/ssh-monitor-watchdog @@ -3,6 +3,11 @@ set -Eeuo pipefail IFS=$'\n\t' CONFIG_FILE="/etc/ssh-monitor.conf" +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 WATCHDOG_LOG_FILE="/var/log/ssh_monitor_watchdog.log" LAST_HEARTBEAT_FILE="/var/log/last_heartbeat.txt" WATCHDOG_MAX_HEARTBEAT_AGE=18000 @@ -75,9 +80,15 @@ notify_send() { load_config() { if [ -f "$CONFIG_FILE" ]; then + local _strict_peek + _strict_peek="$(ssh_monitor_config_peek CONFIG_STRICT_PERMS "$CONFIG_FILE" "0")" + ssh_monitor_check_config_file_perms "$CONFIG_FILE" "$_strict_peek" || true # shellcheck disable=SC1090 source "$CONFIG_FILE" fi + if [ -n "${BACKUP_WEBHOOK_URL:-}" ]; then + write_log "WARN: BACKUP_WEBHOOK_URL deprecated (legacy fallback); будет удалён в 2.3.x" + fi } is_service_active() { diff --git a/ssh-monitor.conf.example b/ssh-monitor.conf.example index c304cc3..e668c4f 100644 --- a/ssh-monitor.conf.example +++ b/ssh-monitor.conf.example @@ -2,11 +2,8 @@ TELEGRAM_BOT_TOKEN="8239219522:AAEyOZX3cwNfgGOMDkf-mgjTIuoaOh5gF7I" TELEGRAM_CHAT_ID="2843230" -# Резервный webhook, если ВСЕ каналы из NOTIFY_CHAIN не смогли доставить сообщение (JSON {"text":"..."}). -# ВАЖНО: BACKUP_WEBHOOK_URL НЕ заменяет основной канал — без хотя бы одного из telegram/email -# скрипт сразу завершится с ошибкой в stderr: «Не настроен ни один канал отправки оповещений» (код 1), -# в том числе при --check-config и --dry-run. Сначала настройте Telegram или SMTP ниже. -BACKUP_WEBHOOK_URL="" +# DEPRECATED: legacy fallback webhook (Slack/Discord). Не используется в типовом деплое; удаление в 2.3.x. +# BACKUP_WEBHOOK_URL="" # --- Очередь оповещений (NOTIFY_ORDER) --- # CSV: telegram, email (или сокращения tg, mail). @@ -39,6 +36,11 @@ SAC_TIMEOUT_SEC="45" SAC_SPOOL_FLUSH_MAX_FILES="50" SAC_SPOOL_MAX_AGE_HOURS="72" +# 0 = проверять TLS SAC (рекомендуется). 1 = curl -k (только lab; CRITICAL в логе). +SAC_TLS_INSECURE="0" +# 0 = WARN при chmod конфига != 600. 1 = exit 1 при ослабленных правах /etc/ssh-monitor.conf. +CONFIG_STRICT_PERMS="0" + # SMTP (python3). Для авто-режима почты нужны MAIL_SMTP_HOST, MAIL_FROM, MAIL_TO. MAIL_SMTP_HOST="" MAIL_SMTP_PORT="587" diff --git a/update_ssh_monitor.sh b/update_ssh_monitor.sh index 50b38b2..ebcf356 100755 --- a/update_ssh_monitor.sh +++ b/update_ssh_monitor.sh @@ -3,7 +3,7 @@ # === Конфигурация скрипта === UPDATE_DIR="/opt/scripts/update" SCRIPT_NAME="ssh-monitor" -REPO_URL="${REPO_URL:-https://git.kalinamall.ru/PapaTramp/ssh-monitor.git}" +REPO_URL="${REPO_URL:-}" GIT_BRANCH="${GIT_BRANCH:-main}" LOCAL_SCRIPT_PATH="/usr/local/bin/ssh-monitor" LOG_FILE="/var/log/update_script.log" @@ -23,6 +23,7 @@ SUMMARY_GIT="—" SUMMARY_SERVICE_RESTART="—" SUMMARY_UPDATER="—" SUMMARY_WATCHDOG="—" +SUMMARY_HARDENING="—" CONFIG_FILE="/etc/ssh-monitor.conf" DEPLOY_INSTALL_PATH="/opt/scripts/update_ssh_monitor.sh" @@ -76,10 +77,34 @@ update_ssh_monitor.sh — обновление или первичная уст При первой установке создаётся /etc/ssh-monitor.conf из example. При обновлении в существующий конфиг дописываются отсутствующие SAC-переменные -и выставляется UseSAC=fallback (Telegram/SMTP не перезаписываются). +и обязательные ключи безопасности (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 @@ -94,6 +119,12 @@ SAC_CONFIG_KEYS=( ) 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 @@ -155,6 +186,7 @@ ensure_ssh_monitor_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 } @@ -235,6 +267,139 @@ sync_sac_config_keys() { 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" if grep -qE "^[[:space:]]*${key}=" "$file" 2>/dev/null; then @@ -539,6 +704,7 @@ log_update_summary() { 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 "перезапуск ssh-monitor.service: $SUMMARY_SERVICE_RESTART" log_action "полный лог: $LOG_FILE" log_action "===========================" @@ -776,6 +942,38 @@ sync_sac_client() { 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 @@ -854,7 +1052,10 @@ install_ssh_monitor_from() { deploy_prepare_dirs() { say_step "Каталоги" - mkdir -p "$UPDATE_DIR" /opt/scripts /var/log "$SAC_SPOOL_DIR_DEFAULT" /var/lib/ssh-monitor 2>/dev/null || true + 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" } @@ -862,9 +1063,11 @@ 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() { @@ -1140,6 +1343,7 @@ update_script() { SUMMARY_SAC_REASON="—" SUMMARY_CONFIG="—" SUMMARY_GIT="—" + SUMMARY_HARDENING="—" SUMMARY_SERVICE_RESTART="не требовался" log_action "Старт обновления (deploy=$DEPLOY_MODE, debug=$DEBUG)" @@ -1201,6 +1405,7 @@ update_script() { log_update_summary return 1 fi + sync_ssh_monitor_perms_lib || true WATCHDOG_CHANGED=0 if ! ensure_watchdog_installed; then @@ -1217,9 +1422,11 @@ update_script() { 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 @@ -1288,6 +1495,8 @@ update_script() { 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 @@ -1303,6 +1512,7 @@ update_script() { parse_command_line "$@" deploy_require_root +require_repo_url if [ "$DEPLOY_MODE" -eq 1 ]; then printf '\n=== ssh-monitor: первичная установка (--deploy) ===\n' @@ -1322,11 +1532,15 @@ if ! ensure_directories; then exit 1 fi -debug_step "Начинаем проверку и установку ipset (КРИТИЧЕСКИЙ ШАГ)" -if ! ensure_ipset_installed; then - log_message "ERROR" "CRITICAL: ipset installation or verification failed" - echo "ERROR: Работа обновленного скрипта невозможна без установленного ipset" >&2 - exit 1 +maybe_enable_deploy_for_missing_install + +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 "$@" diff --git a/version.txt b/version.txt index 6cf3433..d09524a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.6-SAC +2.1.8-SAC