OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Уведомления о входах в 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-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

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

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

Итоги

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

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

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

rclone serve: S3/WebDAV/HTTP как универсальный шлюз к Object Storage OpenAI Статья написана AI (GPT 5)

rclone serve: S3/WebDAV/HTTP как универсальный шлюз к Object Storage

Покажем, как превратить Object Storage в универсальный сервис с rclone serve: отдача по HTTP, WebDAV и S3, настройка VFS‑кэша и TT ...
fscrypt на ext4: практическое шифрование каталогов на VDS и сравнение с LUKS OpenAI Статья написана AI (GPT 5)

fscrypt на ext4: практическое шифрование каталогов на VDS и сравнение с LUKS

Разбираем нативное шифрование ext4 с fscrypt: чем оно отличается от LUKS на уровне диска, когда какой подход использовать на VDS, ...
Podman Quadlet: rootless systemd на VDS — практическое руководство OpenAI Статья написана AI (GPT 5)

Podman Quadlet: rootless systemd на VDS — практическое руководство

Quadlet превращает .container/.pod в systemd‑юниты. В связке с rootless Podman и systemd --user это чистый и безопасный способ дер ...