"Быстрый
<br />
Relay на Gunicorn — это Python Flask-приложение, которое принимает webhook-запросы от систем мониторинга и пересылает их в Telegram через Bot API. Настройка занимает 20-30 минут.</p>
<p>Шаги: создать Telegram-бота через @BotFather, получить токен и chat_id, развернуть Flask-приложение, запустить через Gunicorn + systemd, настроить fail2ban для защиты эндпоинта.</p>
<p>Подключается к: Alertmanager (Prometheus), Zabbix, Grafana, CrowdSec, The Dude (<a class="wpil_keyword_link" href="https://it-apteka.com/tag/mikrotik/" target="_blank" rel="noopener" title="mikrotik" data-wpil-keyword-link="linked" data-wpil-monitor-id="2921">MikroTik</a>). Все системы шлют POST/GET на один эндпоинт.<br />
<h2>Зачем это нужно, если есть встроенная интеграция</h2>
<p>Начало 2025 года. Ты поднимаешь <a class="wpil_keyword_link" href="https://it-apteka.com/category/monitoring/" target="_blank" rel="noopener" title="Мониторинг" data-wpil-keyword-link="linked" data-wpil-monitor-id="2922">мониторинг</a>, настраиваешь Alertmanager, добавляешь telegram_config. Идёшь спать спокойно. В 3 ночи падает сервис. Алерт не приходит. Потому что РКН снова «частично ограничил» работу <a class="wpil_keyword_link" href="https://t.me/it_apteka_com/34" target="_blank" rel="noopener" title="Telegram" data-wpil-keyword-link="linked" data-wpil-monitor-id="2923">Telegram</a> в России.</p>
<p>С августа 2025 года Роскомнадзор официально ввёл ограничения на звонки в <a href="https://it-apteka.com/kak-ugnali-gosuslugi-cherez-telegram-i-whatsapp-shemy-moshennikov-zashhita-i-vosstanovlenie/" title="Как угнали Госуслуги через Telegram и WhatsApp: схемы мошенников, защита и восстановление" target="_blank" rel="noopener" data-wpil-monitor-id="2916">Telegram и WhatsApp</a>. До этого были замедления в феврале 2026-го — задержки в доставке сообщений достигали нескольких минут. Прямые запросы к <code>api.telegram.org</code> с российских серверов периодически не проходят или идут с огромными задержками.</p>
<p>Это прямой удар по инфраструктуре мониторинга. Alertmanager не умеет ретраить через прокси. Zabbix пишет «Couldn’t connect to server» и молчит. Grafana просто не отправляет уведомление.</p>
<p>Relay на Gunicorn решает три проблемы сразу:</p>
<ul>
<li>Централизованная точка отправки — все системы мониторинга шлют алерты в одно место, а relay уже разбирается с доставкой</li>
<li>Прокси через VPS за рубежом — relay разворачивается на сервере вне зоны ограничений, и доступ к Telegram Bot API всегда работает</li>
<li>Единый формат сообщений — нормализуешь JSON от Alertmanager, Zabbix, CrowdSec в читаемые Telegram-сообщения в одном месте, не в пяти разных конфигах</li>
</ul>
<p>Ещё один сценарий: у тебя homelab или корпоративная сеть, и несколько систем мониторинга работают в изолированном сегменте без прямого доступа в интернет. Relay стоит в DMZ или на пограничном узле и принимает запросы изнутри, а наружу ходит только он.</p>
<p>Что ты получишь из этой статьи: рабочий Python-relay на Flask + Gunicorn, настроенный как systemd-сервис, с защитой через fail2ban, и примеры подключения для Alertmanager, Zabbix, Grafana, CrowdSec и The Dude. Всё с реальными конфигами, без абстракций.</p>
<h2>Системные требования</h2>
<table>
<thead>
<tr>
<th>Компонент</th>
<th>Минимум</th>
<th>Рекомендовано</th>
</tr>
</thead>
<tbody>
<tr>
<td>ОС</td>
<td><a href="https://it-apteka.com/linux-v-aprele-2026-jadro-7-0-ubuntu-26-04-lts-i-francija-brosaet-windows/" title="Linux в апреле 2026: ядро 7.0, Ubuntu 26.04 LTS и Франция бросает Windows" target="_blank" rel="noopener" data-wpil-monitor-id="2917">Ubuntu 22.04 LTS</a></td>
<td>Ubuntu 24.04 LTS</td>
</tr>
<tr>
<td>Python</td>
<td>3.10</td>
<td>3.12</td>
</tr>
<tr>
<td>Gunicorn</td>
<td>21.x</td>
<td>23.x</td>
</tr>
<tr>
<td>Flask</td>
<td>3.0</td>
<td>3.1</td>
</tr>
<tr>
<td>RAM</td>
<td>256 MB</td>
<td>512 MB</td>
</tr>
<tr>
<td>Диск</td>
<td>1 GB</td>
<td>5 GB (логи)</td>
</tr>
<tr>
<td>fail2ban</td>
<td>1.0.x</td>
<td>1.1.x</td>
</tr>
</tbody>
</table>
<p>На момент публикации актуальна версия Gunicorn 23.0 и Flask 3.1. Перед установкой проверь свежие релизы на <a href="https://pypi.org/project/gunicorn/" rel="nofollow" target="_blank">PyPI</a>.</p>
<h2>Архитектура решения</h2>
<pre class="mermaid">%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Alertmanager"] --> R
B["Zabbix"] --> R
C["Grafana"] --> R
D["CrowdSec"] --> R
E["The Dude"] --> R
R["Gunicorn Relay :8000"] --> F["fail2ban защита"]
F --> G["Flask /alert endpoint"]
G --> H["Telegram Bot API"]
H --> I["Ваш чат / группа"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style C fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style D fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style E fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style R fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style F fill:#f8fafc,stroke:#ef4444,stroke-width:2px,color:#dc2626
style G fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style H fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style I fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
</pre>
<h2>Шаг 1. Создаём Telegram-бота</h2>
<p>Открой Telegram. Найди <code>@BotFather</code>. Отправь команду <code>/newbot</code> и следуй инструкциям — задай имя бота и username (должен заканчиваться на <code>bot</code>).</p>
<p>BotFather выдаст токен вида <code>1234567890:AAHbcdefGHIJKLMNOPQRSTUVWXYZ</code>. Сохрани его — он понадобится в конфиге.</p>
<p>Теперь узнай chat_id. Добавь бота в нужный чат или группу, затем выполни запрос:</p>
<pre><code class="language-bash">
curl -s "https://api.telegram.org/bot<ВАШ_ТОКЕН>/getUpdates" | python3 -m json.tool
</code></pre>
<p>Найди в ответе поле <code>chat.id</code>. Для личного чата это положительное число, для группы — отрицательное (например, <code>-1001234567890</code>).</p>
"Проверь
<br />
Если запрос выше зависает или возвращает ошибку — ты на российском сервере и api.telegram.org недоступен. Устанавливай relay на зарубежный VPS, а не на продакшн-сервер внутри РФ.<br />
<pre><code class="language-bash">
# Быстрая проверка доступности Bot API
curl -m 5 -s "https://api.telegram.org/bot<ТОКЕН>/getMe" && echo "OK" || echo "BLOCKED"
</code></pre>
<h2>Шаг 2. Готовим окружение</h2>
<p>Создаём отдельного пользователя для relay. Это обязательно — не запускай сервисы от root.</p>
<pre><code class="language-bash">
useradd -m -s /bin/bash tgrelay
mkdir -p /opt/tgrelay
chown tgrelay:tgrelay /opt/tgrelay
</code></pre>
<p>Ставим зависимости и создаём виртуальное окружение:</p>
<pre><code class="language-bash">
apt update && apt install -y python3.12 python3.12-venv python3-pip fail2ban
su - tgrelay
cd /opt/tgrelay
python3.12 -m venv venv
source venv/bin/activate
pip install flask gunicorn requests
</code></pre>
<h2>Шаг 3. Пишем relay-приложение</h2>
<p>Создай файл <code>/opt/tgrelay/app.py</code>. Это сердце всей конструкции — Flask-приложение принимает запросы, форматирует сообщения и отправляет их в Telegram.</p>
<pre><code class="language-python">
import os
import logging
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# --- Конфигурация ---
TG_TOKEN = os.environ.get("TG_TOKEN", "")
TG_CHAT_ID = os.environ.get("TG_CHAT_ID", "")
SECRET_TOKEN = os.environ.get("SECRET_TOKEN", "changeme")
# --- Логирование ---
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("/opt/tgrelay/relay.log"),
logging.StreamHandler()
]
)
log = logging.getLogger(__name__)
def send_telegram(text: str) -> bool:
url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage"
payload = {
"chat_id": TG_CHAT_ID,
"text": text,
"parse_mode": "HTML"
}
try:
r = requests.post(url, json=payload, timeout=10)
r.raise_for_status()
return True
except Exception as e:
log.error(f"Telegram send error: {e}")
return False
def format_alertmanager(data: dict) -> str:
lines = []
for alert in data.get("alerts", []):
status = alert.get("status", "unknown").upper()
name = alert.get("labels", {}).get("alertname", "Unknown")
severity = alert.get("labels", {}).get("severity", "")
instance = alert.get("labels", {}).get("instance", "")
summary = alert.get("annotations", {}).get("summary", "")
icon = "🔴" if status == "FIRING" else "✅"
lines.append(
f"{icon} <b>[{status}] {name}</b>\n"
f"Severity: {severity}\n"
f"Instance: {instance}\n"
f"Summary: {summary}"
)
return "\n\n".join(lines) if lines else "Empty alert payload"
@app.route("/alert", methods=["POST"])
def alert():
token = request.headers.get("X-Token", "")
if token != SECRET_TOKEN:
log.warning(f"Unauthorized request from {request.remote_addr}")
return jsonify({"error": "unauthorized"}), 403
data = request.get_json(silent=True) or {}
source = request.args.get("source", "generic")
if source == "alertmanager":
text = format_alertmanager(data)
elif source == "zabbix":
subject = data.get("subject", "Zabbix Alert")
message = data.get("message", "")
text = f"<b>Zabbix:</b> {subject}\n{message}"
elif source == "crowdsec":
ip = data.get("ip", "unknown")
reason = data.get("reason", "")
text = f"<b>CrowdSec ban:</b> <code>{ip}</code>\nReason: {reason}" else: # Универсальный fallback: передаём любой текст text = data.get("text", str(data)) if not text: return jsonify({"error": "empty message"}), 400 ok = send_telegram(text) if ok: log.info(f"Alert sent from {request.remote_addr}, source={source}") return jsonify({"status": "sent"}), 200 else: return jsonify({"error": "telegram delivery failed"}), 502 @app.route("/health", methods=["GET"]) def health(): return jsonify({"status": "ok"}), 200 if __name__ == "__main__": app.run(host="127.0.0.1", port=8000) </code></pre>
<p>Создай файл конфигурации <code>/opt/tgrelay/config.env</code>:</p>
<pre><code class="language-text">
TG_TOKEN=1234567890:AAHbcdefGHIJKLMNOPQRSTUVWXYZ
TG_CHAT_ID=-1001234567890
SECRET_TOKEN=supersecrettoken123
</code></pre>
<pre><code class="language-bash">
chmod 600 /opt/tgrelay/config.env
chown tgrelay:tgrelay /opt/tgrelay/config.env
</code></pre>
<h2>Шаг 4. Настраиваем Gunicorn</h2>
<p>Создай файл конфигурации Gunicorn <code>/opt/tgrelay/gunicorn.conf.py</code>:</p>
<pre><code class="language-python">
bind = "127.0.0.1:8000"
workers = 2
worker_class = "sync"
timeout = 30
keepalive = 5
accesslog = "/opt/tgrelay/access.log"
errorlog = "/opt/tgrelay/error.log"
loglevel = "info"
proc_name = "tgrelay"
pidfile = "/opt/tgrelay/gunicorn.pid"
user = "tgrelay"
group = "tgrelay"
</code></pre>
<p>Два воркера достаточно — relay не CPU-hungry, основная задача это I/O ожидание ответа от Telegram. Если у тебя больше 10 источников алертов и высокая частота — подними до 4.</p>
<h2>Шаг 5. Systemd-сервис</h2>
<p>Создай файл <code>/etc/systemd/system/tgrelay.service</code>:</p>
<pre><code class="language-text">
[Unit]
Description=Telegram Alert Relay (Gunicorn)
After=network.target
Wants=network-online.target
[Service]
Type=notify
User=tgrelay
Group=tgrelay
WorkingDirectory=/opt/tgrelay
EnvironmentFile=/opt/tgrelay/config.env
ExecStart=/opt/tgrelay/venv/bin/gunicorn \
--config /opt/tgrelay/gunicorn.conf.py \
app:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardError=journal
[Install]
WantedBy=multi-user.target
</code></pre>
<pre><code class="language-bash">
systemctl daemon-reload
systemctl enable tgrelay
systemctl start tgrelay
systemctl status tgrelay
</code></pre>
<p>Проверь что relay слушает:</p>
<pre><code class="language-bash">
ss -tlnp | grep 8000
curl -s http://127.0.0.1:8000/health
</code></pre>
<p>Должен вернуть <code>{"status": "ok"}</code>. Если видишь это — сервис живой, двигаемся дальше.</p>
<h2>Шаг 6. Nginx как reverse proxy (опционально, но правильно)</h2>
<p>Не выставляй Gunicorn напрямую в интернет. Поставь перед ним Nginx — он отдаёт rate limiting, TLS-терминацию и скрывает детали реализации.</p>
<pre><code class="language-text">
server {
listen 443 ssl;
server_name relay.example.com;
ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem;
# Rate limiting для защиты от спама
limit_req_zone $binary_remote_addr zone=relay:10m rate=10r/m;
location /alert {
limit_req zone=relay burst=5 nodelay;
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 30s;
}
location /health {
proxy_pass http://127.0.0.1:8000;
}
}
</code></pre>
<pre><code class="language-bash">
nginx -t && systemctl reload nginx
</code></pre>
<h2>Шаг 7. Защита через fail2ban</h2>
<p>Relay открыт к интернету — значит, его будут сканировать. Fail2ban блокирует адреса, которые долбятся с неверным токеном или слишком часто. Настройка минимальная, но работает.</p>
<p>Создай фильтр <code>/etc/fail2ban/filter.d/tgrelay.conf</code>:</p>
<pre><code class="language-text">
[Definition]
failregex = .*Unauthorized request from .*
.*"(GET|POST|PUT|DELETE) /alert.* (401|403).*
ignoreregex =
</code></pre>
<p>Создай jail <code>/etc/fail2ban/jail.d/tgrelay.local</code>:</p>
<pre><code class="language-text">
[tgrelay]
enabled = true
port = http,https,8000
filter = tgrelay
logpath = /opt/tgrelay/relay.log
/var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-multiport[name=tgrelay, port="80,443,8000", protocol=tcp]
%(action_mwl)s
</code></pre>
<p>Теперь добавь отправку уведомления в Telegram при каждом бане. Создай action-файл <code>/etc/fail2ban/action.d/telegram-notify.conf</code>:</p>
<pre><code class="language-text">
[Definition]
actionban = curl -s -X POST \
"https://api.telegram.org/bot%(tg_token)s/sendMessage" \
-d "chat_id=%(tg_chat_id)s" \
-d "text=🚫 fail2ban ban: %0AJail: %(name)s%0ARetries: %(failures)s"
actionunban = curl -s -X POST \
"https://api.telegram.org/bot%(tg_token)s/sendMessage" \
-d "chat_id=%(tg_chat_id)s" \
-d "text=✅ fail2ban unban: %0AJail: %(name)s"
[Init]
tg_token = 1234567890:AAHbcdefGHIJKLMNOPQRSTUVWXYZ
tg_chat_id = -1001234567890
</code></pre>
<p>Добавь этот action к jail (в тот же <code>tgrelay.local</code>, секция action):</p>
<pre><code class="language-text">
action = iptables-multiport[name=tgrelay, port="80,443,8000", protocol=tcp]
telegram-notify[name=%(__name__)s]
</code></pre>
"Внимание:
<br />
Если relay стоит на российском сервере, curl в action_ban тоже может не достучаться до api.telegram.org. Используй для action только relay на зарубежном VPS или MTProto-прокси. Лучший вариант — отправлять fail2ban алерты через сам relay с localhost.<br />
<pre><code class="language-bash">
# Перезапускаем fail2ban
systemctl restart fail2ban
# Проверяем статус
fail2ban-client status tgrelay
# Тестируем бан вручную
fail2ban-client set tgrelay banip 1.2.3.4
fail2ban-client status tgrelay
fail2ban-client set tgrelay unbanip 1.2.3.4
</code></pre>
<h2>Таблица портов</h2>
<table>
<thead>
<tr>
<th>Порт</th>
<th>Протокол</th>
<th>Назначение</th>
<th>Доступен снаружи?</th>
</tr>
</thead>
<tbody>
<tr>
<td>8000</td>
<td>TCP</td>
<td>Gunicorn (внутренний)</td>
<td>Нет (только 127.0.0.1)</td>
</tr>
<tr>
<td>443</td>
<td>TCP</td>
<td>Nginx HTTPS -> relay</td>
<td>Да (разрешённые IP)</td>
</tr>
<tr>
<td>80</td>
<td>TCP</td>
<td>HTTP -> редирект на HTTPS</td>
<td>Да</td>
</tr>
<tr>
<td>9001</td>
<td>TCP</td>
<td>Gunicorn stats (опционально)</td>
<td>Нет</td>
</tr>
</tbody>
</table>
<h2>Подключаем Alertmanager (Prometheus)</h2>
<p>С версии Alertmanager 0.24 есть встроенная интеграция с Telegram через <code>telegram_configs</code>. Но она не работает через прокси и не форматирует сообщения гибко. Relay решает обе проблемы.</p>
<p>В <code>alertmanager.yml</code> добавь receiver:</p>
<pre><code class="language-text">
receivers:
- name: "telegram-relay"
webhook_configs:
- url: "https://relay.example.com/alert?source=alertmanager"
send_resolved: true
http_config:
headers:
X-Token: "supersecrettoken123"
route:
receiver: "telegram-relay"
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
</code></pre>
<pre><code class="language-bash">
# Проверяем конфиг Alertmanager
amtool check-config /etc/alertmanager/alertmanager.yml
# Перезапускаем
systemctl restart alertmanager
# Тестовый алерт
curl -XPOST http://localhost:9093/api/v1/alerts \
-H "Content-Type: application/json" \
-d '[{"labels":{"alertname":"TestAlert","severity":"warning"},"annotations":{"summary":"Test from curl"}}]'
</code></pre>
<h2>Подключаем Zabbix</h2>
<p>Начиная с Zabbix 5.0 есть встроенный webhook для <a href="https://it-apteka.com/rezervnoe-kopirovanie-mikrotik-routeros-7-v-telegram-avtomatizacija-za-20-minut/" title="Резервное копирование MikroTik RouterOS 7 в Telegram: рабочий скрипт и разбор ошибок" target="_blank" rel="noopener" data-wpil-monitor-id="2918">Telegram — он работает через скрипт</a> на JavaScript прямо в Zabbix Server. Из России не достучится. Используем curl-скрипт через наш relay.</p>
<p>Создай <a class="wpil_keyword_link" href="https://it-apteka.com/category/scripts/" target="_blank" rel="noopener" title="Скрипты" data-wpil-keyword-link="linked" data-wpil-monitor-id="2920">скрипт</a> <code>/usr/lib/zabbix/alertscripts/tgrelay.sh</code>:</p>
<pre><code class="language-bash">
#!/bin/bash
# Zabbix -> Telegram Relay
# $1 = to (игнорируем, chat_id в relay)
# $2 = subject
# $3 = message
RELAY_URL="https://relay.example.com/alert?source=zabbix"
SECRET="supersecrettoken123"
PAYLOAD=$(cat <</code></pre>
<pre><code class="language-bash">
chmod +x /usr/lib/zabbix/alertscripts/tgrelay.sh
chown zabbix:zabbix /usr/lib/zabbix/alertscripts/tgrelay.sh
</code></pre>
<p>В веб-интерфейсе Zabbix: Alerts -> Media Types -> Create media type. Тип: Script. Имя скрипта: <code>tgrelay.sh</code>. Параметры: <code>{ALERT.SENDTO}</code>, <code>{ALERT.SUBJECT}</code>, <code>{ALERT.MESSAGE}</code>.</p>
<p>Назначь media type пользователю в Users -> Media -> Add. В поле «Send to» можно поставить любое значение — relay его игнорирует.</p>
<h2>Подключаем Grafana</h2>
<p>В Grafana Alerting есть нативный Telegram contact point. Он снова идёт напрямую к <code>api.telegram.org</code>. Обходим это через webhook на наш relay.</p>
<p>В Grafana: Alerting -> Contact points -> Add contact point. Выбираем тип Webhook. URL:</p>
<pre><code class="language-text">
https://relay.example.com/alert?source=grafana
</code></pre>
<p>HTTP Headers добавь: <code>X-Token: supersecrettoken123</code>.</p>
<p>Grafana шлёт POST с JSON в своём формате. Добавь обработчик в <code>app.py</code> в функцию <code>alert()</code>:</p>
<pre><code class="language-python">
elif source == "grafana":
state = data.get("state", "unknown")
title = data.get("title", "Grafana Alert")
message = data.get("message", "")
rule_url = data.get("ruleUrl", "")
icon = "🔴" if state == "alerting" else "✅"
text = (
f"{icon} <b>Grafana [{state.upper()}]</b>\n"
f"{title}\n"
f"{message}\n"
f'<a href="{rule_url}" target="_blank">Открыть в Grafana</a>'
)
</code></pre>
<pre><code class="language-bash">
# После изменения app.py - перезапускаем relay
systemctl restart tgrelay
# Тест из Grafana: кнопка "Test" в contact point
</code></pre>
<h2>Подключаем CrowdSec</h2>
<p>CrowdSec использует HTTP notification plugin для отправки алертов. Именно через него подключается relay.</p>
<p>Создай <code>/etc/crowdsec/notifications/http_tgrelay.yaml</code>:</p>
<pre><code class="language-text">
type: http
name: http_tgrelay
log_level: info
group_wait: 10s
group_threshold: 5
max_retry: 3
url: https://relay.example.com/alert?source=crowdsec
method: POST
headers:
Content-Type: application/json
X-Token: supersecrettoken123
format: |
{
"ip": "{{range . }}{{.Source.Value}}{{end}}",
"reason": "{{range . }}{{.Scenario}}{{end}}",
"decisions": {{len .}}
}
</code></pre>
<p>В <code>/etc/crowdsec/profiles.yaml</code> добавь notification в секцию decisions:</p>
<pre><code class="language-text">
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
notifications:
- http_tgrelay
on_success: break
</code></pre>
<pre><code class="language-bash">
systemctl restart crowdsec
cscli notifications test http_tgrelay
</code></pre>
<h2>Подключаем The Dude (MikroTik)</h2>
<p>The Dude работает на RouterOS и не умеет делать HTTP POST. Зато умеет HTTP GET через <code>/tool fetch</code>. Relay поддерживает GET-запросы с текстом в параметре.</p>
<p>Добавь в <code>app.py</code> новый endpoint для GET:</p>
<pre><code class="language-python">
@app.route("/dude", methods=["GET"])
def dude():
token = request.args.get("token", "")
if token != SECRET_TOKEN:
log.warning(f"Dude: unauthorized from {request.remote_addr}")
return "403 Forbidden", 403
text = request.args.get("text", "")
device = request.args.get("device", "unknown")
event = request.args.get("event", "")
if not text and device:
text = f"<b>The Dude:</b> {device}\n{event}"
if text:
send_telegram(text)
return "OK", 200
return "400 Bad Request", 400
</code></pre>
<pre><code class="language-bash">
systemctl restart tgrelay
</code></pre>
<p>В The Dude создай новый Notification с типом «Execute on server». Используй RouterOS-скрипт:</p>
<pre><code class="language-text">
/tool fetch url="https://relay.example.com/dude?token=supersecrettoken123\
&device=[DeviceName]&event=[EventType]&text=[DeviceName] - [EventType]" \
keep-result=no
</code></pre>
"The
<br />
RouterOS 6.x иногда не принимает Let's Encrypt сертификаты. Если /tool fetch падает с SSL error — добавь сертификат в Trust List через /certificate import или используй HTTP (порт 80) с redirect на HTTPS только для MikroTik-сегмента.<br />
<h2>Проверка работы</h2>
<p>Проверяем relay комплексно — статус сервиса, логи, тестовый запрос:</p>
<pre><code class="language-bash">
# Статус сервиса
systemctl status tgrelay
# Логи в реальном времени
journalctl -fu tgrelay
# Health check
curl -s http://127.0.0.1:8000/health
# Тестовый алерт от Alertmanager
curl -s -X POST "http://127.0.0.1:8000/alert?source=alertmanager" \
-H "Content-Type: application/json" \
-H "X-Token: supersecrettoken123" \
-d '{"alerts":[{"status":"firing","labels":{"alertname":"TestAlert","severity":"critical","instance":"server1:9100"},"annotations":{"summary":"Test message from curl"}}]}'
# Тест Zabbix-формата
curl -s -X POST "http://127.0.0.1:8000/alert?source=zabbix" \
-H "Content-Type: application/json" \
-H "X-Token: supersecrettoken123" \
-d '{"subject":"PROBLEM: High CPU on db01","message":"CPU load is 95% for 5 minutes"}'
</code></pre>
<p>Смотри логи relay:</p>
<pre><code class="language-bash">
tail -f /opt/tgrelay/relay.log
tail -f /opt/tgrelay/access.log
</code></pre>
<h2>Мониторинг самого relay</h2>
<p>Relay — это SPOF. Если он упадёт — ты ничего не узнаешь, потому что уведомления будут идти через него же. Нужен независимый watchdog.</p>
<p>Самый простой вариант — cron-скрипт с прямым вызовом Bot API (для зарубежного сервера это работает напрямую):</p>
<pre><code class="language-bash">
cat > /opt/tgrelay/watchdog.sh << 'EOF'
#!/bin/bash
HEALTH=$(curl -s -m 5 http://127.0.0.1:8000/health)
if echo "$HEALTH" | grep -q '"ok"'; then
exit 0
fi
# relay не ответил - шлём напрямую через Bot API
curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT_ID}" \
-d "text=CRITICAL: tgrelay is DOWN on $(hostname)"
EOF
chmod +x /opt/tgrelay/watchdog.sh
</code></pre>
<pre><code class="language-bash">
# Добавляем в cron от root
crontab -e
# Добавить строку:
# */5 * * * * TG_TOKEN="ВАШ_ТОКЕН" TG_CHAT_ID="-1001234567890" /opt/tgrelay/watchdog.sh
</code></pre>
<h2>Резервное копирование</h2>
<p>Бэкапить нужно конфиги и код, не логи. Логи ротируй через logrotate.</p>
<pre><code class="language-bash">
# Создай /etc/logrotate.d/tgrelay
cat > /etc/logrotate.d/tgrelay << 'EOF'
/opt/tgrelay/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
postrotate
systemctl kill -s USR1 tgrelay || true
endscript
}
EOF
</code></pre>
<p>Бэкап конфигов (запускать перед обновлением):</p>
<pre><code class="language-bash">
tar czf /root/tgrelay-backup-$(date +%Y%m%d).tar.gz \
/opt/tgrelay/app.py \
/opt/tgrelay/gunicorn.conf.py \
/opt/tgrelay/config.env \
/etc/systemd/system/tgrelay.service \
/etc/fail2ban/jail.d/tgrelay.local \
/etc/fail2ban/filter.d/tgrelay.conf \
/etc/nginx/sites-available/tgrelay
</code></pre>
<h2>Обновление relay</h2>
<p>Обновление проходит по схеме: бэкап — обновление зависимостей — тест — рестарт.</p>
<pre><code class="language-bash">
# Создаём бэкап перед обновлением
tar czf /root/tgrelay-before-update.tar.gz /opt/tgrelay/
# Обновляем зависимости
su - tgrelay -c "
source /opt/tgrelay/venv/bin/activate
pip install --upgrade flask gunicorn requests
"
# Проверяем синтаксис app.py
python3 -c "import py_compile; py_compile.compile('/opt/tgrelay/app.py')" && echo "OK"
# Graceful reload без даунтайма
systemctl reload tgrelay
# Если reload не поддерживается - restart
systemctl restart tgrelay
# Проверяем что всё встало
curl -s http://127.0.0.1:8000/health
</code></pre>
<h2>Диагностика: типичные ошибки</h2>
<table>
<thead>
<tr>
<th>Симптом</th>
<th>Причина</th>
<th>Решение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>403 Forbidden</code> на /alert</td>
<td>Неверный X-Token в заголовке</td>
<td>Проверить SECRET_TOKEN в config.env и в источнике алертов</td>
</tr>
<tr>
<td>Telegram delivery failed</td>
<td>Bot API недоступен (блокировка РКН)</td>
<td>Relay на зарубежном VPS, проверить curl к api.telegram.org</td>
</tr>
<tr>
<td>502 Bad Gateway от Nginx</td>
<td>Gunicorn не запущен</td>
<td>systemctl status tgrelay, проверить ss -tlnp | grep 8000</td>
</tr>
<tr>
<td>Сообщения приходят пустые</td>
<td>JSON не парсится (неверный Content-Type)</td>
<td>Добавить -H «Content-Type: application/json» в curl-запросы</td>
</tr>
<tr>
<td>fail2ban не банит</td>
<td>Неверный regex в фильтре или путь к логу</td>
<td>fail2ban-regex /opt/tgrelay/relay.log /etc/fail2ban/filter.d/tgrelay.conf</td>
</tr>
<tr>
<td>The Dude SSL error</td>
<td>RouterOS не принимает сертификат</td>
<td>Импортировать CA в /certificate или использовать HTTP на отдельном порту</td>
</tr>
<tr>
<td>Zabbix: script не выполняется</td>
<td>Нет прав execute у скрипта</td>
<td>chmod +x /usr/lib/zabbix/alertscripts/tgrelay.sh</td>
</tr>
<tr>
<td>CrowdSec не шлёт уведомления</td>
<td>name в YAML не совпадает с profiles.yaml</td>
<td>Имя в http_tgrelay.yaml должно совпадать с notifications: — http_tgrelay</td>
</tr>
</tbody>
</table>
<pre><code class="language-bash">
# Отладка fail2ban regex
fail2ban-regex /opt/tgrelay/relay.log /etc/fail2ban/filter.d/tgrelay.conf
# Отладка CrowdSec нотификации
cscli notifications test http_tgrelay
journalctl -u crowdsec -n 50
# Проверка что app.py читает переменные окружения
systemctl show tgrelay --property=Environment
</code></pre>
<h2>Альтернативы: когда relay не нужен</h2>
<p>Relay — это дополнительный компонент инфраструктуры. Нужен он не всегда.</p>
<p>Прямая интеграция Alertmanager через <code>telegram_configs</code> работает если сервер находится вне России или ты используешь MTProto-прокси. Grafana с версии 9.0 нативно поддерживает Telegram contact point — настраивается за 2 минуты в UI. CrowdSec официально документирует прямую отправку в Telegram через http plugin без промежуточного relay.</p>
<p>Relay нужен когда: несколько систем мониторинга, сервер в России или за NAT, нужен единый формат сообщений, нужен audit log всех отправленных алертов, нужно добавить логику маршрутизации (critical -> дежурная группа, warning -> общий чат).</p>
<h2>Профилактика</h2>
<p>Проверяй работу relay раз в неделю — не вручную, а скриптом. Положи в cron тест с настоящим алертом и убедись, что сообщение пришло.</p>
<pre><code class="language-bash">
# Еженедельный тест (добавить в cron)
# 0 9 * * 1 /opt/tgrelay/test_weekly.sh
cat > /opt/tgrelay/test_weekly.sh << 'EOF' #!/bin/bash RESULT=$(curl -s -o /dev/null -w "%{http_code}" \ -X POST "http://127.0.0.1:8000/alert?source=zabbix" \ -H "Content-Type: application/json" \ -H "X-Token: supersecrettoken123" \ -d '{"subject":"Weekly relay test","message":"OK - relay is working"}') if [ "$RESULT" = "200" ]; then echo "$(date): weekly test OK" else echo "$(date): weekly test FAILED, HTTP $RESULT" >&2
fi
EOF
chmod +x /opt/tgrelay/test_weekly.sh
</code></pre>
<h2>FAQ</h2>
<h3>Почему алерты через Telegram не приходят после настройки?</h3>
<p>Первая причина — <code>api.telegram.org</code> недоступен с сервера. Проверь: <code>curl -m 5 https://api.telegram.org</code>. Если timeout — relay нужно ставить на зарубежный VPS или настраивать MTProto-прокси. Вторая причина — неверный токен в config.env. Третья — relay не запущен: <code>systemctl status tgrelay</code>.</p>
<h3>Как проверить что relay работает правильно?</h3>
<p>Выполни health check: <code>curl -s http://127.0.0.1:8000/health</code> — должен вернуть <code>{"status": "ok"}</code>. Потом отправь тестовый POST с корректным токеном и проверь что сообщение появилось в Telegram. Смотри лог: <code>tail -f /opt/tgrelay/relay.log</code>.</p>
<h3>Что делать если fail2ban заблокировал легитимный источник алертов?</h3>
<p>Разбань IP командой: <code>fail2ban-client set tgrelay unbanip 1.2.3.4</code>. Потом добавь этот IP в <code>ignoreip</code> в jail.local: <code>ignoreip = 127.0.0.1/8 192.168.0.0/16 1.2.3.4</code>. Перезапусти fail2ban. Также проверь правильность SECRET_TOKEN на стороне источника.</p>
<h3>Чем relay на Gunicorn отличается от встроенной интеграции Alertmanager?</h3>
<p>Встроенная интеграция работает только с одной системой мониторинга и идёт напрямую к <code>api.telegram.org</code>. Relay принимает запросы от любых источников, нормализует форматы, работает как прокси через безопасный узел, ведёт audit log. Минус — дополнительный компонент который нужно поддерживать.</p>
<h3>Можно ли отправлять алерты в несколько Telegram-чатов?</h3>
<p>Да. Добавь в <code>app.py</code> словарь <code>CHAT_ROUTING</code> с маппингом source -> chat_id. Например, critical алерты от Alertmanager идут в дежурный чат, Zabbix — в общий чат команды. Передавай нужный чат через query parameter <code>?chat=oncall</code>.</p>
<h2>Что в итоге</h2>
<p>Ты развернул relay на Gunicorn с Flask, который принимает алерты от пяти систем мониторинга и отправляет их в Telegram. Fail2ban блокирует попытки подбора токена и отправляет уведомление о каждом бане. Systemd держит сервис живым после перезагрузки. Nginx стоит спереди и ограничивает частоту запросов.</p>
<p>Проблема с <a href="https://it-apteka.com/blokirovka-reklamy-polnoe-rukovodstvo-po-ustanovke-i-nastrojke-v-2026-godu/" title="Блокировка рекламы: полное руководство по установке и настройке в 2026 году" target="_blank" rel="noopener" data-wpil-monitor-id="2919">блокировками РКН решается установкой</a> relay на зарубежный VPS — все внутренние системы мониторинга шлют алерты на внутренний endpoint, а наружу ходит только relay через стабильный канал.</p>
<p>Если что-то не заработало — пиши в комментарии, разберёмся. Конкретный симптом, версии компонентов и вывод <code>journalctl -fu tgrelay</code> ускорят диагностику в разы.</p>
Быстрый ответ
Relay на Gunicorn — это Python Flask-приложение, которое принимает webhook-запросы от систем мониторинга и пересылает их в Telegram через Bot API. Настройка занимает 20-30 минут.
Шаги: создать Telegram-бота через @BotFather, получить токен и chat_id, развернуть Flask-приложение, запустить через Gunicorn + systemd, настроить fail2ban для защиты эндпоинта.
Подключается к: Alertmanager (Prometheus), Zabbix, Grafana, CrowdSec, The Dude (MikroTik). Все системы шлют POST/GET на один эндпоинт.
Зачем это нужно, если есть встроенная интеграция
Начало 2025 года. Ты поднимаешь мониторинг, настраиваешь Alertmanager, добавляешь telegram_config. Идёшь спать спокойно. В 3 ночи падает сервис. Алерт не приходит. Потому что РКН снова «частично ограничил» работу Telegram в России.
С августа 2025 года Роскомнадзор официально ввёл ограничения на звонки в Telegram и WhatsApp. До этого были замедления в феврале 2026-го — задержки в доставке сообщений достигали нескольких минут. Прямые запросы к api.telegram.org с российских серверов периодически не проходят или идут с огромными задержками.
Это прямой удар по инфраструктуре мониторинга. Alertmanager не умеет ретраить через прокси. Zabbix пишет «Couldn’t connect to server» и молчит. Grafana просто не отправляет уведомление.
Relay на Gunicorn решает три проблемы сразу:
- Централизованная точка отправки — все системы мониторинга шлют алерты в одно место, а relay уже разбирается с доставкой
- Прокси через VPS за рубежом — relay разворачивается на сервере вне зоны ограничений, и доступ к Telegram Bot API всегда работает
- Единый формат сообщений — нормализуешь JSON от Alertmanager, Zabbix, CrowdSec в читаемые Telegram-сообщения в одном месте, не в пяти разных конфигах
Ещё один сценарий: у тебя homelab или корпоративная сеть, и несколько систем мониторинга работают в изолированном сегменте без прямого доступа в интернет. Relay стоит в DMZ или на пограничном узле и принимает запросы изнутри, а наружу ходит только он.
Что ты получишь из этой статьи: рабочий Python-relay на Flask + Gunicorn, настроенный как systemd-сервис, с защитой через fail2ban, и примеры подключения для Alertmanager, Zabbix, Grafana, CrowdSec и The Dude. Всё с реальными конфигами, без абстракций.
Системные требования
| Компонент |
Минимум |
Рекомендовано |
| ОС |
Ubuntu 22.04 LTS |
Ubuntu 24.04 LTS |
| Python |
3.10 |
3.12 |
| Gunicorn |
21.x |
23.x |
| Flask |
3.0 |
3.1 |
| RAM |
256 MB |
512 MB |
| Диск |
1 GB |
5 GB (логи) |
| fail2ban |
1.0.x |
1.1.x |
На момент публикации актуальна версия Gunicorn 23.0 и Flask 3.1. Перед установкой проверь свежие релизы на PyPI.
Архитектура решения
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Alertmanager"] --> R
B["Zabbix"] --> R
C["Grafana"] --> R
D["CrowdSec"] --> R
E["The Dude"] --> R
R["Gunicorn Relay :8000"] --> F["fail2ban защита"]
F --> G["Flask /alert endpoint"]
G --> H["Telegram Bot API"]
H --> I["Ваш чат / группа"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style C fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style D fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style E fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style R fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style F fill:#f8fafc,stroke:#ef4444,stroke-width:2px,color:#dc2626
style G fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#c2410c
style H fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style I fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
Шаг 1. Создаём Telegram-бота
Открой Telegram. Найди @BotFather. Отправь команду /newbot и следуй инструкциям — задай имя бота и username (должен заканчиваться на bot).
BotFather выдаст токен вида 1234567890:AAHbcdefGHIJKLMNOPQRSTUVWXYZ. Сохрани его — он понадобится в конфиге.
Теперь узнай chat_id. Добавь бота в нужный чат или группу, затем выполни запрос:
curl -s "https://api.telegram.org/bot<ВАШ_ТОКЕН>/getUpdates" | python3 -m json.tool
Найди в ответе поле chat.id. Для личного чата это положительное число, для группы — отрицательное (например, -1001234567890).
Проверь доступность Bot API
Если запрос выше зависает или возвращает ошибку — ты на российском сервере и api.telegram.org недоступен. Устанавливай relay на зарубежный VPS, а не на продакшн-сервер внутри РФ.
# Быстрая проверка доступности Bot API
curl -m 5 -s "https://api.telegram.org/bot<ТОКЕН>/getMe" && echo "OK" || echo "BLOCKED"
Шаг 2. Готовим окружение
Создаём отдельного пользователя для relay. Это обязательно — не запускай сервисы от root.
useradd -m -s /bin/bash tgrelay
mkdir -p /opt/tgrelay
chown tgrelay:tgrelay /opt/tgrelay
Ставим зависимости и создаём виртуальное окружение:
apt update && apt install -y python3.12 python3.12-venv python3-pip fail2ban
su - tgrelay
cd /opt/tgrelay
python3.12 -m venv venv
source venv/bin/activate
pip install flask gunicorn requests
Шаг 3. Пишем relay-приложение
Создай файл /opt/tgrelay/app.py. Это сердце всей конструкции — Flask-приложение принимает запросы, форматирует сообщения и отправляет их в Telegram.
import os
import logging
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# --- Конфигурация ---
TG_TOKEN = os.environ.get("TG_TOKEN", "")
TG_CHAT_ID = os.environ.get("TG_CHAT_ID", "")
SECRET_TOKEN = os.environ.get("SECRET_TOKEN", "changeme")
# --- Логирование ---
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("/opt/tgrelay/relay.log"),
logging.StreamHandler()
]
)
log = logging.getLogger(__name__)
def send_telegram(text: str) -> bool:
url = f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage"
payload = {
"chat_id": TG_CHAT_ID,
"text": text,
"parse_mode": "HTML"
}
try:
r = requests.post(url, json=payload, timeout=10)
r.raise_for_status()
return True
except Exception as e:
log.error(f"Telegram send error: {e}")
return False
def format_alertmanager(data: dict) -> str:
lines = []
for alert in data.get("alerts", []):
status = alert.get("status", "unknown").upper()
name = alert.get("labels", {}).get("alertname", "Unknown")
severity = alert.get("labels", {}).get("severity", "")
instance = alert.get("labels", {}).get("instance", "")
summary = alert.get("annotations", {}).get("summary", "")
icon = "🔴" if status == "FIRING" else "✅"
lines.append(
f"{icon} [{status}] {name}\n"
f"Severity: {severity}\n"
f"Instance: {instance}\n"
f"Summary: {summary}"
)
return "\n\n".join(lines) if lines else "Empty alert payload"
@app.route("/alert", methods=["POST"])
def alert():
token = request.headers.get("X-Token", "")
if token != SECRET_TOKEN:
log.warning(f"Unauthorized request from {request.remote_addr}")
return jsonify({"error": "unauthorized"}), 403
data = request.get_json(silent=True) or {}
source = request.args.get("source", "generic")
if source == "alertmanager":
text = format_alertmanager(data)
elif source == "zabbix":
subject = data.get("subject", "Zabbix Alert")
message = data.get("message", "")
text = f"Zabbix: {subject}\n{message}"
elif source == "crowdsec":
ip = data.get("ip", "unknown")
reason = data.get("reason", "")
text = f"CrowdSec ban: {ip}\nReason: {reason}" else: # Универсальный fallback: передаём любой текст text = data.get("text", str(data)) if not text: return jsonify({"error": "empty message"}), 400 ok = send_telegram(text) if ok: log.info(f"Alert sent from {request.remote_addr}, source={source}") return jsonify({"status": "sent"}), 200 else: return jsonify({"error": "telegram delivery failed"}), 502 @app.route("/health", methods=["GET"]) def health(): return jsonify({"status": "ok"}), 200 if __name__ == "__main__": app.run(host="127.0.0.1", port=8000)
Создай файл конфигурации /opt/tgrelay/config.env:
TG_TOKEN=1234567890:AAHbcdefGHIJKLMNOPQRSTUVWXYZ
TG_CHAT_ID=-1001234567890
SECRET_TOKEN=supersecrettoken123
chmod 600 /opt/tgrelay/config.env
chown tgrelay:tgrelay /opt/tgrelay/config.env
Шаг 4. Настраиваем Gunicorn
Создай файл конфигурации Gunicorn /opt/tgrelay/gunicorn.conf.py:
bind = "127.0.0.1:8000"
workers = 2
worker_class = "sync"
timeout = 30
keepalive = 5
accesslog = "/opt/tgrelay/access.log"
errorlog = "/opt/tgrelay/error.log"
loglevel = "info"
proc_name = "tgrelay"
pidfile = "/opt/tgrelay/gunicorn.pid"
user = "tgrelay"
group = "tgrelay"
Два воркера достаточно — relay не CPU-hungry, основная задача это I/O ожидание ответа от Telegram. Если у тебя больше 10 источников алертов и высокая частота — подними до 4.
Шаг 5. Systemd-сервис
Создай файл /etc/systemd/system/tgrelay.service:
[Unit]
Description=Telegram Alert Relay (Gunicorn)
After=network.target
Wants=network-online.target
[Service]
Type=notify
User=tgrelay
Group=tgrelay
WorkingDirectory=/opt/tgrelay
EnvironmentFile=/opt/tgrelay/config.env
ExecStart=/opt/tgrelay/venv/bin/gunicorn \
--config /opt/tgrelay/gunicorn.conf.py \
app:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardError=journal
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable tgrelay
systemctl start tgrelay
systemctl status tgrelay
Проверь что relay слушает:
ss -tlnp | grep 8000
curl -s http://127.0.0.1:8000/health
Должен вернуть {"status": "ok"}. Если видишь это — сервис живой, двигаемся дальше.
Шаг 6. Nginx как reverse proxy (опционально, но правильно)
Не выставляй Gunicorn напрямую в интернет. Поставь перед ним Nginx — он отдаёт rate limiting, TLS-терминацию и скрывает детали реализации.
server {
listen 443 ssl;
server_name relay.example.com;
ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem;
# Rate limiting для защиты от спама
limit_req_zone $binary_remote_addr zone=relay:10m rate=10r/m;
location /alert {
limit_req zone=relay burst=5 nodelay;
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 30s;
}
location /health {
proxy_pass http://127.0.0.1:8000;
}
}
nginx -t && systemctl reload nginx
Шаг 7. Защита через fail2ban
Relay открыт к интернету — значит, его будут сканировать. Fail2ban блокирует адреса, которые долбятся с неверным токеном или слишком часто. Настройка минимальная, но работает.
Создай фильтр /etc/fail2ban/filter.d/tgrelay.conf:
[Definition]
failregex = .*Unauthorized request from .*
.*"(GET|POST|PUT|DELETE) /alert.* (401|403).*
ignoreregex =
Создай jail /etc/fail2ban/jail.d/tgrelay.local:
[tgrelay]
enabled = true
port = http,https,8000
filter = tgrelay
logpath = /opt/tgrelay/relay.log
/var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-multiport[name=tgrelay, port="80,443,8000", protocol=tcp]
%(action_mwl)s
Теперь добавь отправку уведомления в Telegram при каждом бане. Создай action-файл /etc/fail2ban/action.d/telegram-notify.conf:
[Definition]
actionban = curl -s -X POST \
"https://api.telegram.org/bot%(tg_token)s/sendMessage" \
-d "chat_id=%(tg_chat_id)s" \
-d "text=🚫 fail2ban ban: %0AJail: %(name)s%0ARetries: %(failures)s"
actionunban = curl -s -X POST \
"https://api.telegram.org/bot%(tg_token)s/sendMessage" \
-d "chat_id=%(tg_chat_id)s" \
-d "text=✅ fail2ban unban: %0AJail: %(name)s"
[Init]
tg_token = 1234567890:AAHbcdefGHIJKLMNOPQRSTUVWXYZ
tg_chat_id = -1001234567890
Добавь этот action к jail (в тот же tgrelay.local, секция action):
action = iptables-multiport[name=tgrelay, port="80,443,8000", protocol=tcp]
telegram-notify[name=%(__name__)s]
Внимание: fail2ban и российские блокировки
Если relay стоит на российском сервере, curl в action_ban тоже может не достучаться до api.telegram.org. Используй для action только relay на зарубежном VPS или MTProto-прокси. Лучший вариант — отправлять fail2ban алерты через сам relay с localhost.
# Перезапускаем fail2ban
systemctl restart fail2ban
# Проверяем статус
fail2ban-client status tgrelay
# Тестируем бан вручную
fail2ban-client set tgrelay banip 1.2.3.4
fail2ban-client status tgrelay
fail2ban-client set tgrelay unbanip 1.2.3.4
Таблица портов
| Порт |
Протокол |
Назначение |
Доступен снаружи? |
| 8000 |
TCP |
Gunicorn (внутренний) |
Нет (только 127.0.0.1) |
| 443 |
TCP |
Nginx HTTPS -> relay |
Да (разрешённые IP) |
| 80 |
TCP |
HTTP -> редирект на HTTPS |
Да |
| 9001 |
TCP |
Gunicorn stats (опционально) |
Нет |
Подключаем Alertmanager (Prometheus)
С версии Alertmanager 0.24 есть встроенная интеграция с Telegram через telegram_configs. Но она не работает через прокси и не форматирует сообщения гибко. Relay решает обе проблемы.
В alertmanager.yml добавь receiver:
receivers:
- name: "telegram-relay"
webhook_configs:
- url: "https://relay.example.com/alert?source=alertmanager"
send_resolved: true
http_config:
headers:
X-Token: "supersecrettoken123"
route:
receiver: "telegram-relay"
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
# Проверяем конфиг Alertmanager
amtool check-config /etc/alertmanager/alertmanager.yml
# Перезапускаем
systemctl restart alertmanager
# Тестовый алерт
curl -XPOST http://localhost:9093/api/v1/alerts \
-H "Content-Type: application/json" \
-d '[{"labels":{"alertname":"TestAlert","severity":"warning"},"annotations":{"summary":"Test from curl"}}]'
Подключаем Zabbix
Начиная с Zabbix 5.0 есть встроенный webhook для Telegram — он работает через скрипт на JavaScript прямо в Zabbix Server. Из России не достучится. Используем curl-скрипт через наш relay.
Создай скрипт /usr/lib/zabbix/alertscripts/tgrelay.sh:
#!/bin/bash
# Zabbix -> Telegram Relay
# $1 = to (игнорируем, chat_id в relay)
# $2 = subject
# $3 = message
RELAY_URL="https://relay.example.com/alert?source=zabbix"
SECRET="supersecrettoken123"
PAYLOAD=$(cat <
chmod +x /usr/lib/zabbix/alertscripts/tgrelay.sh
chown zabbix:zabbix /usr/lib/zabbix/alertscripts/tgrelay.sh
В веб-интерфейсе Zabbix: Alerts -> Media Types -> Create media type. Тип: Script. Имя скрипта: tgrelay.sh. Параметры: {ALERT.SENDTO}, {ALERT.SUBJECT}, {ALERT.MESSAGE}.
Назначь media type пользователю в Users -> Media -> Add. В поле «Send to» можно поставить любое значение — relay его игнорирует.
Подключаем Grafana
В Grafana Alerting есть нативный Telegram contact point. Он снова идёт напрямую к api.telegram.org. Обходим это через webhook на наш relay.
В Grafana: Alerting -> Contact points -> Add contact point. Выбираем тип Webhook. URL:
https://relay.example.com/alert?source=grafana
HTTP Headers добавь: X-Token: supersecrettoken123.
Grafana шлёт POST с JSON в своём формате. Добавь обработчик в app.py в функцию alert():
elif source == "grafana":
state = data.get("state", "unknown")
title = data.get("title", "Grafana Alert")
message = data.get("message", "")
rule_url = data.get("ruleUrl", "")
icon = "🔴" if state == "alerting" else "✅"
text = (
f"{icon} Grafana [{state.upper()}]\n"
f"{title}\n"
f"{message}\n"
f'Открыть в Grafana'
)
# После изменения app.py - перезапускаем relay
systemctl restart tgrelay
# Тест из Grafana: кнопка "Test" в contact point
Подключаем CrowdSec
CrowdSec использует HTTP notification plugin для отправки алертов. Именно через него подключается relay.
Создай /etc/crowdsec/notifications/http_tgrelay.yaml:
type: http
name: http_tgrelay
log_level: info
group_wait: 10s
group_threshold: 5
max_retry: 3
url: https://relay.example.com/alert?source=crowdsec
method: POST
headers:
Content-Type: application/json
X-Token: supersecrettoken123
format: |
{
"ip": "{{range . }}{{.Source.Value}}{{end}}",
"reason": "{{range . }}{{.Scenario}}{{end}}",
"decisions": {{len .}}
}
В /etc/crowdsec/profiles.yaml добавь notification в секцию decisions:
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
notifications:
- http_tgrelay
on_success: break
systemctl restart crowdsec
cscli notifications test http_tgrelay
Подключаем The Dude (MikroTik)
The Dude работает на RouterOS и не умеет делать HTTP POST. Зато умеет HTTP GET через /tool fetch. Relay поддерживает GET-запросы с текстом в параметре.
Добавь в app.py новый endpoint для GET:
@app.route("/dude", methods=["GET"])
def dude():
token = request.args.get("token", "")
if token != SECRET_TOKEN:
log.warning(f"Dude: unauthorized from {request.remote_addr}")
return "403 Forbidden", 403
text = request.args.get("text", "")
device = request.args.get("device", "unknown")
event = request.args.get("event", "")
if not text and device:
text = f"The Dude: {device}\n{event}"
if text:
send_telegram(text)
return "OK", 200
return "400 Bad Request", 400
systemctl restart tgrelay
В The Dude создай новый Notification с типом «Execute on server». Используй RouterOS-скрипт:
/tool fetch url="https://relay.example.com/dude?token=supersecrettoken123\
&device=[DeviceName]&event=[EventType]&text=[DeviceName] - [EventType]" \
keep-result=no
The Dude и HTTPS
RouterOS 6.x иногда не принимает Let’s Encrypt сертификаты. Если /tool fetch падает с SSL error — добавь сертификат в Trust List через /certificate import или используй HTTP (порт 80) с redirect на HTTPS только для MikroTik-сегмента.
Проверка работы
Проверяем relay комплексно — статус сервиса, логи, тестовый запрос:
# Статус сервиса
systemctl status tgrelay
# Логи в реальном времени
journalctl -fu tgrelay
# Health check
curl -s http://127.0.0.1:8000/health
# Тестовый алерт от Alertmanager
curl -s -X POST "http://127.0.0.1:8000/alert?source=alertmanager" \
-H "Content-Type: application/json" \
-H "X-Token: supersecrettoken123" \
-d '{"alerts":[{"status":"firing","labels":{"alertname":"TestAlert","severity":"critical","instance":"server1:9100"},"annotations":{"summary":"Test message from curl"}}]}'
# Тест Zabbix-формата
curl -s -X POST "http://127.0.0.1:8000/alert?source=zabbix" \
-H "Content-Type: application/json" \
-H "X-Token: supersecrettoken123" \
-d '{"subject":"PROBLEM: High CPU on db01","message":"CPU load is 95% for 5 minutes"}'
Смотри логи relay:
tail -f /opt/tgrelay/relay.log
tail -f /opt/tgrelay/access.log
Мониторинг самого relay
Relay — это SPOF. Если он упадёт — ты ничего не узнаешь, потому что уведомления будут идти через него же. Нужен независимый watchdog.
Самый простой вариант — cron-скрипт с прямым вызовом Bot API (для зарубежного сервера это работает напрямую):
cat > /opt/tgrelay/watchdog.sh << 'EOF'
#!/bin/bash
HEALTH=$(curl -s -m 5 http://127.0.0.1:8000/health)
if echo "$HEALTH" | grep -q '"ok"'; then
exit 0
fi
# relay не ответил - шлём напрямую через Bot API
curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT_ID}" \
-d "text=CRITICAL: tgrelay is DOWN on $(hostname)"
EOF
chmod +x /opt/tgrelay/watchdog.sh
# Добавляем в cron от root
crontab -e
# Добавить строку:
# */5 * * * * TG_TOKEN="ВАШ_ТОКЕН" TG_CHAT_ID="-1001234567890" /opt/tgrelay/watchdog.sh
Резервное копирование
Бэкапить нужно конфиги и код, не логи. Логи ротируй через logrotate.
# Создай /etc/logrotate.d/tgrelay
cat > /etc/logrotate.d/tgrelay << 'EOF'
/opt/tgrelay/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
postrotate
systemctl kill -s USR1 tgrelay || true
endscript
}
EOF
Бэкап конфигов (запускать перед обновлением):
tar czf /root/tgrelay-backup-$(date +%Y%m%d).tar.gz \
/opt/tgrelay/app.py \
/opt/tgrelay/gunicorn.conf.py \
/opt/tgrelay/config.env \
/etc/systemd/system/tgrelay.service \
/etc/fail2ban/jail.d/tgrelay.local \
/etc/fail2ban/filter.d/tgrelay.conf \
/etc/nginx/sites-available/tgrelay
Обновление relay
Обновление проходит по схеме: бэкап — обновление зависимостей — тест — рестарт.
# Создаём бэкап перед обновлением
tar czf /root/tgrelay-before-update.tar.gz /opt/tgrelay/
# Обновляем зависимости
su - tgrelay -c "
source /opt/tgrelay/venv/bin/activate
pip install --upgrade flask gunicorn requests
"
# Проверяем синтаксис app.py
python3 -c "import py_compile; py_compile.compile('/opt/tgrelay/app.py')" && echo "OK"
# Graceful reload без даунтайма
systemctl reload tgrelay
# Если reload не поддерживается - restart
systemctl restart tgrelay
# Проверяем что всё встало
curl -s http://127.0.0.1:8000/health
Диагностика: типичные ошибки
| Симптом |
Причина |
Решение |
403 Forbidden на /alert |
Неверный X-Token в заголовке |
Проверить SECRET_TOKEN в config.env и в источнике алертов |
| Telegram delivery failed |
Bot API недоступен (блокировка РКН) |
Relay на зарубежном VPS, проверить curl к api.telegram.org |
| 502 Bad Gateway от Nginx |
Gunicorn не запущен |
systemctl status tgrelay, проверить ss -tlnp | grep 8000 |
| Сообщения приходят пустые |
JSON не парсится (неверный Content-Type) |
Добавить -H «Content-Type: application/json» в curl-запросы |
| fail2ban не банит |
Неверный regex в фильтре или путь к логу |
fail2ban-regex /opt/tgrelay/relay.log /etc/fail2ban/filter.d/tgrelay.conf |
| The Dude SSL error |
RouterOS не принимает сертификат |
Импортировать CA в /certificate или использовать HTTP на отдельном порту |
| Zabbix: script не выполняется |
Нет прав execute у скрипта |
chmod +x /usr/lib/zabbix/alertscripts/tgrelay.sh |
| CrowdSec не шлёт уведомления |
name в YAML не совпадает с profiles.yaml |
Имя в http_tgrelay.yaml должно совпадать с notifications: — http_tgrelay |
# Отладка fail2ban regex
fail2ban-regex /opt/tgrelay/relay.log /etc/fail2ban/filter.d/tgrelay.conf
# Отладка CrowdSec нотификации
cscli notifications test http_tgrelay
journalctl -u crowdsec -n 50
# Проверка что app.py читает переменные окружения
systemctl show tgrelay --property=Environment
Альтернативы: когда relay не нужен
Relay — это дополнительный компонент инфраструктуры. Нужен он не всегда.
Прямая интеграция Alertmanager через telegram_configs работает если сервер находится вне России или ты используешь MTProto-прокси. Grafana с версии 9.0 нативно поддерживает Telegram contact point — настраивается за 2 минуты в UI. CrowdSec официально документирует прямую отправку в Telegram через http plugin без промежуточного relay.
Relay нужен когда: несколько систем мониторинга, сервер в России или за NAT, нужен единый формат сообщений, нужен audit log всех отправленных алертов, нужно добавить логику маршрутизации (critical -> дежурная группа, warning -> общий чат).
Профилактика
Проверяй работу relay раз в неделю — не вручную, а скриптом. Положи в cron тест с настоящим алертом и убедись, что сообщение пришло.
# Еженедельный тест (добавить в cron)
# 0 9 * * 1 /opt/tgrelay/test_weekly.sh
cat > /opt/tgrelay/test_weekly.sh << 'EOF' #!/bin/bash RESULT=$(curl -s -o /dev/null -w "%{http_code}" \ -X POST "http://127.0.0.1:8000/alert?source=zabbix" \ -H "Content-Type: application/json" \ -H "X-Token: supersecrettoken123" \ -d '{"subject":"Weekly relay test","message":"OK - relay is working"}') if [ "$RESULT" = "200" ]; then echo "$(date): weekly test OK" else echo "$(date): weekly test FAILED, HTTP $RESULT" >&2
fi
EOF
chmod +x /opt/tgrelay/test_weekly.sh
FAQ
Почему алерты через Telegram не приходят после настройки?
Первая причина — api.telegram.org недоступен с сервера. Проверь: curl -m 5 https://api.telegram.org. Если timeout — relay нужно ставить на зарубежный VPS или настраивать MTProto-прокси. Вторая причина — неверный токен в config.env. Третья — relay не запущен: systemctl status tgrelay.
Как проверить что relay работает правильно?
Выполни health check: curl -s http://127.0.0.1:8000/health — должен вернуть {"status": "ok"}. Потом отправь тестовый POST с корректным токеном и проверь что сообщение появилось в Telegram. Смотри лог: tail -f /opt/tgrelay/relay.log.
Что делать если fail2ban заблокировал легитимный источник алертов?
Разбань IP командой: fail2ban-client set tgrelay unbanip 1.2.3.4. Потом добавь этот IP в ignoreip в jail.local: ignoreip = 127.0.0.1/8 192.168.0.0/16 1.2.3.4. Перезапусти fail2ban. Также проверь правильность SECRET_TOKEN на стороне источника.
Чем relay на Gunicorn отличается от встроенной интеграции Alertmanager?
Встроенная интеграция работает только с одной системой мониторинга и идёт напрямую к api.telegram.org. Relay принимает запросы от любых источников, нормализует форматы, работает как прокси через безопасный узел, ведёт audit log. Минус — дополнительный компонент который нужно поддерживать.
Можно ли отправлять алерты в несколько Telegram-чатов?
Да. Добавь в app.py словарь CHAT_ROUTING с маппингом source -> chat_id. Например, critical алерты от Alertmanager идут в дежурный чат, Zabbix — в общий чат команды. Передавай нужный чат через query parameter ?chat=oncall.
Что в итоге
Ты развернул relay на Gunicorn с Flask, который принимает алерты от пяти систем мониторинга и отправляет их в Telegram. Fail2ban блокирует попытки подбора токена и отправляет уведомление о каждом бане. Systemd держит сервис живым после перезагрузки. Nginx стоит спереди и ограничивает частоту запросов.
Проблема с блокировками РКН решается установкой relay на зарубежный VPS — все внутренние системы мониторинга шлют алерты на внутренний endpoint, а наружу ходит только relay через стабильный канал.
Если что-то не заработало — пиши в комментарии, разберёмся. Конкретный симптом, версии компонентов и вывод journalctl -fu tgrelay ускорят диагностику в разы.