Новинка Виртуальный VDS сервер в Нидерландах от 390р
Выберите продукт

Уведомления о входах в SSH: pam_exec, journald и отправка алертов

Хотите мгновенно знать, кто и откуда вошёл по SSH? Разбираем практичные схемы оповещений: срабатывание на этапе PAM (pam_exec) и отслеживание логов через journald. Дам готовые скрипты, unit‑файлы systemd, подсказки по троттлингу, фильтрации и безопасной доставке по почте и вебхукам.
Уведомления о входах в SSH: pam_exec, journald и отправка алертов

Если у вас несколько серверов и доступ к ним имеют коллеги, подрядчики или CI, уведомления о входах в SSH помогают быстро заметить необычную активность и сократить время реакции. В этой статье разберём две надёжные техники оповещений: выполнение скрипта на этапе PAM через pam_exec и анализ логов в реальном времени через journald. Плюс — практичные варианты доставки алертов: локальная почта, вебхуки, запись в журнал с последующей пересылкой.

Какие события отслеживать и зачем

Базовый минимум — успешные интерактивные входы по SSH. Но в реальности полезно разделять:

  • успешные входы (кто, откуда, чем: ключ/пароль/двухфакторка),
  • входы под root и в группы с повышенными привилегиями,
  • неинтерактивные сессии (SCP/SFTP, автоматизация из CI),
  • подозрительные попытки (неизвестный пользователь, множество неудачных паролей, география/IP вне белого списка).

Чем раньше вы видите отклонения, тем проще выключить доступ, отозвать ключи или включить временные ограничения.

Подход 1: мгновенное срабатывание через pam_exec

Модуль pam_exec.so запускает указанный скрипт на определённой фазе PAM. Для SSH нас интересуют события открытия/закрытия сессии (open_session/close_session). Преимущество подхода — нет парсинга строк логов, событие приходит детерминированно, с параметрами в переменных окружения (PAM_USER, PAM_RHOST, PAM_TTY, PAM_SERVICE, PAM_TYPE).

Шаг 1. Скрипт алертов

Создадим небольшой скрипт, который формирует уведомление и отправляет его по одному или нескольким каналам. В нём предусмотрим:

  • троттлинг от повторов (например, при пересоздании сессии),
  • фильтрацию служебных или известных безопасных источников,
  • возможность отправки в почту и вебхук (через переменную окружения).
#!/usr/bin/env bash
set -euo pipefail

# /usr/local/bin/ssh-login-alert.sh
# Отправляет оповещение при событиях PAM для sshd

ACTION="${PAM_TYPE:-unknown}"            # open_session | close_session | auth | ...
SERVICE="${PAM_SERVICE:-?}"             # обычно sshd
USER="${PAM_USER:-?}"
RHOST="${PAM_RHOST:-}"
TTY="${PAM_TTY:-?}"

# Пытаемся извлечь удалённый адрес из SSH_CONNECTION, если PAM_RHOST пуст
if ; then
  RHOST="$(awk '{print $1}' <<< "$SSH_CONNECTION" 2>/dev/null || true)"
fi

# Белый список источников (например, CI/CD подсети)
ALLOW_RE="^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)"

HOSTNAME="$(hostname -f 2>/dev/null || hostname)"
WHEN="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
SUBJECT="SSH ${ACTION}: ${USER}@${HOSTNAME} from ${RHOST:-unknown}"

# Троттлинг: одинаковые события в течение 60 секунд не шлём
STATE_DIR="/run/ssh-login-alert"
mkdir -p "$STATE_DIR"
KEY="${ACTION}_${SERVICE}_${USER}_${RHOST}_${TTY}"
HASH="$(printf '%s' "$KEY" | sha256sum | awk '{print $1}')"
STAMP="$STATE_DIR/$HASH"
NOW="$(date +%s)"
if ; then
  LAST="$(cat "$STAMP" 2>/dev/null || echo 0)"
  if (( NOW - LAST < 60 )); then
    exit 0
  fi
fi
echo "$NOW" > "$STAMP"

# Подавляем безопасные источники заранее
if ; then
  exit 0
fi

