# Централізований лог-сервер 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 і в кінці виводить дані адміністратора. Обидва скрипти ідемпотентні — повторний запуск після збою (або для оновлення) продовжує з того місця, де зупинився, а не дублює роботу. Це означає виявлення змін у вмісті, а не лише факту існування: якщо відредагувати regex у правилі, список стейджів у pipeline, правила збігу в стрімі, віджети в дашборді чи поріг в алерті — повторний запуск застосує зміну на місці, а не тихо пропустить оновлення лише тому, що ресурс з такою самою назвою вже існує. Підтверджено наживо (2026-07-23) для кожного типу ресурсу, яким керує цей скрипт, включно з повним циклом створення/видалення правил стріму та віджетів дашборду. ## Варіанти запуску Чотири способи застосувати цей проєкт — від "ввести одну команду" до "натиснути кнопку в браузері". Обирайте залежно від того, що змінилося і скільки налаштування готові зробити наперед. | Спосіб | Звідки запускається | Що робить | Одноразове налаштування | Коли використовувати | |---|---|---|---|---| | **Вручну, повний** | термінал на хості Proxmox | `create-graylog-lxc.sh` → створює/ремонтує LXC-контейнер, встановлює все | немає | перший деплой, або коли хочете бачити процес наживо | | **Вручну, тільки конфіг** | термінал, `pct exec` у контейнер | повторний запуск `install-graylog.sh` напряму | немає | швидкий фікс конфігурації без git | | **CI, тільки конфіг** | веб-інтерфейс Forgejo | workflow `deploy.yml` → перезапускає `install-graylog.sh` з останнього коміту | `setup-forgejo-runner.sh` **всередині контейнера** (див. крок 9) | запушили нове правило/алерт/дашборд, контейнер уже існує | | **CI, повний** | веб-інтерфейс Forgejo | workflow `deploy-from-scratch.yml` → запускає `create-graylog-lxc.sh` з останнього коміту | `setup-forgejo-runner.sh` **на хості Proxmox** (див. крок 9) | одна кнопка для пересоздання після ручного `pct destroy`, без терміналу | Усі чотири варіанти виконують ті самі ідемпотентні скрипти — жодної "CI-only" поведінки, яка б відрізнялась від того, що ввели б вручну. Жоден з CI-workflow не виконує `pct destroy` — це завжди свідомий ручний крок (дивіться "CI/CD через Forgejo Actions" нижче, чому саме так). ## Покрокове розгортання з нуля ### 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`. Перевірте, що він взагалі активний і підтримує саме цей content type: ```bash pvesm status # усі storage; колонка Status має бути "active" pvesm status --content vztmpl # тільки ті, що підтримують vztmpl - серед них має бути ваш --template-storage sudo pveam list local-btrfs # сам список шаблонів (може бути порожнім - скрипт довантажить) ``` Якщо потрібного storage немає у виводі `--content vztmpl` — або оберіть інший кандидата зі списку, або додайте йому цей content type: `pvesm set --content vztmpl,rootdir,...` (цей виклик **перезаписує весь** список content type, тож перелічіть і ті, що там вже були — гляньте поточний список в `/etc/pve/storage.cfg` перед цим). Якщо після цього все ще помилка "storage is disabled" чи "does not exist" — передайте правильну назву через `--template-storage`. - Storage для кореневої файлової системи контейнера (типово: `EX-Ceph`) з content type `rootdir` і достатнім вільним місцем під `--disk` (за замовчуванням 50GB). Перевірте: ```bash pvesm status --content rootdir # тут має бути ваш --rootfs-storage; колонка Avail - вільне місце в KiB ``` Якщо типового `EX-Ceph` немає у списку або вільного місця замало — передайте правильну назву через `--rootfs-storage`. - 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. Прочитати креденшели адміністратора `install-graylog.sh` сам виводить згенерований логін/пароль адміна один раз, наприкінці успішного свіжого прогону, і одразу видаляє одноразовий файл — більше нічого робити не треба. Якщо пропустили (або прогін впав раніше цього моменту), файл усе ще лежить у `/opt/graylog/.admin_credentials_ONE_TIME`, поки не прочитаєте: ```bash ssh claude-deploy@ "sudo pct exec 210 -- cat /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 протягом приблизно хвилини. ### 9. (Опційно) Налаштувати Forgejo Actions CI `setup-forgejo-runner.sh` реєструє self-hosted Actions runner — він може працювати у двох різних місцях, один скрипт для обох, просто різні параметри. Див. "CI/CD через Forgejo Actions" нижче, що саме робить кожен workflow; цей крок — лише одноразове налаштування раннера. Обидва опційні й незалежні — налаштуйте один, жодного чи обидва. **Спільний перший крок:** у веб-інтерфейсі Forgejo: репо → Settings → Actions → Runners → "Create new Runner", щоб отримати токен реєстрації (одноразове значення, не вгадуйте й не перевикористовуйте з іншого репо — беріть новий токен для кожного раннера, який реєструєте). **9a. Раннер усередині контейнера** (для `deploy.yml` — редеплой тільки конфігурації): ```bash pct exec -- env \ FORGEJO_URL="https://ваш-forgejo-інстанс" \ FORGEJO_RUNNER_TOKEN="<токен>" \ bash /opt/graylog-deploy/setup-forgejo-runner.sh ``` **9b. Раннер на самому хості Proxmox** (для `deploy-from-scratch.yml` — повне пересоздання). Спочатку на хості має бути встановлений `git` (`apt-get install -y git` — у контейнері він уже є, на голому хості зазвичай нема). `RUNNER_USER=claude-deploy` тут обов'язковий, не опційний: без нього systemd-сервіс працює від root, видаючи кожному CI-job необмежений root на хості замість вузько-скоупленого sudo `claude-deploy`: ```bash sudo env FORGEJO_URL="https://ваш-forgejo-інстанс" FORGEJO_RUNNER_TOKEN="<токен>" RUNNER_NAME="proxmox-host-runner" RUNNER_LABEL="proxmox-host:host" RUNNER_DIR="/opt/forgejo-runner-host" SERVICE_NAME="forgejo-runner-host" RUNNER_USER="claude-deploy" bash /home/claude-deploy/graylog-deploy/setup-forgejo-runner.sh ``` (Одним рядком, без `\` — у деяких терміналах багаторядкова вставка з переносами губила продовження, і `sudo env` виконувався зовсім без аргументів і без жодної встановленої змінної.) **9c. Секрети**, які потребують workflow (репо → Settings → Actions → Secrets): `GRAYLOG_ADMIN_PASSWORD` та `DISCORD_WEBHOOK_URL`. Третє значення, `secrets.GITHUB_TOKEN`, створювати не треба — Forgejo видає його автоматично на кожен job, прив'язаним лише до цього репо. **Видалення раннера пізніше** (наприклад, host-level, якщо не хочете тримати його зареєстрованим, коли активно не деплоїте): ```bash sudo systemctl disable --now forgejo-runner-host sudo rm -f /etc/systemd/system/forgejo-runner-host.service sudo systemctl daemon-reload sudo rm -rf /opt/forgejo-runner-host ``` а потім видаліть його й у UI: репо → Settings → Actions → Runners. Повторна реєстрація пізніше потребує нового токена з тієї ж сторінки — старий автоматично не перевикористається. ## Параметри Кожен флаг має відповідник у вигляді змінної середовища (див. початок `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` не підтримує цю опцію (перевірено: `--features` покриває `nesting`/`keyctl`/`mount`/`fuse`/`mknod`/ `force_rw_sys`, нічого для AppArmor), тож автоматизувати це в межах звичайних прав `claude-deploy` (`pct create/set/start/stop/exec/status/list`) неможливо. Якщо зіткнетесь із цим, виконайте під root на хості: ```bash echo "lxc.apparmor.profile: unconfined" >> /etc/pve/lxc/.conf pct reboot ``` а потім перезапустіть `create-graylog-lxc.sh` (ідемпотентний, продовжить з цього місця). **Автоматизація цього фіксу (опційно, одноразово на кожен Proxmox-хост):** запустіть `bootstrap-host.sh` один раз від root. Він встановлює `fix-lxc-apparmor.sh` у `/usr/local/sbin/` (власник root, `chmod 700` — недоступний для запису `claude-deploy`) плюс вузьке sudoers-правило, прив'язане саме до цього скрипта й діапазону VMID 200-299: ``` claude-deploy ALL=(root) NOPASSWD: /usr/local/sbin/fix-lxc-apparmor.sh 2[0-9][0-9] ``` Навмисно *не* ширше правило типу `tee -a /etc/pve/lxc/2[0-9][0-9].conf` чи `sh -c '...'`: sudoers обмежує лише власні аргументи команди, а не вміст stdin/heredoc, тож будь-який з цих варіантів дозволив би дописати *довільні* рядки в конфіг будь-якого контейнера 200-299 (наприклад, `lxc.mount.entry` для монтування хостових шляхів усередину) — набагато ширший доступ, ніж потрібно. Саме фіксація точного рядка всередині незмінного, root-owned скрипта тримає ефект sudoers-правила таким же вузьким, як і виглядає його патерн. Після bootstrap `create-graylog-lxc.sh` сам викликає `sudo -n fix-lxc-apparmor.sh` і повертається до ручної інструкції вище лише якщо цього sudoers-правила ще немає. **Відкликання назад:** запустіть `cleanup-host.sh` від root, коли деплой завершено — він видаляє і sudoers-правило, і скрипт, тож підвищений грант існує лише на час активного деплою, а не постійно. `create-graylog-lxc.sh` коректно повертається до ручного fallback, якщо запуститься без цієї автоматизації; запускайте `bootstrap-host.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 | | Повторюваний error/warning accel-ppp з одного сервера | Той самий `event_type` (з рядків `error:`/`warn:` accel-ppp) повторюється понад 5 разів за 5 хвилин з одного сервера — ловить завислу сесію чи триваючий стан, а не разове попередження. Поріг підтверджено наживо на реальному інциденті: `can't determine router address` повторився 2 746 разів за ~4 години для двох конкретних інтерфейсів | Medium | Третій алерт і є тим самим "універсальним" покриттям проблем мережевого обладнання: він не залежить від знання формату повідомлень конкретного вендора — лише від стандартного рівня severity syslog, який шле будь-який притомний пристрій. ### Алерти на обсяг (flood) Останні два алерти захищають від ситуації, коли одне джерело тихо забиває диск у вікні retention — цикл у логах, шторм ретраїв, чи випадково залишений debug-рівень. Вони групуються за `gl2_remote_ip` (`aggregation-v1`, `count() > поріг`), тож кожне джерело порівнюється зі своїм власним обсягом, а не із сумою по всьому стріму. Пороги не вигадані — вони каліброві наживо 2026-07-22 на реальному трафіку через pivot-пошук Views API, згрупований за `gl2_remote_ip`: - Один активний accel-ppp/RADIUS сервер стабільно слав **~46 000-48 000 повідомлень за 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 кожного сервера. **Перший запуск Vector на новому сервері може спрацювати цей алерт один раз, безпечно** — підтверджено наживо 2026-07-23, коли другий NAS-сервер (EX-NAS-1-2) вийшов онлайн: file-джерело Vector при свіжому запуску (без чекпоінта) читає наявний вміст логу від самого початку, тож завантажений accel-ppp-лог із годинами рутинного DHCP/session-трафіку відтворюється одним сплеском — у тому випадку 430 982 повідомлення за одне 10-хвилинне вікно, усі звичайнісінькі (`DHCPv4 Ack`, `ipoe stop/remove`), усі з міткою часу приблизно в один момент. Не цикл, не помилка конфігурації — просто відтворення накопиченого при холодному старті. Перевірте, чи вляглося, тим самим запитом по джерелу нижче; якщо обсяг лишається підвищеним довше одного-двох вікон — тоді вже трактуйте як реальний інцидент. Перевірити поточний обсяг по джерелу можна будь-коли запитом на кшталт: ``` 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, кожен зі своїм раннером (див. крок 9 вище про налаштування). Обидва просто автоматизують скрипти, вже описані в іншому місці цього README — жодної CI-специфічної поведінки немає. ### `deploy.yml` — редеплой тільки конфігурації Клонує репо наново й повторно запускає `install-graylog.sh` усередині контейнера. Використовуйте після пушу нового правила/алерту/дашборду/ pipeline на вже існуючий контейнер. **Як запускати:** репо → вкладка **Actions** → **Deploy Graylog config** у лівому меню → кнопка **Run workflow** → підтвердити. - **Раннер живе прямо всередині контейнера Graylog** (VMID 200, лейбл `self-hosted:host`, встановлений у `/usr/local/bin/forgejo-runner`, працює як systemd-сервіс). Нових SSH-ключів чи міжхостового доступу не знадобилося — workflow просто клонує репо в `/tmp/graylog-deploy-ci` і запускає скрипт локально, точно як це робив би оператор вручну. ### `deploy-from-scratch.yml` — повне пересоздання Клонує репо наново й повторно запускає `create-graylog-lxc.sh` **на хості Proxmox**. Використовуйте після ручного `pct destroy` (чи на контейнері, якого ще ніколи не було), коли хочете просто натиснути кнопку замість вводу повної команди `create-graylog-lxc.sh`. **Як запускати:** репо → вкладка **Actions** → **Deploy Graylog from scratch (host-level)** у лівому меню → кнопка **Run workflow** → з'явиться поле `cores` (за замовчуванням `4`) — змініть або лишіть як є → підтвердити. Спрацьовує лише коли контейнер реально створюється заново; на вже існуючому — no-op (те саме правило, що й для `--cores` всюди в цьому проєкті — див. "Параметри" вище). - **Раннер живе прямо на хості Proxmox**, працює від `claude-deploy` (не root — див. крок 9b вище, чому це важливо), зареєстрований з окремим лейблом (`proxmox-host:host`), тож підхоплює лише job'и для цього workflow, не для `deploy.yml`. - **`pct destroy` навмисно НЕ входить у цей workflow.** Автоматичне пересоздання інфраструктури безпечне, бо `create-graylog-lxc.sh` ідемпотентний (ремонтує на місці, якщо контейнер уже існує, створює заново, якщо ні); знищення — незворотна втрата даних і залишається свідомим набором команди людиною. Кнопка в CI, яка може знищити продакшн одним випадковим кліком, розглядалась і була відхилена. `pct destroy` навіть не входить у sudo-скоуп `claude-deploy` — свідомо. - **На голому хості має бути встановлений `git`** — на відміну від контейнера (де він є з шаблону Debian 12), свіжий хост Proxmox зазвичай його не має (`apt-get install -y git`, одноразово, root). ### Спільне для обох workflow - **Тригер навмисно ручний** (тільки `workflow_dispatch`, без `on: push`) — вони керують продакшн-системою моніторингу, тож людина переглядає диф і сама натискає "Run workflow", а не кожен push тихо передеплоює систему. - **Без `actions/checkout`** — ця дія вимагає Node.js, якого немає в жодного раннера й не повинно бути лише заради CI. Обидва workflow роблять звичайний `git clone --depth 1`. - **Clone автентифікований через `secrets.GITHUB_TOKEN`** — короткоживучий токен, який Forgejo сама створює на початок кожного запуску workflow і знищує по завершенню, прив'язаний тільки до цього репо (підтверджено офіційною документацією Forgejo: працює навіть коли репо приватне, а спроба використати його проти іншого репо повертає 404). Нічого налаштовувати — це видає платформа, а не секрет, яким керує цей проєкт. Саме це не дає clone зламатись, якщо репо пізніше зробити приватним. - **Секрети** (`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 - створює/ремонтує LXC-контейнер install-graylog.sh # усередині контейнера (запускається автоматично) bootstrap-host.sh # одноразово, root, хост Proxmox - автоматизує AppArmor-фікс cleanup-host.sh # відкочує bootstrap-host.sh fix-lxc-apparmor.sh # встановлюється bootstrap-host.sh, напряму не запускати setup-forgejo-runner.sh # реєструє CI-раннер - у контейнері чи на хості docker-compose.yml # опис стеку MongoDB + OpenSearch + Graylog nftables.conf # firewall-правила, що застосовуються всередині контейнера rules/*.json # визначення Graylog Pipeline Rule (імпортуються через API) pipelines/*.json # визначення Graylog Pipeline, що посилаються на правила streams/*.json # визначення Graylog Stream (маршрутизація за портом input) alerts/*.json # Event Definitions + шаблон Discord-нотифікації dashboards/search_*.json # об'єкти пошуку Views API, пара на кожен дашборд dashboards/view_*.json # макет дашборду/віджетів Views API, у парі з search_*.json .forgejo/workflows/deploy.yml # CI: редеплой тільки конфігурації (раннер у контейнері) .forgejo/workflows/deploy-from-scratch.yml # CI: повне пересоздання (раннер на хості) ``` Всередині контейнера все лежить у `/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 ``` **Сам пароль адміністратора** за замовчуванням випадковий, але `GRAYLOG_ADMIN_PASSWORD` (змінна середовища, та сама, яку прокидує `create-graylog-lxc.sh`) може зафіксувати його на відоме значення замість випадкового — заданий при свіжому встановленні, він стає справжнім паролем, а не лише резервним значенням. Дві різні ролі однієї змінної залежно від того, коли вона задана: - **При свіжому встановленні** (`.env` ще не існує): якщо задано — стає реальним паролем замість випадкового. Якщо ні — випадковий, як і раніше. - **При будь-якому подальшому повторному запуску** (`.env` вже існує, одноразовий файл уже спожитий попереднім успішним прогоном): обов'язково, має збігатися з паролем, який уже налаштований — інакше скрипту нема як автентифікуватись до вже запущеного інстансу. Це найважливіше саме для CI: зафіксуйте пароль один раз через Forgejo секрет `GRAYLOG_ADMIN_PASSWORD`, і він ніколи не застаріє — те саме значення правильне і при створенні, і при кожному подальшому повторному запуску, замість того, щоб помічати, що свіже встановлення згенерувало новий випадковий пароль, і оновлювати секрет вручну (перевірено наживо: повторний запуск `deploy-from-scratch.yml` проти вже ініціалізованого Graylog зі застарілим секретом падав на `resolve_admin_password` з "Cannot find admin password").