# Централізований лог-сервер 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` у скрипт всередині контейнера | ## Дашборд Дашборд "Network & RADIUS Monitoring" створюється автоматично (Dashboards → Network & RADIUS Monitoring), з п'ятьма віджетами за замовчуванням на 7-денному вікні: - **Messages Over Time by Stream** — накопичувальна стовпчикова діаграма, щоб одним поглядом бачити обсяг мережевого обладнання vs. серверів - **Vendor Breakdown** — кругова діаграма за полем `vendor`, яке проставляють pipeline rules - **Top Event Types** — таблиця з підрахунком по `event_type` - **Critical Events by Type** — те саме, але відфільтроване по `severity_tag:critical` — тобто саме те, чим переймаються три алерти вище - **Top Sources** — які пристрої/сервери генерують найбільше обсягу Побудований через Views API (`dashboards/search.json` + `dashboards/view.json`), а не через власний конструктор віджетів Graylog у браузері — цей конструктор виявився складно керованим надійно через браузерну автоматизацію (React `combobox`-віджети, які не реагують на прості keyboard/click-події без одночасного тригера внутрішнього React-стану), тоді як REST API прийняв ту саму структуру чисто з першої спроби, щойно формат був реконструйований із JSON існуючого дашборду. Якщо хочете додати віджет — або скористайтесь Graylog UI напряму (людина з мишкою не натикається на проблему автоматизації), а потім за бажанням перенесіть результат назад у ці два JSON-файли, або розширте `dashboards/search.json`/`view.json` вручну — кожен віджет потребує відповідного запису в `search_types` (у `search.json`) та `widgets` + `widget_mapping` + `positions` + `titles.widget` (у `view.json`) з однаковим 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 | Третій алерт і є тим самим "універсальним" покриттям проблем мережевого обладнання: він не залежить від знання формату повідомлень конкретного вендора — лише від стандартного рівня severity syslog, який шле будь-який притомний пристрій. Також парситься (доступне для пошуку, але без алерту — це рутинний обсяг, а не інцидент сам по собі): - 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`) Щоб підключити Discord, передайте `--discord-webhook` (або встановіть `DISCORD_WEBHOOK_URL`) при запуску `create-graylog-lxc.sh`. Під капотом використовується вбудований тип нотифікації Graylog **Slack**, спрямований на `<ваш-webhook-url>/slack` — Slack-сумісний ендпоінт Discord — тож окремий конвертер не потрібен. Шаблон повідомлення показує заголовок/опис події плюс джерело, IP відправника і повний текст кожного повідомлення, що спрацювало: ``` *${event_definition_title}* ${event_definition_description} ${if backlog}${foreach backlog message}• `${message.source}` (IP: ${message.fields.gl2_remote_ip}): ${message.message} ${end}${end} ``` `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-канал. ## Що ще НЕ реалізовано - **Парсинг 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 ```