BODY="time=${WHEN}
host=${HOSTNAME}
user=${USER}
action=${ACTION}
service=${SERVICE}
rhost=${RHOST:-unknown}
tty=${TTY}
"

# Канал 1: запись в журнал
logger -t ssh-login-alert -- "$SUBJECT"

# Канал 2: почта локальному администратору (настроенный MTA/mailx)
if command -v mail >/dev/null 2>&1; then
  printf '%s' "$BODY" | mail -s "$SUBJECT" root || true
fi

# Канал 3: вебхук (если задан WEBHOOK_URL в окружении)
if  && command -v curl >/dev/null 2>&1; then
  curl -m 3 -sS -X POST -H "Content-Type: application/json" -d "{
    \\"subject\\": \\"$SUBJECT\\",
    \\"host\\": \\"$HOSTNAME\\",
    \\"user\\": \\"$USER\\",
    \\"action\\": \\"$ACTION\\",
    \\"rhost\\": \\"${RHOST:-unknown}\\",
    \\"when\\": \\"$WHEN\\"
  }" "$WEBHOOK_URL" >/dev/null || true
fi

exit 0

Сделайте файл исполняемым и ограничьте права:

chown root:root /usr/local/bin/ssh-login-alert.sh
chmod 0750 /usr/local/bin/ssh-login-alert.sh

Шаг 2. Подключаем pam_exec в PAM для SSH

В разных дистрибутивах файл PAM для SSH может называться по‑разному, но чаще всего это /etc/pam.d/sshd. Добавьте строку модуля для фазы session:

# /etc/pam.d/sshd
# ... оставьте остальные строки как есть ...
session optional pam_exec.so seteuid /usr/local/bin/ssh-login-alert.sh

Ключ seteuid запускает скрипт с эффективным UID пользователя, что иногда важно для окружения; при необходимости можно убрать. Убедитесь, что в конфигурации SSH включено UsePAM yes, затем перезапустите службу:

sshd -t
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || systemctl restart sshd

Проверка

Откройте новую SSH‑сессию с другого терминала, затем проверьте журнал:

journalctl -t ssh-login-alert -n 20 --no-pager

Также убедитесь, что на почту пользователя root пришло письмо (при наличии локального MTA). Если используете вебхук, установите WEBHOOK_URL в окружении юнита SSH или глобально (через /etc/environment) — и протестируйте повторно.

Совет: если нужны уведомления только о входах под root или пользователями из определённой группы, добавьте в скрипт быстрые проверки и выход при несоответствии.

Панель с событиями входов по SSH и уведомлениями

Подход 2: анализ логов в реальном времени через journald

Второй способ — не трогать PAM, а «слушать» записи journald от sshd в режиме stream и реагировать на определённые шаблоны сообщений, например Accepted, Failed password, Invalid user. Это удобно для обогащения алертов (тип авторизации: пароль/ключ), а также для наблюдения за неудачными попытками.

Скрипт наблюдателя journald

Скрипт ниже подписывается на сообщения идентификатора sshd и разбирает стандартные форматы OpenSSH. Для защиты от флуда есть простой троттлинг по ключу события.

#!/usr/bin/env bash
set -euo pipefail

# /usr/local/bin/ssh-journal-watch.sh
# Реагирует на строки sshd в journald, формирует и отправляет алерты

STATE_DIR="/run/ssh-journal-watch"
mkdir -p "$STATE_DIR"
HOSTNAME="$(hostname -f 2>/dev/null || hostname)"

send_alert() {
  local subject="$1"
  local body="$2"
  logger -t ssh-journal-watch -- "$subject"
  if command -v mail >/dev/null 2>&1; then
    printf '%s' "$body" | mail -s "$subject" root || true
  fi
  if  && command -v curl >/dev/null 2>&1; then
    local line_preview
    line_preview="$(printf '%s' "$body" | head -n1)"
    curl -m 3 -sS -X POST -H "Content-Type: application/json" -d "{
      \\"subject\\": \\"$subject\\",
      \\"host\\": \\"$HOSTNAME\\",
      \\"line\\": \\"$line_preview\\"
    }" "$WEBHOOK_URL" >/dev/null || true
  fi
}

