# Централізований лог-сервер 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 ` для кандидатів) і передайте через `--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@ \ "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@ "sudo /usr/sbin/pct exec <будь-який-запущений-vmid> -- ping -c2 -W1 <кандидат-ip>" ``` Якщо є відповіді — ця IP зайнята, оберіть іншу. ### 3. Запустити `create-graylog-lxc.sh` ```bash ssh claude-deploy@ 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@ "sudo pct exec 210 -- cat /opt/graylog/.admin_credentials_ONE_TIME" ``` Скопіюйте пароль у менеджер паролів, потім видаліть файл: ```bash ssh claude-deploy@ "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 :9000 ``` Оберіть порт, який справді вільний — під час першого розгортання порт 9000 виявився вже зайнятий ClickHouse на тій самій публічній IP, і відповідь (`Port 9000 is for clickhouse-client program...`) якийсь час виглядала як проблема Graylog, поки причину не знайшли. Якщо на обраному порту приходить підозріла/неочікувана відповідь — спершу підозрюйте вже існуючий сервіс на цьому порту, а не Graylog. ### 6. Направити реальне обладнання на сервер | Джерело | Порт | Примітка | |---|---|---| | Мережеве обладнання (свічі, OLT, роутери) | **514/udp** | стандартний syslog-порт — майже жодне обладнання не дозволяє обрати інший (`logging ` на типовому свічі завжди йде на 514) | | Мережеве обладнання, яке *може* задати кастомний порт | 1514/udp | залишений як другорядний input, той самий стрім, що й 514 | | Сервери (RADIUS, accel-ppp, conntrack/kernel-повідомлення) | **5140/udp** | | На кожному пристрої це зазвичай один рядок конфігу (наприклад, на Cisco-подібному CLI: `logging `). Жодних налаштувань з боку 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/.conf pct reboot ``` а потім перезапустіть `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 `, що завжди використовує стандартний 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: ``` за фіксований проміжок часу на сторінці 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, формат `:` у цьому розгортанні) Це корисно, коли відправна точка — тікет з 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 -- cat /opt/graylog/.admin_credentials_ONE_TIME pct exec -- rm /opt/graylog/.admin_credentials_ONE_TIME ```