Если у вас несколько серверов и доступ к ним имеют коллеги, подрядчики или 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или пользователями из определённой группы, добавьте в скрипт быстрые проверки и выход при несоответствии.

Подход 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

Сравнение подходов
- 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).
Диагностика типичных проблем
- Скрипт PAM не срабатывает: проверьте
UsePAM yesв конфиге SSH и корректность строки сpam_exec.soв/etc/pam.d/sshd. - Почта не уходит: нет
mailили не настроен локальный MTA; временно полагайтесь наloggerи вебхуки. - Сервис наблюдателя не стартует: проверьте журнал сервиса и права на скрипт; удостоверьтесь, что
journalctlдоступен и фильтр-t sshdдействительно выдаёт строки. - Слишком много алертов: ослабьте чувствительность — расширьте белые списки, увеличьте окно троттлинга, отправляйте отдельные типы событий в разные каналы.
Итоги
Для уведомлений о входах в SSH не нужна тяжёлая инфраструктура. Вариант через pam_exec обеспечивает точные и быстрые алерты на открытие сессии, а наблюдение journald даёт гибкость и покрывает неудачные попытки. Простые скрипты, один‑два unit‑файла и пара рекомендаций по безопасности — и у вас появляется оперативная видимость доступов и аккуратный аудит. При росте потребностей эти же подходы масштабируются: добавляете маршрутизацию по важности, централизованный сбор логов и интеграцию с вашей системой инцидент‑менеджмента.