throttle() {
  local key="$1"
  local hash
  hash="$(printf '%s' "$key" | sha256sum | awk '{print $1}')"
  local stamp="$STATE_DIR/$hash"
  local now
  now="$(date +%s)"
  if ; then
    local last
    last="$(cat "$stamp" 2>/dev/null || echo 0)"
    if (( now - last < 30 )); then
      return 1
    fi
  fi
  echo "$now" > "$stamp"
  return 0
}

# Подписка на sshd: -t sshd, начиная с текущего момента (-n0, -f)
journalctl -f -n0 -t sshd -o cat | while IFS= read -r line; do
  # Примеры:
  # Accepted publickey for user from 203.0.113.10 port 51190 ssh2: RSA SHA256:...
  # Accepted password for user from 203.0.113.10 port 51190 ssh2
  # Failed password for user from 203.0.113.10 port 51190 ssh2
  # Invalid user ghost from 203.0.113.10 port 51190
  if +)\ from\ ([^[:space:]]+)\  ]]; then
    method="${BASH_REMATCH[1]}"
    user="${BASH_REMATCH[2]}"
    rhost="${BASH_REMATCH[3]}"
    key="ok_${user}_${rhost}_${method}"
    if throttle "$key"; then
      ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
      subj="SSH accepted: ${user}@${HOSTNAME} from ${rhost} (${method})"
      body="time=${ts}
host=${HOSTNAME}
user=${user}
method=${method}
rhost=${rhost}
line=${line}
"
      send_alert "$subj" "$body"
    fi
  elif +)\ from\ ([^[:space:]]+)\  ]]; then
    user="${BASH_REMATCH[1]}"
    rhost="${BASH_REMATCH[2]}"
    key="fail_${user}_${rhost}"
    if throttle "$key"; then
      ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
      subj="SSH failed password: ${user} from ${rhost}"
      body="time=${ts}
host=${HOSTNAME}
user=${user}
rhost=${rhost}
line=${line}
"
      send_alert "$subj" "$body"
    fi
  elif +)\ from\ ([^[:space:]]+)\  ]]; then
    user="${BASH_REMATCH[1]}"
    rhost="${BASH_REMATCH[2]}"
    key="invalid_${user}_${rhost}"
    if throttle "$key"; then
      ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
      subj="SSH invalid user: ${user} from ${rhost}"
      body="time=${ts}
host=${HOSTNAME}
user=${user}
rhost=${rhost}
line=${line}
"
      send_alert "$subj" "$body"
    fi
  fi
done

Unit systemd для наблюдателя

Оформим скрипт как сервис systemd, чтобы он стартовал на буте и перезапускался при сбоях.

# /etc/systemd/system/ssh-journal-watch.service
[Unit]
Description=SSH journald watcher (alerts on login attempts)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/ssh-journal-watch.sh
Restart=always
RestartSec=2s
User=root
Group=root
Nice=5
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
ProtectHome=yes

[Install]
WantedBy=multi-user.target

Активируем сервис:

systemctl daemon-reload
systemctl enable --now ssh-journal-watch.service
systemctl status ssh-journal-watch.service --no-pager

Наблюдение journald и unit systemd для алертов SSH

Сравнение подходов

  • pam_exec: срабатывает гарантированно при открытии сессии, не зависит от формата логов; удобно выделять интерактивные входы. Требует правки PAM и аккуратности в сценариях обновлений.
  • journald: лёгкий запуск и выключение, хорошо подходит для наблюдения и за неуспешными попытками; но зависит от формата сообщений OpenSSH и может потребовать доработки при апгрейде.

На практике часто совмещают оба подхода: PAM — для точного и моментального уведомления об успешном входе, journald — для статистики неудачных попыток и дополнительных триггеров.

Каналы доставки алертов

Локальная почта

Самый простой канал — локальная почта на узле. Пакеты вроде mailx и минимальный MTA отправят письмо пользователю root (с последующей пересылкой админам). Не забудьте настроить систему так, чтобы почта не терялась: задать корректный домен, обратный адрес, правила форварда.

Вебхуки и чат‑боты

