graylog-deploy/README.uk.md
byrsapty d76f1e766b Fix the same update-gap bug in step_streams()/step_dashboard() as rules/pipelines/alerts
Both only checked title existence, so editing a stream's rules or a
dashboard's widgets under the same title silently skipped the update
forever. step_dashboard() now diffs the search's queries content
(delete+recreate view+search on change - no subscribers/history to lose,
unlike alerts or streams); step_streams() diffs rules individually by
(field,type,value,inverted) and PUT-updates metadata in place, since
delete+recreating a stream would briefly break live message routing.

Confirmed live on a local test deployment: a false "changed" positive
from search_types list-reordering and from search_servers.json's own
query-format inconsistency (bare string vs the {query_string,type} object
Graylog actually stores) had to be fixed before step_dashboard() settled
into a stable no-op; step_streams() was verified with a full inert
rule-add/remove and a metadata-change round-trip against the live API.
2026-07-23 21:00:06 +03:00

62 KiB
Raw Permalink Blame History

Централізований лог-сервер 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):

./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:
    pvesm status                    # усі storage; колонка Status має бути "active"
    pvesm status --content vztmpl   # тільки ті, що підтримують vztmpl - серед них має бути ваш --template-storage
    sudo pveam list local-btrfs     # сам список шаблонів (може бути порожнім - скрипт довантажить)
    
    Якщо потрібного storage немає у виводі --content vztmplабо оберіть інший кандидата зі списку, або додайте йому цей content type: pvesm set <storage> --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). Перевірте:
    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

З вашої робочої машини:

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 виявився вже зайнятим, і трафік випадково потрапляв не на той хост). Швидка перевірка без жодних змін:

ssh claude-deploy@<proxmox-host> "sudo /usr/sbin/pct exec <будь-який-запущений-vmid> -- ping -c2 -W1 <кандидат-ip>"

Якщо є відповіді — ця IP зайнята, оберіть іншу.

3. Запустити create-graylog-lxc.sh

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. Прочитати креденшели адміністратора

install-graylog.sh сам виводить згенерований логін/пароль адміна один раз, наприкінці успішного свіжого прогону, і одразу видаляє одноразовий файл — більше нічого робити не треба. Якщо пропустили (або прогін впав раніше цього моменту), файл усе ще лежить у /opt/graylog/.admin_credentials_ONE_TIME, поки не прочитаєте:

ssh claude-deploy@<proxmox-host> "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 <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:
    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 — редеплой тільки конфігурації):

pct exec <VMID> -- 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:

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, якщо не хочете тримати його зареєстрованим, коли активно не деплоїте):

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 на хості:

    echo "lxc.apparmor.profile: unconfined" >> /etc/pve/lxc/<VMID>.conf
    pct reboot <VMID>
    

    а потім перезапустіть 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 <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
Повторюваний 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:<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_idAcct-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 надійніший, бо береться з самого пакета, а не з даних, які надає пристрій.

Перевірка тестового алерту

# зсередини контейнера, імітуючи кожен тригер:
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 на вже існуючий контейнер.

Як запускати: репо → вкладка ActionsDeploy 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.

Як запускати: репо → вкладка ActionsDeploy 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 всередині контейнера. Сам скрипт виводить його і автоматично видаляє файл наприкінці успішного прогону — збережіть пароль у менеджері паролів тоді ж. Якщо прогін впав раніше цього моменту, прочитайте й видаліть файл вручну:

pct exec <VMID> -- cat /opt/graylog/.admin_credentials_ONE_TIME
pct exec <VMID> -- 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").