graylog-deploy/README.uk.md
byrsapty 0da7aa5a14 Document the Forgejo Actions CI setup in both READMEs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 21:36:59 +03:00

571 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Централізований лог-сервер Graylog — скрипти розгортання
Розгортає Debian 12 LXC-контейнер на Proxmox VE зі стеком Docker Compose
(MongoDB + OpenSearch + Graylog) для збору syslog з мережевого обладнання
(Juniper, ZTE OLT, D-Link) та серверів (RADIUS, accel-ppp).
## Швидкий старт
Виконайте на хості Proxmox під користувачем `claude-deploy` (або будь-яким
іншим користувачем з такими самими обмеженими правами sudo на `pct`/`pveam`
для VMID 200-299):
```bash
./create-graylog-lxc.sh \
--ip 10.254.254.202/24 \
--gw 10.254.254.235 \
--vlan 1254 \
--external-uri http://93.171.241.5:9000/ \
--discord-webhook "https://discord.com/api/webhooks/xxx/yyy"
```
Це **єдина команда, яка потрібна**. Вона створює контейнер, встановлює Docker,
застосовує firewall (nftables: відкриті лише 9000/tcp, 514+1514/udp,
5140/udp), піднімає стек, створює Syslog inputs, імпортує pipeline rules і
в кінці виводить дані адміністратора.
Обидва скрипти ідемпотентні — повторний запуск після збою (або для
оновлення) продовжує з того місця, де зупинився, а не дублює роботу.
## Покрокове розгортання з нуля
### 0. Передумови
- SSH-доступ до хоста Proxmox під користувачем з sudo-правами на `pct`
create/set/start/stop/exec/status/list та `pveam` update/list/download
(root **не потрібен** — весь цей інструментарій побудований і
протестований саме в такому обмеженому обсязі прав).
- Storage на хості Proxmox з увімкненим content type `vztmpl`, де вже є
(або можна завантажити) template `debian-12-standard` — типова назва за
замовчуванням: `local-btrfs`. Перевірте:
```bash
sudo pveam list local-btrfs # замініть на назву вашого storage
```
Якщо помилка "storage is disabled" чи "does not exist" — знайдіть
правильний (`sudo pveam list <name>` для кандидатів) і передайте через
`--template-storage`.
- Storage для кореневої файлової системи контейнера (типово: `EX-Ceph`) з
достатнім вільним місцем під `--disk` (за замовчуванням 50GB).
- IP-адреса, шлюз і VLAN-тег (якщо є) мережі, де житиме контейнер — тобто
та сама мережа, звідки ваші RADIUS/NAS-сервери й мережеве обладнання
зможуть до нього достукатись.
- Якщо потрібен доступ до Web UI ззовні цієї мережі — вільний зовнішній
порт для DNAT на `9000/tcp` контейнера (див. примітку "доступ до Web UI
ззовні" нижче — порт 9000 під час тестування виявився вже зайнятий
сторонім ClickHouse, тож не покладайтесь, що якийсь конкретний порт
вільний).
- (Опційно) Discord webhook URL, якщо хочете отримувати критичні алерти в
канал.
### 1. Перенести скрипти на хост Proxmox
З вашої робочої машини:
```bash
tar czf - -C /шлях/до/graylog-deploy . | ssh claude-deploy@<proxmox-host> \
"mkdir -p ~/graylog-deploy && tar xzf - -C ~/graylog-deploy && chmod +x ~/graylog-deploy/*.sh"
```
(Або `git clone`/`scp` теки, якщо тримаєте її в репозиторії — підійде
будь-який спосіб, що перенесе всю теку, включно з `rules/`, `pipelines/`,
`streams/`, `alerts/`.)
### 2. Визначитись із параметрами
Заздалегідь виберіть:
| Потрібно | Приклад | Навіщо |
|---|---|---|
| Вільний VMID у 200-299 | `210` | або пропустіть `--vmid`, щоб скрипт сам обрав перший вільний |
| IP контейнера + CIDR у мережі керування | `10.254.254.220/24` | має бути досяжним з кожного пристрою, що шле syslog |
| Шлюз у цій мережі | `10.254.254.235` | |
| VLAN-тег (якщо міст транкований) | `1254` | пропустіть `--vlan`, якщо без тегу |
| Публічна URL для Web UI | `http://<публічний-ip>:<порт>/` | використовується і в конфігу Graylog, і в посиланнях Discord-алертів |
| Discord webhook (опційно) | `https://discord.com/api/webhooks/.../...` | не вказуйте, щоб пропустити Discord повністю |
**Спершу перевірте, що IP реально вільний** — дубльований IP у цій мережі
непомітно спричиняє ARP flapping і плутану, важко діагностовану
мережеву поведінку (саме так сталось під час першого розгортання: `.201`
виявився вже зайнятим, і трафік випадково потрапляв не на той хост).
Швидка перевірка без жодних змін:
```bash
ssh claude-deploy@<proxmox-host> "sudo /usr/sbin/pct exec <будь-який-запущений-vmid> -- ping -c2 -W1 <кандидат-ip>"
```
Якщо є відповіді — ця IP зайнята, оберіть іншу.
### 3. Запустити `create-graylog-lxc.sh`
```bash
ssh claude-deploy@<proxmox-host>
cd ~/graylog-deploy
./create-graylog-lxc.sh \
--vmid 210 \
--ip 10.254.254.220/24 \
--gw 10.254.254.235 \
--vlan 1254 \
--external-uri http://<публічний-ip>:<порт>/ \
--discord-webhook "https://discord.com/api/webhooks/xxx/yyy"
```
Що відбувається по черзі: перевірка/завантаження template → `pct create`
→ старт контейнера → очікування мережі → копіювання всієї цієї теки в
контейнер за шляхом `/opt/graylog-deploy/` → запуск `install-graylog.sh`
всередині нього, який встановлює Docker, застосовує firewall, піднімає
MongoDB/OpenSearch/Graylog, чекає, поки стек стане healthy, створює Syslog
inputs, імпортує pipeline rules/pipelines/streams, і (якщо переданий
webhook) створює Discord-notification та три alert definitions.
**Якщо скрипт зупиняється з помилкою AppArmor** (`open sysctl
net.ipv4.ip_unprivileged_port_start: permission denied`) — це очікувано на
деяких Proxmox-налаштуваннях і потребує одного ручного кроку від root на
хості — див. "Docker-in-unprivileged-LXC AppArmor block" нижче. Зробіть
це, а потім просто запустіть ту саму команду ще раз; усе вже зроблене
автоматично пропускається.
Загальний час чистого прогону: кілька хвилин, здебільшого очікування
завантаження образів і поки Graylog повідомить healthy.
### 4. Прочитати креденшели адміністратора
```bash
ssh claude-deploy@<proxmox-host> "sudo pct exec 210 -- cat /opt/graylog/.admin_credentials_ONE_TIME"
```
Скопіюйте пароль у менеджер паролів, потім видаліть файл:
```bash
ssh claude-deploy@<proxmox-host> "sudo pct exec 210 -- rm /opt/graylog/.admin_credentials_ONE_TIME"
```
Зайдіть на `http://<публічний-ip>:<порт>/` під користувачем `admin` з цим
паролем.
### 5. Доступ до Web UI ззовні мережі керування (якщо потрібно)
Якщо IP контейнера не досяжна напряму звідти, звідки ви заходите в
браузер, додайте DNAT-правило на вашому edge-роутері/файрволі для
`9000/tcp`:
```
-A PREROUTING -d <публічний-ip>/32 -p tcp -m tcp --dport <порт> -j DNAT --to-destination <ip-контейнера>:9000
```
Оберіть порт, який справді вільний — під час першого розгортання порт
9000 виявився вже зайнятий ClickHouse на тій самій публічній IP, і
відповідь (`Port 9000 is for clickhouse-client program...`) якийсь час
виглядала як проблема Graylog, поки причину не знайшли. Якщо на обраному
порту приходить підозріла/неочікувана відповідь — спершу підозрюйте
вже існуючий сервіс на цьому порту, а не Graylog.
### 6. Направити реальне обладнання на сервер
| Джерело | Порт | Примітка |
|---|---|---|
| Мережеве обладнання (свічі, OLT, роутери) | **514/udp** | стандартний syslog-порт — майже жодне обладнання не дозволяє обрати інший (`logging <host>` на типовому свічі завжди йде на 514) |
| Мережеве обладнання, яке *може* задати кастомний порт | 1514/udp | залишений як другорядний input, той самий стрім, що й 514 |
| Сервери (RADIUS, accel-ppp, conntrack/kernel-повідомлення) | **5140/udp** | |
На кожному пристрої це зазвичай один рядок конфігу (наприклад, на
Cisco-подібному CLI: `logging <ip-контейнера>`). Жодних налаштувань з боку
Graylog під кожен пристрій не потрібно — inputs і стріми вже слухають на
всіх трьох портах.
### 7. Перевірити, що дані реально надходять
1. Web UI → **System → Inputs**: кожен input показує живий "Traffic Last
Minute". Якщо залишається 0 після того, як пристрій мав щось надіслати
— проблема в мережі/firewall, а не в Graylog — підтвердіть через
`tcpdump` всередині контейнера, перш ніж чіпати налаштування Graylog:
```bash
sudo pct exec 210 -- tcpdump -i eth0 -n udp port 514
```
2. Web UI → **Search**, розширте часовий діапазон (вгорі зліва), клікніть
на будь-яке повідомлення, щоб розгорнути. Якщо поля `vendor` /
`event_type` заповнені — спрацювало pipeline rule. Якщо повідомлення
прийшло, але ці поля порожні — воно дійшло до Graylog нормально, але
жодне правило поки не розпізнає його формат — це ознака, що новому
пристрою/вендору потрібне нове правило (див. "Що ще НЕ реалізовано"
нижче щодо процесу).
3. Web UI → **Streams**: колонка "Throughput" показує живий msg/s по
кожному стріму.
### 8. (Опційно) Викликати реальний тестовий алерт
Див. "Перевірка тестового алерту" нижче — надішліть один із відомих
критичних рядків логу через `logger` зсередини контейнера і подивіться,
як він приходить у Discord протягом приблизно хвилини.
## Параметри
Кожен флаг має відповідник у вигляді змінної середовища (див. початок
`create-graylog-lxc.sh`), тож можна також робити `export MEMORY_MB=16384`
тощо замість передачі флагів.
| Флаг | За замовчуванням | Примітка |
|---|---|---|
| `--ip` | *(обов'язково)* | Статична IP + CIDR для контейнера |
| `--gw` | *(обов'язково)* | IP шлюзу |
| `--external-uri` | *(обов'язково)* | Публічна URL-адреса Web UI Graylog (використовується в `GRAYLOG_HTTP_EXTERNAL_URI`) |
| `--vmid` | перший вільний 200-299 | |
| `--hostname` | `graylog` | |
| `--cores` | `4` | |
| `--memory` | `8192` (МБ) | |
| `--swap` | `512` (МБ) | |
| `--disk` | `50` (ГБ) | розмір rootfs на `--rootfs-storage` |
| `--bridge` | `vmbr0` | |
| `--vlan` | *(немає = без тегу)* | |
| `--nameserver` | `1.1.1.1` | |
| `--searchdomain` | *(немає)* | |
| `--timezone` | `Europe/Kyiv` | |
| `--template-storage` | `local-btrfs` | має мати увімкнений content type `vztmpl` |
| `--rootfs-storage` | `EX-Ceph` | сховище для диска контейнера |
| `--discord-webhook` | *(немає)* | передається як `DISCORD_WEBHOOK_URL` у скрипт всередині контейнера |
## Дашборди
Замість одного комбінованого дашборду автоматично створюються три
фокусовані — кожен під конкретного читача, тож відкривши Graylog, одразу
бачиш релевантне, а не одну велику сторінку зі змішаними мережею,
серверами й алертами:
- **Overview & Alerts** (Dashboards → Overview & Alerts) — стартова
сторінка. Recent Alerts (останні 24г, витягнуто прямо зі стріму "All
events", тож видно самі спрацьовані алерти, а не лише лічильники),
Message Volume by Source за останню годину (ловить флуд візуально ще до
того, як спрацюють flood-алерти), Events by Priority (24г) та Critical
Events by Type (24г).
- **Network Equipment** (Dashboards → Network Equipment) — тільки стрім
Network Equipment, 7-денне вікно: обсяг у часі по event_type, розбивка
по вендору (Juniper vs. BDCOM), таблиця event type, топ пристроїв за
обсягом.
- **Servers & Sessions** (Dashboards → Servers & Sessions) — тільки стрім
Servers, 7-денне вікно: обсяг у часі по event_type, таблиця event type,
топ серверів за обсягом (той самий зріз, під який каліброві
flood-алерти), і розбивка RADIUS accounting статусів (Start/Alive/Stop).
Кожен побудований через Views API (пари `dashboards/search_<назва>.json` +
`dashboards/view_<назва>.json`, по одній парі на дашборд), а не через
власний конструктор віджетів Graylog у браузері — цей конструктор виявився
складно керованим надійно через браузерну автоматизацію (React
`combobox`-віджети, які не реагують на прості keyboard/click-події без
одночасного тригера внутрішнього React-стану), тоді як REST API прийняв ту
саму структуру чисто, щойно формат був реконструйований із JSON існуючого
дашборду. Єдина частина, що не є простим aggregation pivot — віджет Recent
Alerts, `type: "messages"` замість `type: "aggregation"` — потребувала
власної реконструкції: поле `sort` на рівні віджета для message-list
віджету має бути `[]`, а не об'єктом з парою `field`/`order`, інакше
Graylog відхиляє його з помилкою Jackson-поліморфізму
(`missing type id property 'type'` — DTO сортування для message-віджетів
взагалі не має зареєстрованих підтипів у цій версії Graylog).
Якщо хочете додати віджет — або скористайтесь Graylog UI напряму (людина з
мишкою не натикається на проблему автоматизації), а потім за бажанням
перенесіть результат назад у ці JSON-файли, або розширте пару
`search_<назва>.json`/`view_<назва>.json` вручну — кожен віджет потребує
відповідного запису в `search_types` (у `search_<назва>.json`) та
`widgets` + `widget_mapping` + `positions` + `titles.widget` (у
`view_<назва>.json`) з однаковим ID. `step_dashboard()` в
`install-graylog.sh` сам підхоплює будь-яку пару
`search_*.json`/`view_*.json` (за збігом імені файлу), тож нова пара
просто має існувати в директорії `dashboards/` — жодних змін у скрипті не
потрібно. Плейсхолдери `__NETWORK_STREAM_ID__` та `__SERVERS_STREAM_ID__`
підставляються так само, як і в шаблонах алертів.
## Особливості середовища, які скрипт обходить
- **Блокування Docker-в-unprivileged-LXC через AppArmor**: контейнери
падають з помилкою `open sysctl net.ipv4.ip_unprivileged_port_start:
permission denied`, якщо адміністратор хоста не додасть сирий рядок
конфігурації LXC. `pct set` не підтримує цю опцію, тож автоматизувати
це в межах прав `claude-deploy` неможливо. Якщо зіткнетесь із цим,
виконайте під root на хості:
```bash
echo "lxc.apparmor.profile: unconfined" >> /etc/pve/lxc/<VMID>.conf
pct reboot <VMID>
```
а потім перезапустіть `create-graylog-lxc.sh` (ідемпотентний, продовжить
з цього місця).
- **`vm.max_map_count`**: OpenSearch вимагає >= 262144. Це
загальносистемний параметр ядра хоста, не прив'язаний до конкретного
LXC, тож встановити його зсередини контейнера теж неможливо.
`install-graylog.sh` лише перевіряє значення і завершується з
інструкціями, якщо воно замале — у цьому розгортанні воно вже було
262144 за замовчуванням, тож нічого робити не довелось.
- **TLS Docker Hub через IPv6**: у цій мережі шляхи IPv6 до
`registry-1.docker.io` періодично перехоплюються і повертають невідповідний
сертифікат (`*.docker.com`). Скрипт встановлення вимикає IPv6 всередині
контейнера, щоб форсувати вихід тільки через IPv4. Завантаження образів
також повторюється до 5 разів, бо навіть IPv4 інколи потрапляє на
проблемний вузол.
- **Версія MongoDB**: Graylog 7.1 вимагає MongoDB >= 7.0 (деяка застаріла
документація досі згадує 6.0.x — не довіряйте кешованій документації
більше, ніж тому, що фактично повідомляє запущений сервер).
- **У RFC3164 syslog немає часового поясу — Graylog за замовчуванням
вважає це UTC**: більшість мережевого обладнання і accel-ppp шлють
класичний RFC3164 syslog (`Jul 22 09:15:13`, без року, без зсуву). Без
явного налаштування `timezone` на input Graylog зберігає цей "голий"
час так, ніби він уже в UTC — тож пристрій, що пише за київським часом
(UTC+3), в Graylog виглядає так, ніби події відбулись на 3 години в
майбутньому. Перевірено наживо: тестовий пакет із `Jul 22 09:15:13`
зберігався як `09:15:13Z` (неправильно), поки в конфігурації кожного
Syslog UDP input не виставили `timezone` на часовий пояс самого
контейнера (`Europe/Kyiv`); після цього він коректно зберігається як
`06:15:13Z` (`09:15:13` за Києвом = `06:15:13` UTC). `step_inputs()` у
`install-graylog.sh` виставляє це автоматично з `/etc/timezone` для
кожного input, який створює, і сам виправляє це на вже існуючих input,
створених до цього фіксу.
- **Часовий пояс відображення для вбудованого користувача `admin` — це
окреме налаштування, не те саме, що фікс вище**: навіть після фіксу
RFC3164 веб-інтерфейс міг далі показувати час в UTC для вбудованого
read-only акаунта `admin`. Часовий пояс цього акаунта **не можна**
змінити через `PUT /api/users/admin` (перевірено наживо — падає з
`"state should be: hexString has 24 characters"`, оскільки цей ендпоінт
не призначений для спеціального вбудованого акаунта). Він
налаштовується лише на рівні сервера — опція `root_timezone`, тобто
змінна оточення Docker `GRAYLOG_ROOT_TIMEZONE`. `step_compose_files()`
виставляє її з `/etc/timezone` для нових інсталяцій і сам додає її в
уже існуючий `.env`, створений до цього фіксу; `docker-compose.yml`
прокидає її в сервіс `graylog`. Повторний запуск `docker compose up -d`
підхоплює зміну і сам перестворює контейнер.
- **Retention індексів звужений навмисно**: фабричний дефолт Graylog 7.1
зберігає 30-40 днів даних у до 20 індексах — прийнятно загалом, але
ризиковано на малому диску (це розгортання: 50GB) у поєднанні з
неперевіреним реальним обсягом логів (деякі джерела, наприклад accel-ppp
на debug-рівні, можуть бути дуже "балакучими"). Звужено до вікна 14-21
день / 15 індексів для більшого запасу безпеки. Перегляньте це рішення,
коли назбирається кілька тижнів реального продакшн-обсягу.
- **Мережеве обладнання шле syslog на порт 514, а не на кастомний порт**:
більшість комутаторів/OLT (перевірено наживо на BDCOM S5612) підтримують
лише `logging <host>`, що завжди використовує стандартний UDP/514, без
можливості вказати інший порт. Тому створено **два** input для
"мережевого обладнання" — 514 (те, що реально використовують пристрої) і
1514 (залишений для обладнання, яке *може* слати на кастомний порт), і
обидва ведуть у той самий стрім "Network Equipment" (`matching_type:
OR`). Якщо додасте нове обладнання, і воно не з'являється — перевірте
`tcpdump -i eth0 udp port 514` всередині контейнера, перш ніж
припускати, що проблема в pipeline rules.
- **nftables ніколи не повинен робити `flush ruleset`**: рання версія
цього кроку firewall використовувала `flush ruleset`, що також знищує
власні таблиці Docker у iptables-nft (`DOCKER`, `DOCKER-USER` тощо),
ламаючи публікацію портів контейнера при наступному `docker compose up`
(перевірено наживо — довелось відновлювати через повний `docker compose
down && up`). `nftables.conf` тут робить лише `add table inet filter` +
`flush table inet filter`, що торкається лише цієї однієї таблиці і
безпечне незалежно від порядку запуску відносно `docker.service`.
## Алерти та Discord-нотифікації
Шість алертів працюють одразу з коробки (все інше навмисно мовчить —
рутинні auth-fail, поодинокі розриви сесій тощо парсяться й доступні для
пошуку, але нікого не турбують сповіщенням):
| Алерт | Спрацьовує на | Пріоритет |
|---|---|---|
| RADIUS server unreachable | `radius: server(N) not responding` або `radius: no available servers` (перевірені рядки з вихідного коду accel-ppp, `radius/req.c`) | High |
| conntrack table full (packet loss) | `nf_conntrack: table full, dropping packet` (стандартне повідомлення ядра Linux — активна втрата пакетів прямо зараз) | High |
| Unrecognized critical-severity syslog | Будь-яке повідомлення (будь-який вендор, будь-який стрім) із syslog-severity Emergency/Alert/Critical (0-2) за RFC5424/3164, яке не класифікувало жодне спеціальне правило | Medium |
| Juniper chassis hardware alarm | `CHASSISD_SNMP_TRAP`/`CHASSISD_SNMP_TRAP6` (перегрів, вентилятор, блок живлення тощо) — перевірено наживо на реальному шасі `VC.MYRONIVKA` | High |
| Аномальний обсяг повідомлень від одного сервера | Один сервер у стрімі Servers шле понад 150 000 повідомлень за 10-хвилинне вікно — див. "Алерти на обсяг (flood)" нижче | Medium |
| Аномальний обсяг syslog від мережевого обладнання | Один пристрій у стрімі Network Equipment шле понад 500 повідомлень за 5-хвилинне вікно — див. "Алерти на обсяг (flood)" нижче | Medium |
Третій алерт і є тим самим "універсальним" покриттям проблем мережевого
обладнання: він не залежить від знання формату повідомлень конкретного
вендора — лише від стандартного рівня severity syslog, який шле будь-який
притомний пристрій.
### Алерти на обсяг (flood)
Останні два алерти захищають від ситуації, коли одне джерело тихо забиває
диск у вікні retention — цикл у логах, шторм ретраїв, чи випадково
залишений debug-рівень. Вони групуються за `gl2_remote_ip`
(`aggregation-v1`, `count() > поріг`), тож кожне джерело порівнюється зі
своїм власним обсягом, а не із сумою по всьому стріму.
Пороги не вигадані — вони каліброві наживо 2026-07-22 на реальному
трафіку через pivot-пошук Views API, згрупований за `gl2_remote_ip`:
- Один активний accel-ppp/RADIUS сервер стабільно слав **~4 600-4 800
повідомлень за 10 хвилин** (~278 тис/год) за нормального навантаження.
Поріг для стріму Servers (150 000/10хв) дає приблизно 3x запасу над цим.
- Один активний мережевий пристрій стабільно слав **~30-60 повідомлень за
5 хвилин** (~360/год). Поріг для Network Equipment (500/5хв) дає
приблизно 10x запасу над цим.
Це стартові значення на основі часткового розгортання (1 сервер + 1
пристрій були активні на момент калібрування). Переглядайте обидва пороги
по мірі підключення решти запланованих ~10-15 серверів і ~10-20
комутаторів/OLT — те, що сьогодні виглядає як 3x запасу, може виявитись
затісним або занадто вільним, коли буде відомий власний baseline кожного
сервера. Перевірити поточний обсяг по джерелу можна будь-коли запитом
на кшталт:
```
gl2_remote_ip:<ip>
```
за фіксований проміжок часу на сторінці Search, або тим самим
pivot-пошуком (групування за `gl2_remote_ip`, серія `count()`) через Views
API, якщо потрібні точні числа замість погляду на графік.
Також парситься (доступне для пошуку, але без алерту — це рутинний обсяг,
а не інцидент сам по собі):
- accel-ppp: PPP authentication failed (`ppp_auth.c`)
- Окремий FreeRADIUS: `Auth: (n) Login OK: [user] (from client X port P)` /
`Auth: Login incorrect: [user] (from client X port P)` (типовий формат
`auth_log`)
## Кореляція сесій (абонентські сесії accel-ppp)
Кожен рядок логу accel-ppp для конкретної сесії абонента — RADIUS
Access-Request (спроба авторизації), Accounting-Request
(start/interim/stop), DHCP discover/offer/request/ack, ipoe session
create/start/finish/terminate — тепер позначається тим самим полем
`accelppp_interface` (ім'я інтерфейсу `vlanNNNN.NNN`, яке сам accel-ppp
використовує для кожного абонента). Це працює навіть для типів
повідомлень без жодних інших структурованих полів — завдяки fallback
правилу (`accelppp_interface_tag`), яке позначає лише ті рядки, які ще не
класифікувало жодне спеціальне правило.
Щоб побачити повний життєвий цикл сесії абонента одним запитом, шукайте:
```
accelppp_interface:"vlan1779.124"
```
відсортовано за часом (за замовчуванням). Це показує DHCP-хендшейк,
RADIUS auth/accounting обмін і кінцеве завершення сесії як один
хронологічний список — замість пошуку імені інтерфейсу вручну по сирому
тексту.
Рядки RADIUS Access-Request та Accounting-Request додатково отримують три
багатших поля кореляції, витягнуті прямо з RADIUS AVP:
- `radius_session_id` — `Acct-Session-Id` accel-ppp, стабільний для всієї
сесії
- `calling_station_id` — MAC-адреса абонента
- `radius_username` — логін абонента (`User-Name` accel-ppp, формат
`<vlan>:<qinq>` у цьому розгортанні)
Це корисно, коли відправна точка — тікет з MAC-адресою чи логіном, а не
ім'ям інтерфейсу, наприклад:
```
calling_station_id:"48:8f:5a:a4:f9:ba"
```
Перевірено наживо 2026-07-22 на реальному трафіку EX-NAS-1-1: одне
значення `accelppp_interface` коректно об'єднало DHCPv4 Ack, DHCPv4
Request, RADIUS Accounting-Response та RADIUS Accounting-Request, що
належали одній і тій самій сесії абонента.
Щоб підключити Discord, передайте `--discord-webhook` (або встановіть
`DISCORD_WEBHOOK_URL`) при запуску `create-graylog-lxc.sh`. Під капотом
використовується вбудований тип нотифікації Graylog **Slack**, спрямований
на `<ваш-webhook-url>/slack` — Slack-сумісний ендпоінт Discord — тож окремий
конвертер не потрібен. Шаблон повідомлення показує заголовок/опис події
плюс джерело, IP відправника і повний текст кожного повідомлення, що
спрацювало:
```
*${event_definition_title}*
${event_definition_description}
${event.message}
${if backlog}${foreach backlog message}• `${message.source}` (IP: ${message.fields.gl2_remote_ip}): ${message.message}
${end}${end}
```
Рядок `${event.message}` показує автоматично згенероване Graylog резюме
події — для звичайних critical-алертів це просто дублює заголовок, а для
двох flood-алертів (групованих за `gl2_remote_ip`) саме тут з'являється
конкретна IP-адреса джерела і фактичне значення `count()`, наприклад
`WARNING: ...: 93.171.243.4 - count()=278474.0`.
`gl2_remote_ip` — поле, яке Graylog проставляє автоматично для кожного
повідомлення на основі реальної адреси відправника UDP-пакета, незалежно
від того, яке ім'я хоста заявляє сам пристрій у полі syslog `source`.
### Пошук за IP-адресою
Кожне повідомлення можна знайти за реальною IP-адресою пристрою через те
саме поле `gl2_remote_ip`, у рядку пошуку на сторінці Search:
```
gl2_remote_ip:93.171.243.4
```
`source:<значення>` теж працює, але лише якщо пристрій сам надсилає свою
IP-адресу як ім'я хоста (деяке обладнання так робить, інше шле
налаштоване ім'я) — `gl2_remote_ip` надійніший, бо береться з самого
пакета, а не з даних, які надає пристрій.
### Перевірка тестового алерту
```bash
# зсередини контейнера, імітуючи кожен тригер:
docker exec -it graylog-server bash
logger -n 127.0.0.1 -P 5140 -d 'radius: server(1) not responding'
logger -n 127.0.0.1 -P 5140 -d 'kernel: nf_conntrack: table full, dropping packet'
```
Планувальник перевіряє раз на 60с, тож повідомлення в Discord може прийти
із затримкою до хвилини. Перевірте сторінку Alerts у Graylog та відповідний
Discord-канал.
## CI/CD через Forgejo Actions
Forgejo-репозиторій проєкту (`git.zotac.keenetic.link/zotac/graylog-deploy`)
має вручну-запускний деплой-workflow: `.forgejo/workflows/deploy.yml`.
Його запуск (вкладка Actions → Deploy Graylog config → Run workflow)
клонує репо наново й повторно запускає `install-graylog.sh` — той самий
ідемпотентний скрипт, описаний по всьому цьому README, просто
автоматизований замість ручного scp.
- **Тригер навмисно ручний** (тільки `workflow_dispatch`, без
`on: push`) — це репо керує продакшн-системою моніторингу, тож людина
переглядає диф і сама натискає "Run workflow", а не кожен push тихо
передеплоює систему.
- **Раннер живе прямо всередині контейнера Graylog** (VMID 200, лейбл
`self-hosted:host`, встановлений у `/usr/local/bin/forgejo-runner`,
працює як systemd-сервіс). Нових SSH-ключів чи міжхостового доступу не
знадобилося — workflow просто клонує репо в `/tmp/graylog-deploy-ci` і
запускає скрипт локально, точно як це робив би оператор вручну.
- **Без `actions/checkout`** — ця дія вимагає Node.js, якого немає (і не
повинно бути) на цьому appliance-контейнері лише заради CI. Замість неї
workflow робить звичайний `git clone --depth 1`.
- **Секрети** (`GRAYLOG_ADMIN_PASSWORD`, `DISCORD_WEBHOOK_URL`) зберігаються
як repo-level секрети Forgejo Actions, а не в жодному відстежуваному
файлі. `GRAYLOG_EXTERNAL_URI` не є секретом (це публічна адреса Web UI),
тож вказаний прямо у workflow.
## Що ще НЕ реалізовано
- **Парсинг D-Link switch** — не було зразків логів.
- **ZTE OLT ONU online/offline + оптичні аларми** — не було зразків логів
(надані були лише BDCOM OLT-подібні CLI-логи: privilege-mode/logout/
ARP-move/config-write, які *вже* парсяться).
- **Чистий RADIUS Access-Accept/Reject із реального розгортання** —
правило для окремого FreeRADIUS вище побудоване на задокументованому
типовому форматі логів FreeRADIUS, а не на зразку з реальних RADIUS-серверів
цієї мережі (не було SSH-доступу до них); формат добре встановлений у
проєкті, але варто звірити з реальним рядком логу, коли він з'явиться.
Щоб закрити ці прогалини: дайте кілька реальних сирих рядків логів по
кожному джерелу (D-Link syslog, ZTE OLT ONU up/down + оптичні аларми) — і
відповідні `rules/*.json` та alert definitions можна буде додати так само,
як були побудовані наявні.
## Структура файлів
```
create-graylog-lxc.sh # запускається на хості Proxmox
install-graylog.sh # запускається всередині контейнера (автоматично)
docker-compose.yml # опис стеку MongoDB + OpenSearch + Graylog
nftables.conf # firewall-правила, що застосовуються всередині контейнера
rules/*.json # визначення Graylog Pipeline Rule (імпортуються через API)
pipelines/*.json # визначення Graylog Pipeline, що посилаються на правила
streams/*.json # визначення Graylog Stream (маршрутизація за портом input)
```
Всередині контейнера все лежить у `/opt/graylog/` (`docker-compose.yml`,
`.env` із секретами) та `/opt/graylog-deploy/` (копія цього репозиторію,
використовується для ідемпотентних повторних запусків).
## Креденшели
`install-graylog.sh` генерує `GRAYLOG_PASSWORD_SECRET` та випадковий
пароль адміністратора під час першого запуску, одноразово записуючи
пароль адміністратора у `/opt/graylog/.admin_credentials_ONE_TIME`
всередині контейнера — прочитайте його, збережіть у менеджері паролів,
а потім видаліть файл:
```bash
pct exec <VMID> -- cat /opt/graylog/.admin_credentials_ONE_TIME
pct exec <VMID> -- rm /opt/graylog/.admin_credentials_ONE_TIME
```