В примерах выше задействуется переменная окружения WEBHOOK_URL. Это позволяет гибко интегрировать любой канал: корпоративный чат, чат‑бот, собственный «приёмник событий». Не храните URL с токенами в скрипте: используйте переменные окружения или отдельный защищённый файл с правами 0600 и EnvironmentFile= в unit.

Посредством системных журналов

Запись в journald через logger удобна, если у вас уже настроена пересылка журналов на центральный узел. Тогда доставку, агрегацию и ролевой доступ берёт на себя существующая система логирования.

Тестирование и отладка

  • Проверяйте sshd -t после любых изменений конфигурации SSH.
  • Смотрите, что приходит в окружение PAM: временно добавьте вывод переменных в отдельный лог и убедитесь, что скрипт отрабатывает только на open_session.
  • Для journald‑наблюдателя используйте journalctl -t sshd -f, чтобы видеть «сырые» строки, и корректируйте регулярные выражения.
  • Создайте тестового пользователя и сделайте как успешный вход, так и пару «провалов», чтобы проверить условия и троттлинг.

Тонкая настройка: фильтры, приоритеты, частота

Несколько полезных приёмов, которые помогают избежать шума и ложных срабатываний:

  • Фильтрация источников: разрешить «тихие» логины из CI/CD‑подсетей, но слать алерты со всех остальных.
  • Повышать приоритет событий для root или членов группы sudo — например, менять тему письма и всегда дублировать в чат.
  • Разнести троттлинг по типам событий: для «Accepted» — 30–60 секунд, для «Failed» — 5–10 секунд, чтобы не потерять динамику атак.
  • Добавлять геоинформацию и обратные DNS‑имена — оффлайн или на стороне приёмника вебхуков, чтобы не тормозить скрипт на сервере.

Надёжность и безопасность

  • Права скриптов: владелец root, режим не шире 0750, без возможности записи для других.
  • Переменные окружения с секретами (токены вебхуков) не храните в мире: используйте EnvironmentFile с правами 0600 и минимизируйте доступ.
  • Троттлинг и отказоустойчивость: рестарт сервиса journald‑наблюдателя, аккуратное поведение при недоступности почты или сети (скрипт не должен «ронять» PAM).
  • Учёт формата логов: при обновлении OpenSSH проверяйте, не изменились ли тексты сообщений, и при необходимости правьте регулярные выражения.

Персистентность журналов и аудит

Если вы полагаетесь на анализ journald, убедитесь, что логи сохраняются на диск и не исчезают после перезагрузки. Проверьте Storage=persistent и разумные лимиты использования места. Для долгосрочного аудита используйте центральный сбор журналов или периодическую выгрузку на внешний узел хранения. На центральный сбор логов удобно выделить отдельный VDS, чтобы разгрузить рабочие сервера и упростить доступ к истории.

Если управляете серверами через панель, взгляните на сравнение популярных решений: сравнение панелей для VDS (2025).

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды

Диагностика типичных проблем

  • Скрипт PAM не срабатывает: проверьте UsePAM yes в конфиге SSH и корректность строки с pam_exec.so в /etc/pam.d/sshd.
  • Почта не уходит: нет mail или не настроен локальный MTA; временно полагайтесь на logger и вебхуки.
  • Сервис наблюдателя не стартует: проверьте журнал сервиса и права на скрипт; удостоверьтесь, что journalctl доступен и фильтр -t sshd действительно выдаёт строки.
  • Слишком много алертов: ослабьте чувствительность — расширьте белые списки, увеличьте окно троттлинга, отправляйте отдельные типы событий в разные каналы.

Итоги

Для уведомлений о входах в SSH не нужна тяжёлая инфраструктура. Вариант через pam_exec обеспечивает точные и быстрые алерты на открытие сессии, а наблюдение journald даёт гибкость и покрывает неудачные попытки. Простые скрипты, один‑два unit‑файла и пара рекомендаций по безопасности — и у вас появляется оперативная видимость доступов и аккуратный аудит. При росте потребностей эти же подходы масштабируются: добавляете маршрутизацию по важности, централизованный сбор логов и интеграцию с вашей системой инцидент‑менеджмента.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...