feat: add automated deployment scripts and monitoring configuration for Graylog infrastructure
This commit is contained in:
parent
aa5cfbfbd8
commit
06233065cb
33 changed files with 1699 additions and 0 deletions
400
README.uk.md
Normal file
400
README.uk.md
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
# Централізований лог-сервер Graylog — скрипти розгортання
|
||||||
|
|
||||||
|
Розгортає Debian 12 LXC-контейнер на Proxmox VE зі стеком Docker Compose
|
||||||
|
(MongoDB + OpenSearch + Graylog) для збору syslog з мережевого обладнання
|
||||||
|
(Juniper, ZTE OLT, D-Link) та серверів (RADIUS, accel-ppp).
|
||||||
|
|
||||||
|
## Швидкий старт
|
||||||
|
|
||||||
|
Виконайте на хості Proxmox під користувачем `claude-deploy` (або будь-яким
|
||||||
|
іншим користувачем з такими самими обмеженими правами sudo на `pct`/`pveam`
|
||||||
|
для VMID 200-299):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./create-graylog-lxc.sh \
|
||||||
|
--ip 10.254.254.202/24 \
|
||||||
|
--gw 10.254.254.235 \
|
||||||
|
--vlan 1254 \
|
||||||
|
--external-uri http://93.171.241.5:9000/ \
|
||||||
|
--discord-webhook "https://discord.com/api/webhooks/xxx/yyy"
|
||||||
|
```
|
||||||
|
|
||||||
|
Це **єдина команда, яка потрібна**. Вона створює контейнер, встановлює Docker,
|
||||||
|
застосовує firewall (nftables: відкриті лише 9000/tcp, 514+1514/udp,
|
||||||
|
5140/udp), піднімає стек, створює Syslog inputs, імпортує pipeline rules і
|
||||||
|
в кінці виводить дані адміністратора.
|
||||||
|
|
||||||
|
Обидва скрипти ідемпотентні — повторний запуск після збою (або для
|
||||||
|
оновлення) продовжує з того місця, де зупинився, а не дублює роботу.
|
||||||
|
|
||||||
|
## Покрокове розгортання з нуля
|
||||||
|
|
||||||
|
### 0. Передумови
|
||||||
|
|
||||||
|
- SSH-доступ до хоста Proxmox під користувачем з sudo-правами на `pct`
|
||||||
|
create/set/start/stop/exec/status/list та `pveam` update/list/download
|
||||||
|
(root **не потрібен** — весь цей інструментарій побудований і
|
||||||
|
протестований саме в такому обмеженому обсязі прав).
|
||||||
|
- Storage на хості Proxmox з увімкненим content type `vztmpl`, де вже є
|
||||||
|
(або можна завантажити) template `debian-12-standard` — типова назва за
|
||||||
|
замовчуванням: `local-btrfs`. Перевірте:
|
||||||
|
```bash
|
||||||
|
sudo pveam list local-btrfs # замініть на назву вашого storage
|
||||||
|
```
|
||||||
|
Якщо помилка "storage is disabled" чи "does not exist" — знайдіть
|
||||||
|
правильний (`sudo pveam list <name>` для кандидатів) і передайте через
|
||||||
|
`--template-storage`.
|
||||||
|
- Storage для кореневої файлової системи контейнера (типово: `EX-Ceph`) з
|
||||||
|
достатнім вільним місцем під `--disk` (за замовчуванням 50GB).
|
||||||
|
- IP-адреса, шлюз і VLAN-тег (якщо є) мережі, де житиме контейнер — тобто
|
||||||
|
та сама мережа, звідки ваші RADIUS/NAS-сервери й мережеве обладнання
|
||||||
|
зможуть до нього достукатись.
|
||||||
|
- Якщо потрібен доступ до Web UI ззовні цієї мережі — вільний зовнішній
|
||||||
|
порт для DNAT на `9000/tcp` контейнера (див. примітку "доступ до Web UI
|
||||||
|
ззовні" нижче — порт 9000 під час тестування виявився вже зайнятий
|
||||||
|
сторонім ClickHouse, тож не покладайтесь, що якийсь конкретний порт
|
||||||
|
вільний).
|
||||||
|
- (Опційно) Discord webhook URL, якщо хочете отримувати критичні алерти в
|
||||||
|
канал.
|
||||||
|
|
||||||
|
### 1. Перенести скрипти на хост Proxmox
|
||||||
|
|
||||||
|
З вашої робочої машини:
|
||||||
|
```bash
|
||||||
|
tar czf - -C /шлях/до/graylog-deploy . | ssh claude-deploy@<proxmox-host> \
|
||||||
|
"mkdir -p ~/graylog-deploy && tar xzf - -C ~/graylog-deploy && chmod +x ~/graylog-deploy/*.sh"
|
||||||
|
```
|
||||||
|
(Або `git clone`/`scp` теки, якщо тримаєте її в репозиторії — підійде
|
||||||
|
будь-який спосіб, що перенесе всю теку, включно з `rules/`, `pipelines/`,
|
||||||
|
`streams/`, `alerts/`.)
|
||||||
|
|
||||||
|
### 2. Визначитись із параметрами
|
||||||
|
|
||||||
|
Заздалегідь виберіть:
|
||||||
|
|
||||||
|
| Потрібно | Приклад | Навіщо |
|
||||||
|
|---|---|---|
|
||||||
|
| Вільний VMID у 200-299 | `210` | або пропустіть `--vmid`, щоб скрипт сам обрав перший вільний |
|
||||||
|
| IP контейнера + CIDR у мережі керування | `10.254.254.220/24` | має бути досяжним з кожного пристрою, що шле syslog |
|
||||||
|
| Шлюз у цій мережі | `10.254.254.235` | |
|
||||||
|
| VLAN-тег (якщо міст транкований) | `1254` | пропустіть `--vlan`, якщо без тегу |
|
||||||
|
| Публічна URL для Web UI | `http://<публічний-ip>:<порт>/` | використовується і в конфігу Graylog, і в посиланнях Discord-алертів |
|
||||||
|
| Discord webhook (опційно) | `https://discord.com/api/webhooks/.../...` | не вказуйте, щоб пропустити Discord повністю |
|
||||||
|
|
||||||
|
**Спершу перевірте, що IP реально вільний** — дубльований IP у цій мережі
|
||||||
|
непомітно спричиняє ARP flapping і плутану, важко діагностовану
|
||||||
|
мережеву поведінку (саме так сталось під час першого розгортання: `.201`
|
||||||
|
виявився вже зайнятим, і трафік випадково потрапляв не на той хост).
|
||||||
|
Швидка перевірка без жодних змін:
|
||||||
|
```bash
|
||||||
|
ssh claude-deploy@<proxmox-host> "sudo /usr/sbin/pct exec <будь-який-запущений-vmid> -- ping -c2 -W1 <кандидат-ip>"
|
||||||
|
```
|
||||||
|
Якщо є відповіді — ця IP зайнята, оберіть іншу.
|
||||||
|
|
||||||
|
### 3. Запустити `create-graylog-lxc.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh claude-deploy@<proxmox-host>
|
||||||
|
cd ~/graylog-deploy
|
||||||
|
./create-graylog-lxc.sh \
|
||||||
|
--vmid 210 \
|
||||||
|
--ip 10.254.254.220/24 \
|
||||||
|
--gw 10.254.254.235 \
|
||||||
|
--vlan 1254 \
|
||||||
|
--external-uri http://<публічний-ip>:<порт>/ \
|
||||||
|
--discord-webhook "https://discord.com/api/webhooks/xxx/yyy"
|
||||||
|
```
|
||||||
|
|
||||||
|
Що відбувається по черзі: перевірка/завантаження template → `pct create`
|
||||||
|
→ старт контейнера → очікування мережі → копіювання всієї цієї теки в
|
||||||
|
контейнер за шляхом `/opt/graylog-deploy/` → запуск `install-graylog.sh`
|
||||||
|
всередині нього, який встановлює Docker, застосовує firewall, піднімає
|
||||||
|
MongoDB/OpenSearch/Graylog, чекає, поки стек стане healthy, створює Syslog
|
||||||
|
inputs, імпортує pipeline rules/pipelines/streams, і (якщо переданий
|
||||||
|
webhook) створює Discord-notification та три alert definitions.
|
||||||
|
|
||||||
|
**Якщо скрипт зупиняється з помилкою AppArmor** (`open sysctl
|
||||||
|
net.ipv4.ip_unprivileged_port_start: permission denied`) — це очікувано на
|
||||||
|
деяких Proxmox-налаштуваннях і потребує одного ручного кроку від root на
|
||||||
|
хості — див. "Docker-in-unprivileged-LXC AppArmor block" нижче. Зробіть
|
||||||
|
це, а потім просто запустіть ту саму команду ще раз; усе вже зроблене
|
||||||
|
автоматично пропускається.
|
||||||
|
|
||||||
|
Загальний час чистого прогону: кілька хвилин, здебільшого очікування
|
||||||
|
завантаження образів і поки Graylog повідомить healthy.
|
||||||
|
|
||||||
|
### 4. Прочитати креденшели адміністратора
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh claude-deploy@<proxmox-host> "sudo pct exec 210 -- cat /opt/graylog/.admin_credentials_ONE_TIME"
|
||||||
|
```
|
||||||
|
Скопіюйте пароль у менеджер паролів, потім видаліть файл:
|
||||||
|
```bash
|
||||||
|
ssh claude-deploy@<proxmox-host> "sudo pct exec 210 -- rm /opt/graylog/.admin_credentials_ONE_TIME"
|
||||||
|
```
|
||||||
|
Зайдіть на `http://<публічний-ip>:<порт>/` під користувачем `admin` з цим
|
||||||
|
паролем.
|
||||||
|
|
||||||
|
### 5. Доступ до Web UI ззовні мережі керування (якщо потрібно)
|
||||||
|
|
||||||
|
Якщо IP контейнера не досяжна напряму звідти, звідки ви заходите в
|
||||||
|
браузер, додайте DNAT-правило на вашому edge-роутері/файрволі для
|
||||||
|
`9000/tcp`:
|
||||||
|
```
|
||||||
|
-A PREROUTING -d <публічний-ip>/32 -p tcp -m tcp --dport <порт> -j DNAT --to-destination <ip-контейнера>:9000
|
||||||
|
```
|
||||||
|
Оберіть порт, який справді вільний — під час першого розгортання порт
|
||||||
|
9000 виявився вже зайнятий ClickHouse на тій самій публічній IP, і
|
||||||
|
відповідь (`Port 9000 is for clickhouse-client program...`) якийсь час
|
||||||
|
виглядала як проблема Graylog, поки причину не знайшли. Якщо на обраному
|
||||||
|
порту приходить підозріла/неочікувана відповідь — спершу підозрюйте
|
||||||
|
вже існуючий сервіс на цьому порту, а не Graylog.
|
||||||
|
|
||||||
|
### 6. Направити реальне обладнання на сервер
|
||||||
|
|
||||||
|
| Джерело | Порт | Примітка |
|
||||||
|
|---|---|---|
|
||||||
|
| Мережеве обладнання (свічі, OLT, роутери) | **514/udp** | стандартний syslog-порт — майже жодне обладнання не дозволяє обрати інший (`logging <host>` на типовому свічі завжди йде на 514) |
|
||||||
|
| Мережеве обладнання, яке *може* задати кастомний порт | 1514/udp | залишений як другорядний input, той самий стрім, що й 514 |
|
||||||
|
| Сервери (RADIUS, accel-ppp, conntrack/kernel-повідомлення) | **5140/udp** | |
|
||||||
|
|
||||||
|
На кожному пристрої це зазвичай один рядок конфігу (наприклад, на
|
||||||
|
Cisco-подібному CLI: `logging <ip-контейнера>`). Жодних налаштувань з боку
|
||||||
|
Graylog під кожен пристрій не потрібно — inputs і стріми вже слухають на
|
||||||
|
всіх трьох портах.
|
||||||
|
|
||||||
|
### 7. Перевірити, що дані реально надходять
|
||||||
|
|
||||||
|
1. Web UI → **System → Inputs**: кожен input показує живий "Traffic Last
|
||||||
|
Minute". Якщо залишається 0 після того, як пристрій мав щось надіслати
|
||||||
|
— проблема в мережі/firewall, а не в Graylog — підтвердіть через
|
||||||
|
`tcpdump` всередині контейнера, перш ніж чіпати налаштування Graylog:
|
||||||
|
```bash
|
||||||
|
sudo pct exec 210 -- tcpdump -i eth0 -n udp port 514
|
||||||
|
```
|
||||||
|
2. Web UI → **Search**, розширте часовий діапазон (вгорі зліва), клікніть
|
||||||
|
на будь-яке повідомлення, щоб розгорнути. Якщо поля `vendor` /
|
||||||
|
`event_type` заповнені — спрацювало pipeline rule. Якщо повідомлення
|
||||||
|
прийшло, але ці поля порожні — воно дійшло до Graylog нормально, але
|
||||||
|
жодне правило поки не розпізнає його формат — це ознака, що новому
|
||||||
|
пристрою/вендору потрібне нове правило (див. "Що ще НЕ реалізовано"
|
||||||
|
нижче щодо процесу).
|
||||||
|
3. Web UI → **Streams**: колонка "Throughput" показує живий msg/s по
|
||||||
|
кожному стріму.
|
||||||
|
|
||||||
|
### 8. (Опційно) Викликати реальний тестовий алерт
|
||||||
|
|
||||||
|
Див. "Перевірка тестового алерту" нижче — надішліть один із відомих
|
||||||
|
критичних рядків логу через `logger` зсередини контейнера і подивіться,
|
||||||
|
як він приходить у Discord протягом приблизно хвилини.
|
||||||
|
|
||||||
|
## Параметри
|
||||||
|
|
||||||
|
Кожен флаг має відповідник у вигляді змінної середовища (див. початок
|
||||||
|
`create-graylog-lxc.sh`), тож можна також робити `export MEMORY_MB=16384`
|
||||||
|
тощо замість передачі флагів.
|
||||||
|
|
||||||
|
| Флаг | За замовчуванням | Примітка |
|
||||||
|
|---|---|---|
|
||||||
|
| `--ip` | *(обов'язково)* | Статична IP + CIDR для контейнера |
|
||||||
|
| `--gw` | *(обов'язково)* | IP шлюзу |
|
||||||
|
| `--external-uri` | *(обов'язково)* | Публічна URL-адреса Web UI Graylog (використовується в `GRAYLOG_HTTP_EXTERNAL_URI`) |
|
||||||
|
| `--vmid` | перший вільний 200-299 | |
|
||||||
|
| `--hostname` | `graylog` | |
|
||||||
|
| `--cores` | `4` | |
|
||||||
|
| `--memory` | `8192` (МБ) | |
|
||||||
|
| `--swap` | `512` (МБ) | |
|
||||||
|
| `--disk` | `50` (ГБ) | розмір rootfs на `--rootfs-storage` |
|
||||||
|
| `--bridge` | `vmbr0` | |
|
||||||
|
| `--vlan` | *(немає = без тегу)* | |
|
||||||
|
| `--nameserver` | `1.1.1.1` | |
|
||||||
|
| `--searchdomain` | *(немає)* | |
|
||||||
|
| `--timezone` | `Europe/Kyiv` | |
|
||||||
|
| `--template-storage` | `local-btrfs` | має мати увімкнений content type `vztmpl` |
|
||||||
|
| `--rootfs-storage` | `EX-Ceph` | сховище для диска контейнера |
|
||||||
|
| `--discord-webhook` | *(немає)* | передається як `DISCORD_WEBHOOK_URL` у скрипт всередині контейнера |
|
||||||
|
|
||||||
|
## Дашборд
|
||||||
|
|
||||||
|
Дашборд "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/<VMID>.conf
|
||||||
|
pct reboot <VMID>
|
||||||
|
```
|
||||||
|
а потім перезапустіть `create-graylog-lxc.sh` (ідемпотентний, продовжить
|
||||||
|
з цього місця).
|
||||||
|
|
||||||
|
- **`vm.max_map_count`**: OpenSearch вимагає >= 262144. Це
|
||||||
|
загальносистемний параметр ядра хоста, не прив'язаний до конкретного
|
||||||
|
LXC, тож встановити його зсередини контейнера теж неможливо.
|
||||||
|
`install-graylog.sh` лише перевіряє значення і завершується з
|
||||||
|
інструкціями, якщо воно замале — у цьому розгортанні воно вже було
|
||||||
|
262144 за замовчуванням, тож нічого робити не довелось.
|
||||||
|
|
||||||
|
- **TLS Docker Hub через IPv6**: у цій мережі шляхи IPv6 до
|
||||||
|
`registry-1.docker.io` періодично перехоплюються і повертають невідповідний
|
||||||
|
сертифікат (`*.docker.com`). Скрипт встановлення вимикає IPv6 всередині
|
||||||
|
контейнера, щоб форсувати вихід тільки через IPv4. Завантаження образів
|
||||||
|
також повторюється до 5 разів, бо навіть IPv4 інколи потрапляє на
|
||||||
|
проблемний вузол.
|
||||||
|
|
||||||
|
- **Версія MongoDB**: Graylog 7.1 вимагає MongoDB >= 7.0 (деяка застаріла
|
||||||
|
документація досі згадує 6.0.x — не довіряйте кешованій документації
|
||||||
|
більше, ніж тому, що фактично повідомляє запущений сервер).
|
||||||
|
|
||||||
|
- **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 |
|
||||||
|
|
||||||
|
Третій алерт і є тим самим "універсальним" покриттям проблем мережевого
|
||||||
|
обладнання: він не залежить від знання формату повідомлень конкретного
|
||||||
|
вендора — лише від стандартного рівня 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 — тож окремий
|
||||||
|
конвертер не потрібен. Шаблон повідомлення показує заголовок/опис події
|
||||||
|
плюс джерело і повний текст кожного повідомлення, що спрацювало:
|
||||||
|
```
|
||||||
|
*${event_definition_title}*
|
||||||
|
${event_definition_description}
|
||||||
|
${if backlog}${foreach backlog message}• `${message.source}`: ${message.message}
|
||||||
|
${end}${end}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Перевірка тестового алерту
|
||||||
|
|
||||||
|
```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 <VMID> -- cat /opt/graylog/.admin_credentials_ONE_TIME
|
||||||
|
pct exec <VMID> -- rm /opt/graylog/.admin_credentials_ONE_TIME
|
||||||
|
```
|
||||||
21
alerts/alert1_radius_server_down.json
Normal file
21
alerts/alert1_radius_server_down.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "CRITICAL: RADIUS server unreachable",
|
||||||
|
"description": "accel-ppp cannot reach a RADIUS server, or all RADIUS servers are down",
|
||||||
|
"priority": 3,
|
||||||
|
"alert": true,
|
||||||
|
"config": {
|
||||||
|
"type": "aggregation-v1",
|
||||||
|
"query": "event_type:radius_server_timeout OR event_type:radius_all_servers_down",
|
||||||
|
"streams": ["__SERVERS_STREAM_ID__"],
|
||||||
|
"group_by": [],
|
||||||
|
"series": [],
|
||||||
|
"conditions": {"expression": null},
|
||||||
|
"search_within_ms": 60000,
|
||||||
|
"execute_every_ms": 60000,
|
||||||
|
"event_limit": 100
|
||||||
|
},
|
||||||
|
"field_spec": {},
|
||||||
|
"key_spec": [],
|
||||||
|
"notification_settings": {"grace_period_ms": 300000, "backlog_size": 5},
|
||||||
|
"notifications": [{"notification_id": "__DISCORD_NOTIFICATION_ID__"}]
|
||||||
|
}
|
||||||
21
alerts/alert2_conntrack_full.json
Normal file
21
alerts/alert2_conntrack_full.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "CRITICAL: conntrack table full (packet loss)",
|
||||||
|
"description": "nf_conntrack table full on a server - active packet loss is happening right now",
|
||||||
|
"priority": 3,
|
||||||
|
"alert": true,
|
||||||
|
"config": {
|
||||||
|
"type": "aggregation-v1",
|
||||||
|
"query": "event_type:conntrack_table_full",
|
||||||
|
"streams": ["__SERVERS_STREAM_ID__"],
|
||||||
|
"group_by": [],
|
||||||
|
"series": [],
|
||||||
|
"conditions": {"expression": null},
|
||||||
|
"search_within_ms": 60000,
|
||||||
|
"execute_every_ms": 60000,
|
||||||
|
"event_limit": 100
|
||||||
|
},
|
||||||
|
"field_spec": {},
|
||||||
|
"key_spec": [],
|
||||||
|
"notification_settings": {"grace_period_ms": 300000, "backlog_size": 5},
|
||||||
|
"notifications": [{"notification_id": "__DISCORD_NOTIFICATION_ID__"}]
|
||||||
|
}
|
||||||
21
alerts/alert3_generic_critical.json
Normal file
21
alerts/alert3_generic_critical.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "CRITICAL: unrecognized critical-severity syslog",
|
||||||
|
"description": "Any network equipment or server sent a message with syslog severity Emergency/Alert/Critical that no specific pipeline rule recognizes",
|
||||||
|
"priority": 2,
|
||||||
|
"alert": true,
|
||||||
|
"config": {
|
||||||
|
"type": "aggregation-v1",
|
||||||
|
"query": "event_type:generic_critical_syslog",
|
||||||
|
"streams": ["__NETWORK_STREAM_ID__", "__SERVERS_STREAM_ID__"],
|
||||||
|
"group_by": [],
|
||||||
|
"series": [],
|
||||||
|
"conditions": {"expression": null},
|
||||||
|
"search_within_ms": 60000,
|
||||||
|
"execute_every_ms": 60000,
|
||||||
|
"event_limit": 100
|
||||||
|
},
|
||||||
|
"field_spec": {},
|
||||||
|
"key_spec": [],
|
||||||
|
"notification_settings": {"grace_period_ms": 300000, "backlog_size": 5},
|
||||||
|
"notifications": [{"notification_id": "__DISCORD_NOTIFICATION_ID__"}]
|
||||||
|
}
|
||||||
21
alerts/notification_discord.json
Normal file
21
alerts/notification_discord.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "Discord - Critical Alerts",
|
||||||
|
"description": "Discord webhook via Slack-compatible endpoint",
|
||||||
|
"config": {
|
||||||
|
"type": "slack-notification-v1",
|
||||||
|
"color": "#FF0000",
|
||||||
|
"webhook_url": "__DISCORD_WEBHOOK_URL_SLACK__",
|
||||||
|
"channel": "#alerts",
|
||||||
|
"custom_message": "*${event_definition_title}*\n${event_definition_description}\n${if backlog}${foreach backlog message}• `${message.source}`: ${message.message}\n${end}${end}",
|
||||||
|
"user_name": "Graylog",
|
||||||
|
"notify_channel": false,
|
||||||
|
"notify_here": false,
|
||||||
|
"link_names": false,
|
||||||
|
"icon_url": "",
|
||||||
|
"icon_emoji": "",
|
||||||
|
"include_title": true,
|
||||||
|
"include_event_procedure": false,
|
||||||
|
"time_zone": "Europe/Kyiv",
|
||||||
|
"backlog_size": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
229
create-graylog-lxc.sh
Normal file
229
create-graylog-lxc.sh
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Runs on the Proxmox host as the claude-deploy user (restricted sudo: only
|
||||||
|
# pct create/set/start/stop/exec/status/list and pveam update/list/download
|
||||||
|
# for VMIDs 200-299). Creates the Graylog LXC container and triggers the
|
||||||
|
# in-container install. Idempotent: safe to re-run.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# ---- defaults (override via flags or env) ----------------------------------
|
||||||
|
VMID="${VMID:-}"
|
||||||
|
CT_HOSTNAME="${CT_HOSTNAME:-graylog}"
|
||||||
|
CORES="${CORES:-4}"
|
||||||
|
MEMORY_MB="${MEMORY_MB:-8192}"
|
||||||
|
SWAP_MB="${SWAP_MB:-512}"
|
||||||
|
DISK_GB="${DISK_GB:-50}"
|
||||||
|
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local-btrfs}"
|
||||||
|
ROOTFS_STORAGE="${ROOTFS_STORAGE:-EX-Ceph}"
|
||||||
|
BRIDGE="${BRIDGE:-vmbr0}"
|
||||||
|
VLAN_TAG="${VLAN_TAG:-}"
|
||||||
|
IP_CIDR="${IP_CIDR:-}"
|
||||||
|
GATEWAY="${GATEWAY:-}"
|
||||||
|
NAMESERVER="${NAMESERVER:-1.1.1.1}"
|
||||||
|
SEARCHDOMAIN="${SEARCHDOMAIN:-}"
|
||||||
|
TIMEZONE="${TIMEZONE:-Europe/Kyiv}"
|
||||||
|
DEBIAN_TEMPLATE_GLOB="${DEBIAN_TEMPLATE_GLOB:-debian-12-standard_*_amd64.tar.zst}"
|
||||||
|
GRAYLOG_EXTERNAL_URI="${GRAYLOG_EXTERNAL_URI:-}"
|
||||||
|
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||||
|
|
||||||
|
# Colors only when stderr is an actual terminal (never garble log files/pipes).
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
C_RESET=$'\033[0m'; C_CYAN=$'\033[36m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_RED=$'\033[1;31m'
|
||||||
|
else
|
||||||
|
C_RESET=''; C_CYAN=''; C_GREEN=''; C_YELLOW=''; C_RED=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
log() { echo "${C_CYAN}[create-graylog-lxc]${C_RESET} $*" >&2; }
|
||||||
|
ok() { echo "${C_GREEN}[create-graylog-lxc] ✓${C_RESET} $*" >&2; }
|
||||||
|
skip() { echo "${C_YELLOW}[create-graylog-lxc] ⏭${C_RESET} $*" >&2; }
|
||||||
|
die() { echo "${C_RED}[create-graylog-lxc] ✗ ERROR:${C_RESET} $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 --ip 10.254.254.202/24 --gw 10.254.254.235 --vlan 1254 [options]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--ip <cidr> Static IP for the container, e.g. 10.254.254.202/24
|
||||||
|
--gw <ip> Gateway IP
|
||||||
|
--external-uri <url> Public URL used for GRAYLOG_HTTP_EXTERNAL_URI, e.g. http://93.171.241.5:9000/
|
||||||
|
|
||||||
|
Optional (sane defaults shown):
|
||||||
|
--vmid <200-299> default: first free VMID in 200-299
|
||||||
|
--hostname <name> default: graylog
|
||||||
|
--cores <n> default: 4
|
||||||
|
--memory <MB> default: 8192
|
||||||
|
--swap <MB> default: 512
|
||||||
|
--disk <GB> default: 50
|
||||||
|
--bridge <name> default: vmbr0
|
||||||
|
--vlan <tag> default: none (untagged)
|
||||||
|
--nameserver <ip> default: 1.1.1.1
|
||||||
|
--searchdomain <domain> default: (none)
|
||||||
|
--timezone <tz> default: Europe/Kyiv
|
||||||
|
--template-storage <s> default: local-btrfs (must support content type vztmpl)
|
||||||
|
--rootfs-storage <s> default: EX-Ceph (container disk)
|
||||||
|
--discord-webhook <url> optional, passed through to install-graylog.sh
|
||||||
|
|
||||||
|
Everything can also be set via environment variables of the same name in
|
||||||
|
UPPER_SNAKE_CASE (see top of script).
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--vmid) VMID="$2"; shift 2 ;;
|
||||||
|
--hostname) CT_HOSTNAME="$2"; shift 2 ;;
|
||||||
|
--cores) CORES="$2"; shift 2 ;;
|
||||||
|
--memory) MEMORY_MB="$2"; shift 2 ;;
|
||||||
|
--swap) SWAP_MB="$2"; shift 2 ;;
|
||||||
|
--disk) DISK_GB="$2"; shift 2 ;;
|
||||||
|
--bridge) BRIDGE="$2"; shift 2 ;;
|
||||||
|
--vlan) VLAN_TAG="$2"; shift 2 ;;
|
||||||
|
--ip) IP_CIDR="$2"; shift 2 ;;
|
||||||
|
--gw) GATEWAY="$2"; shift 2 ;;
|
||||||
|
--nameserver) NAMESERVER="$2"; shift 2 ;;
|
||||||
|
--searchdomain) SEARCHDOMAIN="$2"; shift 2 ;;
|
||||||
|
--timezone) TIMEZONE="$2"; shift 2 ;;
|
||||||
|
--template-storage) TEMPLATE_STORAGE="$2"; shift 2 ;;
|
||||||
|
--rootfs-storage) ROOTFS_STORAGE="$2"; shift 2 ;;
|
||||||
|
--external-uri) GRAYLOG_EXTERNAL_URI="$2"; shift 2 ;;
|
||||||
|
--discord-webhook) DISCORD_WEBHOOK_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) die "unknown argument: $1 (see --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$IP_CIDR" ] || { usage; die "--ip is required"; }
|
||||||
|
[ -n "$GATEWAY" ] || { usage; die "--gw is required"; }
|
||||||
|
[ -n "$GRAYLOG_EXTERNAL_URI" ] || { usage; die "--external-uri is required"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
pick_free_vmid() {
|
||||||
|
local used id
|
||||||
|
used="$(sudo /usr/sbin/pct list | awk 'NR>1{print $1}')"
|
||||||
|
for id in $(seq 200 299); do
|
||||||
|
if ! echo "$used" | grep -qx "$id"; then
|
||||||
|
echo "$id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
die "no free VMID in range 200-299"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "$VMID" ] || VMID="$(pick_free_vmid)"
|
||||||
|
case "$VMID" in
|
||||||
|
2[0-9][0-9]) ;;
|
||||||
|
*) die "VMID must be in range 200-299, got: $VMID" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CONTAINER_EXISTS=0
|
||||||
|
if sudo /usr/sbin/pct list | awk 'NR>1{print $1}' | grep -qx "$VMID"; then
|
||||||
|
CONTAINER_EXISTS=1
|
||||||
|
skip "container VMID $VMID already exists, reusing it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ensure_template() {
|
||||||
|
log "Checking for Debian 12 template on storage '$TEMPLATE_STORAGE'..."
|
||||||
|
local found
|
||||||
|
found="$(sudo /usr/bin/pveam list "$TEMPLATE_STORAGE" 2>/dev/null | awk -v s="$TEMPLATE_STORAGE" '$1 ~ s":vztmpl/debian-12-standard" {print $1}' | tail -1)"
|
||||||
|
if [ -n "$found" ]; then
|
||||||
|
TEMPLATE_VOLID="$found"
|
||||||
|
skip "template already cached: $TEMPLATE_VOLID"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "No cached template found, downloading (this can take a minute)..."
|
||||||
|
sudo /usr/bin/pveam update
|
||||||
|
local latest
|
||||||
|
latest="$(sudo /usr/bin/pveam available --section system | awk '/debian-12-standard/{print $2}' | sort -V | tail -1)"
|
||||||
|
[ -n "$latest" ] || die "could not find a debian-12-standard template in the pveam catalog"
|
||||||
|
sudo /usr/bin/pveam download "$TEMPLATE_STORAGE" "$latest"
|
||||||
|
TEMPLATE_VOLID="$(sudo /usr/bin/pveam list "$TEMPLATE_STORAGE" | awk -v s="$TEMPLATE_STORAGE" '$1 ~ s":vztmpl/debian-12-standard" {print $1}' | tail -1)"
|
||||||
|
[ -n "$TEMPLATE_VOLID" ] || die "template download reported success but volume not found"
|
||||||
|
ok "downloaded template: $TEMPLATE_VOLID"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_container() {
|
||||||
|
local net0="name=eth0,bridge=$BRIDGE,ip=${IP_CIDR},gw=${GATEWAY}"
|
||||||
|
[ -n "$VLAN_TAG" ] && net0="${net0},tag=${VLAN_TAG}"
|
||||||
|
|
||||||
|
log "Creating container $VMID ($CT_HOSTNAME) from $TEMPLATE_VOLID ..."
|
||||||
|
sudo /usr/sbin/pct create "$VMID" "$TEMPLATE_VOLID" \
|
||||||
|
--hostname "$CT_HOSTNAME" \
|
||||||
|
--cores "$CORES" \
|
||||||
|
--memory "$MEMORY_MB" \
|
||||||
|
--swap "$SWAP_MB" \
|
||||||
|
--rootfs "${ROOTFS_STORAGE}:${DISK_GB}" \
|
||||||
|
--net0 "$net0" \
|
||||||
|
--unprivileged 1 \
|
||||||
|
--features nesting=1,keyctl=1 \
|
||||||
|
--onboot 1 \
|
||||||
|
--nameserver "$NAMESERVER" \
|
||||||
|
${SEARCHDOMAIN:+--searchdomain "$SEARCHDOMAIN"} \
|
||||||
|
--timezone "$TIMEZONE" \
|
||||||
|
--description "Graylog centralized log server"
|
||||||
|
ok "container $VMID created"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_container_and_wait_net() {
|
||||||
|
log "Starting container $VMID..."
|
||||||
|
sudo /usr/sbin/pct start "$VMID"
|
||||||
|
log "Waiting for network..."
|
||||||
|
local i
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
if sudo /usr/sbin/pct exec "$VMID" -- ping -c1 -W2 "$(echo "$GATEWAY")" >/dev/null 2>&1; then
|
||||||
|
ok "network is up"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
die "container did not get network connectivity within ~40s"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_apparmor_hint() {
|
||||||
|
# We cannot fix this ourselves (no permission to edit /etc/pve/lxc/*.conf),
|
||||||
|
# so just remind the operator up front; install-graylog.sh will hard-stop
|
||||||
|
# with the same instructions if it's actually needed.
|
||||||
|
log "NOTE: if Docker fails inside the container with a sysctl/AppArmor permission error,"
|
||||||
|
log " the host admin must run:"
|
||||||
|
log " echo 'lxc.apparmor.profile: unconfined' >> /etc/pve/lxc/${VMID}.conf && pct reboot ${VMID}"
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_install_payload() {
|
||||||
|
log "Copying install payload into the container (/opt/graylog-deploy)..."
|
||||||
|
sudo /usr/sbin/pct exec "$VMID" -- mkdir -p /opt/graylog-deploy
|
||||||
|
tar czf - -C "$SCRIPT_DIR" rules streams pipelines alerts dashboards install-graylog.sh docker-compose.yml nftables.conf \
|
||||||
|
| sudo /usr/sbin/pct exec "$VMID" -- bash -c 'tar xzf - -C /opt/graylog-deploy'
|
||||||
|
sudo /usr/sbin/pct exec "$VMID" -- chmod +x /opt/graylog-deploy/install-graylog.sh
|
||||||
|
}
|
||||||
|
|
||||||
|
run_install() {
|
||||||
|
log "Running install-graylog.sh inside the container..."
|
||||||
|
# GRAYLOG_ADMIN_PASSWORD is only needed on a re-run after the one-time
|
||||||
|
# credentials file was deleted (as instructed in the README); forward it
|
||||||
|
# through if the operator set it, otherwise install-graylog.sh reads the
|
||||||
|
# one-time file itself on a fresh install.
|
||||||
|
sudo /usr/sbin/pct exec "$VMID" -- env \
|
||||||
|
"GRAYLOG_EXTERNAL_URI=$GRAYLOG_EXTERNAL_URI" \
|
||||||
|
"DISCORD_WEBHOOK_URL=$DISCORD_WEBHOOK_URL" \
|
||||||
|
${GRAYLOG_ADMIN_PASSWORD:+"GRAYLOG_ADMIN_PASSWORD=$GRAYLOG_ADMIN_PASSWORD"} \
|
||||||
|
bash /opt/graylog-deploy/install-graylog.sh
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "$CONTAINER_EXISTS" -eq 0 ]; then
|
||||||
|
ensure_template
|
||||||
|
create_container
|
||||||
|
start_container_and_wait_net
|
||||||
|
check_apparmor_hint
|
||||||
|
else
|
||||||
|
sudo /usr/sbin/pct status "$VMID" | grep -q running || sudo /usr/sbin/pct start "$VMID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
copy_install_payload
|
||||||
|
run_install
|
||||||
|
|
||||||
|
echo >&2
|
||||||
|
echo "${C_GREEN}==================================================${C_RESET}" >&2
|
||||||
|
echo "${C_GREEN} DONE - VMID=$VMID, Web UI: $GRAYLOG_EXTERNAL_URI${C_RESET}" >&2
|
||||||
|
echo "${C_GREEN}==================================================${C_RESET}" >&2
|
||||||
94
dashboards/search.json
Normal file
94
dashboards/search.json
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"id": "89d6c0f3-3d35-42ed-9ad3-14cd7daff7c1",
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"filter": null,
|
||||||
|
"filters": [],
|
||||||
|
"query": {"type": "elasticsearch", "query_string": ""},
|
||||||
|
"search_types": [
|
||||||
|
{
|
||||||
|
"id": "b5613b69-ccc8-40a6-a7c6-8c9b9b909abf",
|
||||||
|
"type": "pivot",
|
||||||
|
"name": "chart",
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"series": [{"type": "count", "id": "Message count", "field": null}],
|
||||||
|
"sort": [],
|
||||||
|
"rollup": false,
|
||||||
|
"row_groups": [{"type": "time", "fields": ["timestamp"], "interval": {"type": "auto", "scaling": 1.0}}],
|
||||||
|
"column_groups": [{"type": "values", "fields": ["streams"], "limit": 15, "skip_empty_values": false}],
|
||||||
|
"filter": null,
|
||||||
|
"filters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01f7bdc5-14f0-4221-9bbf-ba3bea6b308c",
|
||||||
|
"type": "pivot",
|
||||||
|
"name": "chart",
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"series": [{"type": "count", "id": "Message count", "field": null}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"rollup": true,
|
||||||
|
"row_groups": [{"type": "values", "fields": ["vendor"], "limit": 10, "skip_empty_values": true}],
|
||||||
|
"column_groups": [],
|
||||||
|
"filter": null,
|
||||||
|
"filters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1295ec49-8bb6-42d2-87ba-adad9ed43e73",
|
||||||
|
"type": "pivot",
|
||||||
|
"name": "chart",
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"series": [{"type": "count", "id": "Message count", "field": null}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"rollup": true,
|
||||||
|
"row_groups": [{"type": "values", "fields": ["event_type"], "limit": 15, "skip_empty_values": true}],
|
||||||
|
"column_groups": [],
|
||||||
|
"filter": null,
|
||||||
|
"filters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d9551aab-be85-43b6-a205-02a8a6434401",
|
||||||
|
"type": "pivot",
|
||||||
|
"name": "chart",
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": {"type": "elasticsearch", "query_string": "severity_tag:critical"},
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"series": [{"type": "count", "id": "Message count", "field": null}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"rollup": true,
|
||||||
|
"row_groups": [{"type": "values", "fields": ["event_type"], "limit": 15, "skip_empty_values": true}],
|
||||||
|
"column_groups": [],
|
||||||
|
"filter": null,
|
||||||
|
"filters": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8694431d-0f1a-4162-910e-de6d3fbe0b3e",
|
||||||
|
"type": "pivot",
|
||||||
|
"name": "chart",
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"series": [{"type": "count", "id": "Message count", "field": null}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"rollup": true,
|
||||||
|
"row_groups": [{"type": "values", "fields": ["source"], "limit": 15, "skip_empty_values": true}],
|
||||||
|
"column_groups": [],
|
||||||
|
"filter": null,
|
||||||
|
"filters": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": []
|
||||||
|
}
|
||||||
176
dashboards/view.json
Normal file
176
dashboards/view.json
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"type": "DASHBOARD",
|
||||||
|
"title": "Network & RADIUS Monitoring",
|
||||||
|
"summary": "Overview of collected syslog: volume, sources, vendors, event types, and critical issues.",
|
||||||
|
"description": "Built for the Juniper/BDCOM/accel-ppp/RADIUS log collection pipeline. Default range is 7 days - adjust with the time selector at the top.",
|
||||||
|
"search_id": "__SEARCH_ID__",
|
||||||
|
"properties": [],
|
||||||
|
"requires": {},
|
||||||
|
"state": {
|
||||||
|
"89d6c0f3-3d35-42ed-9ad3-14cd7daff7c1": {
|
||||||
|
"selected_fields": null,
|
||||||
|
"static_message_list_id": null,
|
||||||
|
"titles": {
|
||||||
|
"tab": {"title": "Overview"},
|
||||||
|
"widget": {
|
||||||
|
"b5613b69-ccc8-40a6-a7c6-8c9b9b909abf": "Messages Over Time by Stream",
|
||||||
|
"01f7bdc5-14f0-4221-9bbf-ba3bea6b308c": "Vendor Breakdown",
|
||||||
|
"1295ec49-8bb6-42d2-87ba-adad9ed43e73": "Top Event Types",
|
||||||
|
"d9551aab-be85-43b6-a205-02a8a6434401": "Critical Events by Type",
|
||||||
|
"8694431d-0f1a-4162-910e-de6d3fbe0b3e": "Top Sources"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"id": "b5613b69-ccc8-40a6-a7c6-8c9b9b909abf",
|
||||||
|
"type": "aggregation",
|
||||||
|
"filter": null,
|
||||||
|
"filters": [],
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"config": {
|
||||||
|
"row_pivots": [{"fields": ["timestamp"], "type": "time", "config": {"interval": {"type": "auto", "scaling": 1.0}}}],
|
||||||
|
"units": {},
|
||||||
|
"column_pivots": [{"fields": ["streams"], "type": "values", "config": {"limit": 15}}],
|
||||||
|
"series": [{"config": {"name": "Message count", "thresholds": []}, "function": "count()"}],
|
||||||
|
"sort": [],
|
||||||
|
"visualization": "bar",
|
||||||
|
"visualization_config": {"barmode": "stack", "axis_type": "linear", "axis_config": null},
|
||||||
|
"formatting_settings": null,
|
||||||
|
"rollup": false,
|
||||||
|
"event_annotation": false,
|
||||||
|
"row_limit": null,
|
||||||
|
"column_limit": 15
|
||||||
|
},
|
||||||
|
"description": null,
|
||||||
|
"context": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "01f7bdc5-14f0-4221-9bbf-ba3bea6b308c",
|
||||||
|
"type": "aggregation",
|
||||||
|
"filter": null,
|
||||||
|
"filters": [],
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"config": {
|
||||||
|
"row_pivots": [{"fields": ["vendor"], "type": "values", "config": {"limit": 10}}],
|
||||||
|
"units": {},
|
||||||
|
"column_pivots": [],
|
||||||
|
"series": [{"config": {"name": "Message count", "thresholds": []}, "function": "count()"}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"visualization": "pie",
|
||||||
|
"visualization_config": null,
|
||||||
|
"formatting_settings": null,
|
||||||
|
"rollup": true,
|
||||||
|
"event_annotation": false,
|
||||||
|
"row_limit": 10,
|
||||||
|
"column_limit": null
|
||||||
|
},
|
||||||
|
"description": null,
|
||||||
|
"context": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1295ec49-8bb6-42d2-87ba-adad9ed43e73",
|
||||||
|
"type": "aggregation",
|
||||||
|
"filter": null,
|
||||||
|
"filters": [],
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"config": {
|
||||||
|
"row_pivots": [{"fields": ["event_type"], "type": "values", "config": {"limit": 15}}],
|
||||||
|
"units": {},
|
||||||
|
"column_pivots": [],
|
||||||
|
"series": [{"config": {"name": "Message count", "thresholds": []}, "function": "count()"}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"visualization": "table",
|
||||||
|
"visualization_config": {"pinned_columns": [], "show_row_numbers": true},
|
||||||
|
"formatting_settings": null,
|
||||||
|
"rollup": true,
|
||||||
|
"event_annotation": false,
|
||||||
|
"row_limit": 15,
|
||||||
|
"column_limit": null
|
||||||
|
},
|
||||||
|
"description": null,
|
||||||
|
"context": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d9551aab-be85-43b6-a205-02a8a6434401",
|
||||||
|
"type": "aggregation",
|
||||||
|
"filter": null,
|
||||||
|
"filters": [],
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": "severity_tag:critical",
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"config": {
|
||||||
|
"row_pivots": [{"fields": ["event_type"], "type": "values", "config": {"limit": 15}}],
|
||||||
|
"units": {},
|
||||||
|
"column_pivots": [],
|
||||||
|
"series": [{"config": {"name": "Message count", "thresholds": []}, "function": "count()"}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"visualization": "table",
|
||||||
|
"visualization_config": {"pinned_columns": [], "show_row_numbers": true},
|
||||||
|
"formatting_settings": null,
|
||||||
|
"rollup": true,
|
||||||
|
"event_annotation": false,
|
||||||
|
"row_limit": 15,
|
||||||
|
"column_limit": null
|
||||||
|
},
|
||||||
|
"description": null,
|
||||||
|
"context": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8694431d-0f1a-4162-910e-de6d3fbe0b3e",
|
||||||
|
"type": "aggregation",
|
||||||
|
"filter": null,
|
||||||
|
"filters": [],
|
||||||
|
"timerange": {"type": "relative", "range": 604800},
|
||||||
|
"query": null,
|
||||||
|
"streams": [],
|
||||||
|
"stream_categories": [],
|
||||||
|
"config": {
|
||||||
|
"row_pivots": [{"fields": ["source"], "type": "values", "config": {"limit": 15}}],
|
||||||
|
"units": {},
|
||||||
|
"column_pivots": [],
|
||||||
|
"series": [{"config": {"name": "Message count", "thresholds": []}, "function": "count()"}],
|
||||||
|
"sort": [{"type": "series", "field": "count()", "direction": "Descending"}],
|
||||||
|
"visualization": "table",
|
||||||
|
"visualization_config": {"pinned_columns": [], "show_row_numbers": true},
|
||||||
|
"formatting_settings": null,
|
||||||
|
"rollup": true,
|
||||||
|
"event_annotation": false,
|
||||||
|
"row_limit": 15,
|
||||||
|
"column_limit": null
|
||||||
|
},
|
||||||
|
"description": null,
|
||||||
|
"context": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"widget_mapping": {
|
||||||
|
"b5613b69-ccc8-40a6-a7c6-8c9b9b909abf": ["b5613b69-ccc8-40a6-a7c6-8c9b9b909abf"],
|
||||||
|
"01f7bdc5-14f0-4221-9bbf-ba3bea6b308c": ["01f7bdc5-14f0-4221-9bbf-ba3bea6b308c"],
|
||||||
|
"1295ec49-8bb6-42d2-87ba-adad9ed43e73": ["1295ec49-8bb6-42d2-87ba-adad9ed43e73"],
|
||||||
|
"d9551aab-be85-43b6-a205-02a8a6434401": ["d9551aab-be85-43b6-a205-02a8a6434401"],
|
||||||
|
"8694431d-0f1a-4162-910e-de6d3fbe0b3e": ["8694431d-0f1a-4162-910e-de6d3fbe0b3e"]
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"b5613b69-ccc8-40a6-a7c6-8c9b9b909abf": {"col": 1, "row": 1, "height": 4, "width": "Infinity"},
|
||||||
|
"01f7bdc5-14f0-4221-9bbf-ba3bea6b308c": {"col": 1, "row": 5, "height": 4, "width": 6},
|
||||||
|
"1295ec49-8bb6-42d2-87ba-adad9ed43e73": {"col": 7, "row": 5, "height": 4, "width": 6},
|
||||||
|
"d9551aab-be85-43b6-a205-02a8a6434401": {"col": 1, "row": 9, "height": 4, "width": 6},
|
||||||
|
"8694431d-0f1a-4162-910e-de6d3fbe0b3e": {"col": 7, "row": 9, "height": 4, "width": 6}
|
||||||
|
},
|
||||||
|
"formatting": {"highlighting": []},
|
||||||
|
"display_mode_settings": {"positions": {}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"share_request": {"selected_grantee_capabilities": {}}
|
||||||
|
}
|
||||||
66
docker-compose.yml
Normal file
66
docker-compose.yml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
services:
|
||||||
|
mongodb:
|
||||||
|
image: mongo:7.0.37
|
||||||
|
container_name: graylog-mongodb
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 1g
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
networks:
|
||||||
|
- graylog-net
|
||||||
|
|
||||||
|
opensearch:
|
||||||
|
image: opensearchproject/opensearch:2.15.0
|
||||||
|
container_name: graylog-opensearch
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 3g
|
||||||
|
environment:
|
||||||
|
- cluster.name=graylog-cluster
|
||||||
|
- node.name=opensearch-node1
|
||||||
|
- discovery.type=single-node
|
||||||
|
- "OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||||
|
- plugins.security.disabled=true
|
||||||
|
- plugins.security.ssl.http.enabled=false
|
||||||
|
- DISABLE_INSTALL_DEMO_CONFIG=true
|
||||||
|
ulimits:
|
||||||
|
nofile:
|
||||||
|
soft: 65536
|
||||||
|
hard: 65536
|
||||||
|
volumes:
|
||||||
|
- os_data:/usr/share/opensearch/data
|
||||||
|
networks:
|
||||||
|
- graylog-net
|
||||||
|
|
||||||
|
graylog:
|
||||||
|
image: graylog/graylog:7.1.5-1
|
||||||
|
container_name: graylog-server
|
||||||
|
restart: unless-stopped
|
||||||
|
mem_limit: 3g
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
- opensearch
|
||||||
|
environment:
|
||||||
|
- GRAYLOG_PASSWORD_SECRET=${GRAYLOG_PASSWORD_SECRET}
|
||||||
|
- GRAYLOG_ROOT_PASSWORD_SHA2=${GRAYLOG_ROOT_PASSWORD_SHA2}
|
||||||
|
- GRAYLOG_HTTP_EXTERNAL_URI=${GRAYLOG_HTTP_EXTERNAL_URI}
|
||||||
|
- GRAYLOG_MONGODB_URI=mongodb://mongodb:27017/graylog
|
||||||
|
- GRAYLOG_ELASTICSEARCH_HOSTS=http://opensearch:9200
|
||||||
|
- "GRAYLOG_SERVER_JAVA_OPTS=-Xms1g -Xmx1g -XX:NewRatio=1 -server -XX:+UseG1GC"
|
||||||
|
ports:
|
||||||
|
- "9000:9000/tcp"
|
||||||
|
- "514:514/udp" # standard syslog port - most network gear can't be pointed anywhere else
|
||||||
|
- "1514:1514/udp" # kept for equipment that *can* use a custom port
|
||||||
|
- "5140:5140/udp"
|
||||||
|
volumes:
|
||||||
|
- graylog_data:/usr/share/graylog/data
|
||||||
|
networks:
|
||||||
|
- graylog-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
graylog-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
os_data:
|
||||||
|
graylog_data:
|
||||||
485
install-graylog.sh
Normal file
485
install-graylog.sh
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Runs INSIDE the Graylog LXC container as root (invoked via `pct exec <vmid> -- bash install-graylog.sh`).
|
||||||
|
# Idempotent: safe to re-run after a partial failure.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
INSTALL_DIR="/opt/graylog"
|
||||||
|
GRAYLOG_EXTERNAL_URI="${GRAYLOG_EXTERNAL_URI:?Set GRAYLOG_EXTERNAL_URI, e.g. http://93.171.241.5:9000/}"
|
||||||
|
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
||||||
|
ADMIN_USER="admin"
|
||||||
|
|
||||||
|
# Colors only when stderr is an actual terminal (never garble log files/pipes).
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
C_RESET=$'\033[0m'; C_CYAN=$'\033[36m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_RED=$'\033[1;31m'
|
||||||
|
else
|
||||||
|
C_RESET=''; C_CYAN=''; C_GREEN=''; C_YELLOW=''; C_RED=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
log() { echo "${C_CYAN}[install-graylog]${C_RESET} $*" >&2; }
|
||||||
|
ok() { echo "${C_GREEN}[install-graylog] ✓${C_RESET} $*" >&2; }
|
||||||
|
skip() { echo "${C_YELLOW}[install-graylog] ⏭${C_RESET} $*" >&2; }
|
||||||
|
die() { echo "${C_RED}[install-graylog] ✗ ERROR:${C_RESET} $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
[ "$(id -u)" -eq 0 ] || die "must run as root inside the container (use pct exec)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
step_packages() {
|
||||||
|
log "Installing base packages (curl, gnupg, python3, jq, nftables)..."
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq ca-certificates curl gnupg python3 jq nftables >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
step_disable_ipv6() {
|
||||||
|
# Workaround: on this hosting network, IPv6 paths to some registries
|
||||||
|
# (docker.io) get intercepted and served a mismatched TLS cert. Disabling
|
||||||
|
# IPv6 in the container forces IPv4-only egress and avoids it.
|
||||||
|
if [ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null || echo 0)" = "1" ]; then
|
||||||
|
skip "IPv6 already disabled"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "Disabling IPv6 (registry TLS workaround for this network)..."
|
||||||
|
cat > /etc/sysctl.d/99-disable-ipv6.conf <<'EOF'
|
||||||
|
net.ipv6.conf.all.disable_ipv6 = 1
|
||||||
|
net.ipv6.conf.default.disable_ipv6 = 1
|
||||||
|
EOF
|
||||||
|
sysctl -p /etc/sysctl.d/99-disable-ipv6.conf >/dev/null
|
||||||
|
ok "IPv6 disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_docker_install() {
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
skip "Docker already installed ($(docker --version))"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "Installing Docker Engine from the official apt repo..."
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
. /etc/os-release
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $VERSION_CODENAME stable" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null
|
||||||
|
systemctl enable --now docker >/dev/null
|
||||||
|
ok "Docker Engine installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_check_max_map_count() {
|
||||||
|
local current
|
||||||
|
current="$(cat /proc/sys/vm/max_map_count)"
|
||||||
|
if [ "$current" -lt 262144 ]; then
|
||||||
|
die "vm.max_map_count=$current (<262144), required by OpenSearch. This is a HOST-level kernel \
|
||||||
|
parameter and cannot be changed from inside an unprivileged LXC. Ask the Proxmox host admin to run: \
|
||||||
|
echo 'vm.max_map_count=262144' >> /etc/sysctl.d/99-opensearch.conf && sysctl -p /etc/sysctl.d/99-opensearch.conf \
|
||||||
|
Then re-run this script."
|
||||||
|
fi
|
||||||
|
ok "vm.max_map_count=$current (>= 262144)"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_docker_smoke_test() {
|
||||||
|
log "Testing Docker can actually run a container (AppArmor check)..."
|
||||||
|
if docker run --rm hello-world >/tmp/docker-test.log 2>&1; then
|
||||||
|
ok "Docker smoke test passed"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if grep -q "ip_unprivileged_port_start" /tmp/docker-test.log; then
|
||||||
|
die "Docker containers fail to start due to unprivileged-LXC AppArmor confinement. \
|
||||||
|
This must be fixed on the PROXMOX HOST (not from inside the container) by the host admin: \
|
||||||
|
echo 'lxc.apparmor.profile: unconfined' >> /etc/pve/lxc/<VMID>.conf && pct reboot <VMID> \
|
||||||
|
Then re-run this script."
|
||||||
|
fi
|
||||||
|
cat /tmp/docker-test.log >&2
|
||||||
|
die "docker run hello-world failed for an unrecognized reason; see output above."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
step_compose_files() {
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
cp "$SCRIPT_DIR/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml"
|
||||||
|
|
||||||
|
if [ -f "$INSTALL_DIR/.env" ]; then
|
||||||
|
skip ".env already exists, keeping existing secrets"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "Generating fresh secrets into $INSTALL_DIR/.env ..."
|
||||||
|
local secret admin_pass admin_sha2
|
||||||
|
secret="$(openssl rand -hex 48)"
|
||||||
|
admin_pass="$(openssl rand -base64 18 | tr -dc 'A-Za-z0-9' | head -c20)"
|
||||||
|
admin_sha2="$(echo -n "$admin_pass" | sha256sum | cut -d' ' -f1)"
|
||||||
|
|
||||||
|
cat > "$INSTALL_DIR/.env" <<EOF
|
||||||
|
GRAYLOG_PASSWORD_SECRET=$secret
|
||||||
|
GRAYLOG_ROOT_PASSWORD_SHA2=$admin_sha2
|
||||||
|
GRAYLOG_HTTP_EXTERNAL_URI=$GRAYLOG_EXTERNAL_URI
|
||||||
|
EOF
|
||||||
|
chmod 600 "$INSTALL_DIR/.env"
|
||||||
|
|
||||||
|
# Only shown once, at generation time - not re-printed on later runs.
|
||||||
|
cat > "$INSTALL_DIR/.admin_credentials_ONE_TIME" <<EOF
|
||||||
|
Graylog admin user: $ADMIN_USER
|
||||||
|
Graylog admin password: $admin_pass
|
||||||
|
(This file is only written once, at first install. Store the password in your
|
||||||
|
password manager, then delete this file: rm $INSTALL_DIR/.admin_credentials_ONE_TIME)
|
||||||
|
EOF
|
||||||
|
chmod 600 "$INSTALL_DIR/.admin_credentials_ONE_TIME"
|
||||||
|
ok "Admin password generated. See $INSTALL_DIR/.admin_credentials_ONE_TIME (read it now, then delete it)."
|
||||||
|
}
|
||||||
|
|
||||||
|
step_compose_up() {
|
||||||
|
log "Pulling images (retrying up to 5x - this registry intermittently serves a bad TLS cert)..."
|
||||||
|
local i
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
if (cd "$INSTALL_DIR" && docker compose pull); then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
log "Pull attempt $i failed, retrying..."
|
||||||
|
sleep 3
|
||||||
|
[ "$i" -eq 5 ] && die "docker compose pull failed after 5 attempts."
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Starting the stack..."
|
||||||
|
(cd "$INSTALL_DIR" && docker compose up -d)
|
||||||
|
|
||||||
|
log "Waiting for graylog-server to become healthy (up to 5 minutes)..."
|
||||||
|
local waited=0
|
||||||
|
while true; do
|
||||||
|
status="$(docker inspect --format '{{.State.Health.Status}}' graylog-server 2>/dev/null || echo starting)"
|
||||||
|
[ "$status" = "healthy" ] && break
|
||||||
|
waited=$((waited + 5))
|
||||||
|
[ "$waited" -ge 300 ] && die "graylog-server did not become healthy within 5 minutes. Check: docker logs graylog-server"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
ok "Stack is healthy"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
GRAYLOG_API="http://localhost:9000/api"
|
||||||
|
|
||||||
|
# Resolves the admin password into the global ADMIN_PASSWORD exactly once.
|
||||||
|
# Must be called as a plain statement (never via `x=$(resolve_admin_password)`)
|
||||||
|
# so that `die`'s `exit` terminates the real script instead of a subshell.
|
||||||
|
ADMIN_PASSWORD=""
|
||||||
|
resolve_admin_password() {
|
||||||
|
[ -n "$ADMIN_PASSWORD" ] && return
|
||||||
|
if [ -f "$INSTALL_DIR/.admin_credentials_ONE_TIME" ]; then
|
||||||
|
ADMIN_PASSWORD="$(grep -oP '(?<=password: ).*' "$INSTALL_DIR/.admin_credentials_ONE_TIME")"
|
||||||
|
elif [ -n "${GRAYLOG_ADMIN_PASSWORD:-}" ]; then
|
||||||
|
ADMIN_PASSWORD="$GRAYLOG_ADMIN_PASSWORD"
|
||||||
|
else
|
||||||
|
die "Cannot find admin password: $INSTALL_DIR/.admin_credentials_ONE_TIME was deleted. \
|
||||||
|
Re-run with GRAYLOG_ADMIN_PASSWORD=<password> set."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
gcurl() {
|
||||||
|
local method="$1" path="$2" data="${3:-}"
|
||||||
|
resolve_admin_password
|
||||||
|
if [ -n "$data" ]; then
|
||||||
|
curl -s -u "${ADMIN_USER}:${ADMIN_PASSWORD}" -H 'X-Requested-By: cli' -H 'Content-Type: application/json' \
|
||||||
|
-X "$method" "${GRAYLOG_API}${path}" -d "$data"
|
||||||
|
else
|
||||||
|
curl -s -u "${ADMIN_USER}:${ADMIN_PASSWORD}" -H 'X-Requested-By: cli' -X "$method" "${GRAYLOG_API}${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step_index_retention() {
|
||||||
|
# Graylog's factory default (as of 7.1) keeps 30-40 days of data across up
|
||||||
|
# to 20 indices - reasonable in general, but risky on a small disk (this
|
||||||
|
# deployment: 50GB) combined with unverified log volume (some sources,
|
||||||
|
# e.g. accel-ppp at debug level, can be very chatty). Tightened to a
|
||||||
|
# 14-21 day window / 15 indices for a larger safety margin. Revisit once
|
||||||
|
# real production volume has been observed for a few weeks.
|
||||||
|
log "Tightening default index set retention (idempotent)..."
|
||||||
|
local idx_id current
|
||||||
|
idx_id="$(gcurl GET /system/indices/index_sets | python3 -c "import json,sys;d=json.load(sys.stdin);print(next(s['id'] for s in d['index_sets'] if s.get('default')))")"
|
||||||
|
current="$(gcurl GET "/system/indices/index_sets/$idx_id")"
|
||||||
|
if echo "$current" | python3 -c "import json,sys;d=json.load(sys.stdin);sys.exit(0 if d['rotation_strategy']['index_lifetime_max']=='P21D' else 1)" 2>/dev/null; then
|
||||||
|
skip "index retention already tightened (P14D/P21D, max 15 indices)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
gcurl PUT "/system/indices/index_sets/$idx_id" "$(python3 -c "
|
||||||
|
import json
|
||||||
|
d = json.loads('''$current''')
|
||||||
|
d['rotation_strategy']['index_lifetime_min'] = 'P14D'
|
||||||
|
d['rotation_strategy']['index_lifetime_max'] = 'P21D'
|
||||||
|
d['retention_strategy']['max_number_of_indices'] = 15
|
||||||
|
d['data_tiering']['index_lifetime_min'] = 'P14D'
|
||||||
|
d['data_tiering']['index_lifetime_max'] = 'P21D'
|
||||||
|
d['writable'] = True
|
||||||
|
print(json.dumps(d))
|
||||||
|
")" >/dev/null
|
||||||
|
ok "index retention tightened to P14D/P21D, max 15 indices"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_inputs() {
|
||||||
|
log "Creating Syslog UDP inputs (idempotent)..."
|
||||||
|
local existing
|
||||||
|
existing="$(gcurl GET /system/inputs)"
|
||||||
|
|
||||||
|
# Port 514 is the standard syslog port and the one most network gear
|
||||||
|
# actually sends to (confirmed live: BDCOM switches here can't be pointed
|
||||||
|
# at a custom port). Port 1514 is kept as a secondary input for any
|
||||||
|
# equipment that *can* be configured with a non-standard destination port.
|
||||||
|
NETWORK_INPUT_ID_514="$(echo "$existing" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((i['id'] for i in d['inputs'] if i['attributes'].get('port')==514),''))")"
|
||||||
|
if [ -z "$NETWORK_INPUT_ID_514" ]; then
|
||||||
|
NETWORK_INPUT_ID_514="$(gcurl POST /system/inputs '{
|
||||||
|
"title": "Network Equipment Syslog (standard port 514)",
|
||||||
|
"type": "org.graylog2.inputs.syslog.udp.SyslogUDPInput",
|
||||||
|
"configuration": {"bind_address":"0.0.0.0","port":514,"recv_buffer_size":262144,
|
||||||
|
"number_worker_threads":2,"force_rdns":false,"allow_override_date":true,
|
||||||
|
"store_full_message":false,"expand_structured_data":true,"charset_name":"UTF-8"},
|
||||||
|
"global": true
|
||||||
|
}' | python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")"
|
||||||
|
ok "Created Network Equipment input (514): $NETWORK_INPUT_ID_514"
|
||||||
|
else
|
||||||
|
skip "Network Equipment input (514) already exists: $NETWORK_INPUT_ID_514"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NETWORK_INPUT_ID_1514="$(echo "$existing" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((i['id'] for i in d['inputs'] if i['attributes'].get('port')==1514),''))")"
|
||||||
|
if [ -z "$NETWORK_INPUT_ID_1514" ]; then
|
||||||
|
NETWORK_INPUT_ID_1514="$(gcurl POST /system/inputs '{
|
||||||
|
"title": "Network Equipment Syslog (Juniper-ZTE-DLink)",
|
||||||
|
"type": "org.graylog2.inputs.syslog.udp.SyslogUDPInput",
|
||||||
|
"configuration": {"bind_address":"0.0.0.0","port":1514,"recv_buffer_size":262144,
|
||||||
|
"number_worker_threads":2,"force_rdns":false,"allow_override_date":true,
|
||||||
|
"store_full_message":false,"expand_structured_data":true,"charset_name":"UTF-8"},
|
||||||
|
"global": true
|
||||||
|
}' | python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")"
|
||||||
|
ok "Created Network Equipment input (1514): $NETWORK_INPUT_ID_1514"
|
||||||
|
else
|
||||||
|
skip "Network Equipment input (1514) already exists: $NETWORK_INPUT_ID_1514"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVERS_INPUT_ID="$(echo "$existing" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((i['id'] for i in d['inputs'] if i['attributes'].get('port')==5140),''))")"
|
||||||
|
if [ -z "$SERVERS_INPUT_ID" ]; then
|
||||||
|
SERVERS_INPUT_ID="$(gcurl POST /system/inputs '{
|
||||||
|
"title": "Servers Syslog (RADIUS-accel-ppp)",
|
||||||
|
"type": "org.graylog2.inputs.syslog.udp.SyslogUDPInput",
|
||||||
|
"configuration": {"bind_address":"0.0.0.0","port":5140,"recv_buffer_size":262144,
|
||||||
|
"number_worker_threads":2,"force_rdns":false,"allow_override_date":true,
|
||||||
|
"store_full_message":false,"expand_structured_data":true,"charset_name":"UTF-8"},
|
||||||
|
"global": true
|
||||||
|
}' | python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")"
|
||||||
|
ok "Created Servers input: $SERVERS_INPUT_ID"
|
||||||
|
else
|
||||||
|
skip "Servers input already exists: $SERVERS_INPUT_ID"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
step_pipeline_rules() {
|
||||||
|
log "Importing pipeline rules from $SCRIPT_DIR/rules/*.json (idempotent)..."
|
||||||
|
local existing_titles
|
||||||
|
existing_titles="$(gcurl GET /system/pipelines/rule | python3 -c "import json,sys;print('\n'.join(r['title'] for r in json.load(sys.stdin)))")"
|
||||||
|
|
||||||
|
local f title
|
||||||
|
for f in "$SCRIPT_DIR"/rules/*.json; do
|
||||||
|
title="$(python3 -c "import json;print(json.load(open('$f'))['title'])")"
|
||||||
|
if echo "$existing_titles" | grep -qx "$title"; then
|
||||||
|
skip "rule '$title' already exists"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local result
|
||||||
|
result="$(gcurl POST /system/pipelines/rule "$(cat "$f")")"
|
||||||
|
if echo "$result" | python3 -c "import json,sys;d=json.load(sys.stdin);sys.exit(0 if d.get('errors') is None else 1)"; then
|
||||||
|
ok "created rule '$title'"
|
||||||
|
else
|
||||||
|
die "rule '$title' failed to compile: $result"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
step_pipelines() {
|
||||||
|
log "Importing pipelines from $SCRIPT_DIR/pipelines/*.json (idempotent)..."
|
||||||
|
local existing
|
||||||
|
existing="$(gcurl GET /system/pipelines/pipeline)"
|
||||||
|
|
||||||
|
local f title id
|
||||||
|
for f in "$SCRIPT_DIR"/pipelines/*.json; do
|
||||||
|
title="$(python3 -c "import json;print(json.load(open('$f'))['title'])")"
|
||||||
|
id="$(echo "$existing" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((p['id'] for p in d if p['title']=='$title'),''))")"
|
||||||
|
if [ -n "$id" ]; then
|
||||||
|
skip "pipeline '$title' already exists ($id)"
|
||||||
|
if [ "$title" = "Network Equipment Parsing" ]; then NETWORK_PIPELINE_ID="$id"; fi
|
||||||
|
if [ "$title" = "Servers Parsing" ]; then SERVERS_PIPELINE_ID="$id"; fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
result="$(gcurl POST /system/pipelines/pipeline "$(cat "$f")")"
|
||||||
|
id="$(echo "$result" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")"
|
||||||
|
[ -n "$id" ] || die "pipeline '$title' failed to create: $result"
|
||||||
|
ok "created pipeline '$title' ($id)"
|
||||||
|
if [ "$title" = "Network Equipment Parsing" ]; then NETWORK_PIPELINE_ID="$id"; fi
|
||||||
|
if [ "$title" = "Servers Parsing" ]; then SERVERS_PIPELINE_ID="$id"; fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
step_streams() {
|
||||||
|
log "Creating streams from $SCRIPT_DIR/streams/*.json (idempotent)..."
|
||||||
|
local index_set_id existing
|
||||||
|
index_set_id="$(gcurl GET /system/indices/index_sets | python3 -c "import json,sys;d=json.load(sys.stdin);print(next(s['id'] for s in d['index_sets'] if s.get('default')))")"
|
||||||
|
existing="$(gcurl GET /streams)"
|
||||||
|
|
||||||
|
# $2.. are extra `sed -e` substitution expressions applied on top of the
|
||||||
|
# index-set-id one, so each template can carry however many input-id
|
||||||
|
# placeholders it needs.
|
||||||
|
render_and_create() {
|
||||||
|
local tmpl="$1" title="$2"; shift 2
|
||||||
|
local id
|
||||||
|
id="$(echo "$existing" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((s['id'] for s in d['streams'] if s['title']=='$title'),''))")"
|
||||||
|
if [ -n "$id" ]; then
|
||||||
|
skip "stream '$title' already exists ($id)"
|
||||||
|
echo "$id"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local sed_args=(-e "s/__DEFAULT_INDEX_SET_ID__/$index_set_id/")
|
||||||
|
local expr
|
||||||
|
for expr in "$@"; do sed_args+=(-e "$expr"); done
|
||||||
|
local body
|
||||||
|
body="$(sed "${sed_args[@]}" "$tmpl")"
|
||||||
|
id="$(gcurl POST /streams "$body" | python3 -c "import json,sys;print(json.load(sys.stdin)['stream_id'])")"
|
||||||
|
ok "created stream '$title' ($id)"
|
||||||
|
gcurl POST "/streams/$id/resume" "" >/dev/null
|
||||||
|
echo "$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
NETWORK_STREAM_ID="$(render_and_create "$SCRIPT_DIR/streams/stream1_network.json" "Network Equipment" \
|
||||||
|
"s/__NETWORK_INPUT_ID_514__/$NETWORK_INPUT_ID_514/" \
|
||||||
|
"s/__NETWORK_INPUT_ID_1514__/$NETWORK_INPUT_ID_1514/")"
|
||||||
|
SERVERS_STREAM_ID="$(render_and_create "$SCRIPT_DIR/streams/stream2_servers.json" "Servers" \
|
||||||
|
"s/__SERVERS_INPUT_ID__/$SERVERS_INPUT_ID/")"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_connect_pipelines() {
|
||||||
|
log "Connecting pipelines to streams..."
|
||||||
|
gcurl POST /system/pipelines/connections/to_stream \
|
||||||
|
"{\"stream_id\":\"$NETWORK_STREAM_ID\",\"pipeline_ids\":[\"$NETWORK_PIPELINE_ID\"]}" >/dev/null
|
||||||
|
gcurl POST /system/pipelines/connections/to_stream \
|
||||||
|
"{\"stream_id\":\"$SERVERS_STREAM_ID\",\"pipeline_ids\":[\"$SERVERS_PIPELINE_ID\"]}" >/dev/null
|
||||||
|
ok "Pipelines connected to streams"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_firewall() {
|
||||||
|
cp "$SCRIPT_DIR/nftables.conf" /etc/nftables.conf
|
||||||
|
systemctl enable nftables >/dev/null 2>&1 || true
|
||||||
|
nft -f /etc/nftables.conf
|
||||||
|
ok "nftables applied (only 9000/tcp, 514+1514/udp, 5140/udp open)"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_dashboard() {
|
||||||
|
if [ ! -f "$SCRIPT_DIR/dashboards/search.json" ]; then
|
||||||
|
skip "Dashboard (no dashboards/search.json in this checkout)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "Creating dashboard (idempotent)..."
|
||||||
|
local title id
|
||||||
|
title="$(python3 -c "import json;print(json.load(open('$SCRIPT_DIR/dashboards/view.json'))['entity']['title'])")"
|
||||||
|
id="$(gcurl GET /views | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((v['id'] for v in d['views'] if v['title']=='$title'),''))")"
|
||||||
|
if [ -n "$id" ]; then
|
||||||
|
skip "dashboard '$title' already exists ($id)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local search_id
|
||||||
|
search_id="$(gcurl POST /views/search "$(cat "$SCRIPT_DIR/dashboards/search.json")" | python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")"
|
||||||
|
[ -n "$search_id" ] || die "dashboard search creation failed"
|
||||||
|
|
||||||
|
local body
|
||||||
|
body="$(sed "s/__SEARCH_ID__/$search_id/" "$SCRIPT_DIR/dashboards/view.json")"
|
||||||
|
id="$(gcurl POST /views "$body" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")"
|
||||||
|
[ -n "$id" ] || die "dashboard '$title' failed to create"
|
||||||
|
ok "created dashboard '$title' ($id)"
|
||||||
|
}
|
||||||
|
|
||||||
|
step_alerts() {
|
||||||
|
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
|
||||||
|
skip "Discord notification and alerts (DISCORD_WEBHOOK_URL not set)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ ! -f "$SCRIPT_DIR/alerts/notification_discord.json" ]; then
|
||||||
|
skip "Alerts (no alerts/notification_discord.json in this checkout)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Creating Discord notification (idempotent)..."
|
||||||
|
local existing_notifs notif_id
|
||||||
|
existing_notifs="$(gcurl GET /events/notifications)"
|
||||||
|
notif_id="$(echo "$existing_notifs" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((n['id'] for n in d['notifications'] if n['title']=='Discord - Critical Alerts'),''))")"
|
||||||
|
if [ -n "$notif_id" ]; then
|
||||||
|
skip "Discord notification already exists ($notif_id)"
|
||||||
|
else
|
||||||
|
local webhook_slack body
|
||||||
|
webhook_slack="${DISCORD_WEBHOOK_URL%/}/slack"
|
||||||
|
body="$(sed "s#__DISCORD_WEBHOOK_URL_SLACK__#${webhook_slack}#" "$SCRIPT_DIR/alerts/notification_discord.json")"
|
||||||
|
notif_id="$(gcurl POST /events/notifications "{\"entity\": ${body}, \"share_request\": {\"selected_grantee_capabilities\": {}}}" \
|
||||||
|
| python3 -c "import json,sys;print(json.load(sys.stdin)['id'])")"
|
||||||
|
ok "created Discord notification ($notif_id)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Creating alert (event) definitions from $SCRIPT_DIR/alerts/*.json (idempotent)..."
|
||||||
|
local existing_defs
|
||||||
|
existing_defs="$(gcurl GET /events/definitions)"
|
||||||
|
local f title id
|
||||||
|
for f in "$SCRIPT_DIR"/alerts/alert*.json; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
title="$(python3 -c "import json;print(json.load(open('$f'))['title'])")"
|
||||||
|
id="$(echo "$existing_defs" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((e['id'] for e in d['event_definitions'] if e['title']=='$title'),''))")"
|
||||||
|
if [ -n "$id" ]; then
|
||||||
|
skip "alert '$title' already exists ($id)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local body
|
||||||
|
body="$(sed \
|
||||||
|
-e "s/__NETWORK_STREAM_ID__/$NETWORK_STREAM_ID/" \
|
||||||
|
-e "s/__SERVERS_STREAM_ID__/$SERVERS_STREAM_ID/" \
|
||||||
|
-e "s/__DISCORD_NOTIFICATION_ID__/$notif_id/" \
|
||||||
|
"$f")"
|
||||||
|
id="$(gcurl POST /events/definitions "{\"entity\": ${body}, \"share_request\": {\"selected_grantee_capabilities\": {}}}" \
|
||||||
|
| python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")"
|
||||||
|
[ -n "$id" ] || die "alert '$title' failed to create"
|
||||||
|
gcurl PUT "/events/definitions/$id/schedule" "" >/dev/null
|
||||||
|
ok "created and enabled alert '$title' ($id)"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
require_root
|
||||||
|
step_packages
|
||||||
|
step_disable_ipv6
|
||||||
|
step_docker_install
|
||||||
|
step_check_max_map_count
|
||||||
|
step_docker_smoke_test
|
||||||
|
step_firewall
|
||||||
|
step_compose_files
|
||||||
|
step_compose_up
|
||||||
|
|
||||||
|
# Must be a plain statement (not `x=$(...)`) so a failure here truly
|
||||||
|
# aborts the script - gcurl() is always called from inside command
|
||||||
|
# substitutions downstream, where `die`'s `exit` would only kill a subshell.
|
||||||
|
resolve_admin_password
|
||||||
|
|
||||||
|
step_index_retention
|
||||||
|
step_inputs
|
||||||
|
step_pipeline_rules
|
||||||
|
step_pipelines
|
||||||
|
step_streams
|
||||||
|
step_connect_pipelines
|
||||||
|
step_dashboard
|
||||||
|
step_alerts
|
||||||
|
|
||||||
|
echo >&2
|
||||||
|
echo "${C_GREEN}==================================================${C_RESET}" >&2
|
||||||
|
echo "${C_GREEN} DONE - Graylog is up${C_RESET}" >&2
|
||||||
|
echo "${C_GREEN}==================================================${C_RESET}" >&2
|
||||||
|
echo " Web UI: $GRAYLOG_EXTERNAL_URI" >&2
|
||||||
|
echo " Config: $INSTALL_DIR/docker-compose.yml and $INSTALL_DIR/.env" >&2
|
||||||
|
if [ -f "$INSTALL_DIR/.admin_credentials_ONE_TIME" ]; then
|
||||||
|
echo " Admin credentials: $INSTALL_DIR/.admin_credentials_ONE_TIME (read once, then delete)" >&2
|
||||||
|
fi
|
||||||
|
echo "${C_GREEN}==================================================${C_RESET}" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
34
nftables.conf
Normal file
34
nftables.conf
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/sbin/nft -f
|
||||||
|
# NOTE: deliberately does NOT `flush ruleset` - that wipes Docker's own
|
||||||
|
# iptables-nft managed nat/filter tables too, breaking container port
|
||||||
|
# publishing on `docker compose up` (confirmed live: this exact mistake
|
||||||
|
# broke Graylog's Docker network and had to be recovered with a full
|
||||||
|
# `docker compose down && up`). Only this table is touched, and only this
|
||||||
|
# table's own rules are flushed - safe whether or not it already exists,
|
||||||
|
# and safe regardless of nftables.service vs docker.service start order.
|
||||||
|
|
||||||
|
add table inet filter
|
||||||
|
flush table inet filter
|
||||||
|
|
||||||
|
table inet filter {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority 0; policy drop;
|
||||||
|
|
||||||
|
iif lo accept
|
||||||
|
ct state established,related accept
|
||||||
|
ip protocol icmp accept
|
||||||
|
|
||||||
|
tcp dport 9000 accept # Graylog Web UI / REST API
|
||||||
|
udp dport 514 accept # syslog, standard port (most network gear can't use anything else)
|
||||||
|
udp dport 1514 accept # syslog, network equipment that can use a custom port
|
||||||
|
udp dport 5140 accept # syslog, servers (RADIUS/accel-ppp)
|
||||||
|
}
|
||||||
|
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority 0; policy accept;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain output {
|
||||||
|
type filter hook output priority 0; policy accept;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
pipelines/pipeline1_network.json
Normal file
5
pipelines/pipeline1_network.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "Network Equipment Parsing",
|
||||||
|
"description": "Parses Juniper and BDCOM/OLT-style syslog, plus universal critical-severity fallback",
|
||||||
|
"source": "pipeline \"Network Equipment Parsing\"\nstage 0 match either\n rule \"juniper_ntp_unreachable\";\n rule \"juniper_ssh_login_success\";\n rule \"juniper_ssh_login_failed_tagged\";\n rule \"juniper_ssh_failed_password\";\n rule \"juniper_ui_configuration_error\";\n rule \"olt_privilege_mode\";\n rule \"olt_cli_logout\";\n rule \"olt_ip_arp_moved\";\n rule \"olt_config_write\";\n rule \"generic_critical_severity\";\nend"
|
||||||
|
}
|
||||||
5
pipelines/pipeline2_servers.json
Normal file
5
pipelines/pipeline2_servers.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "Servers Parsing",
|
||||||
|
"description": "Parses accel-ppp/RADIUS/conntrack syslog",
|
||||||
|
"source": "pipeline \"Servers Parsing\"\nstage 0 match either\n rule \"accelppp_router_address_error\";\n rule \"accelppp_radius_accounting\";\n rule \"accelppp_radius_server_down\";\n rule \"accelppp_no_radius_servers\";\n rule \"accelppp_auth_failed\";\n rule \"conntrack_table_full\";\n rule \"freeradius_login_ok\";\n rule \"freeradius_login_incorrect\";\n rule \"generic_critical_severity\";\nend"
|
||||||
|
}
|
||||||
5
rules/rule10_accelppp_router_addr_error.json
Normal file
5
rules/rule10_accelppp_router_addr_error.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "accelppp_router_address_error",
|
||||||
|
"description": "accel-ppp: can't determine router address (PPPoE/IPoE interface error)",
|
||||||
|
"source": "rule \"accelppp_router_address_error\"\nwhen\n contains(to_string($message.message), \"can't determine router address\")\nthen\n set_field(\"vendor\", \"accel-ppp\");\n set_field(\"event_type\", \"router_address_error\");\n let m = regex(\"error: (\\\\S+): can't determine router address\", to_string($message.message), [\"interface\"]);\n set_field(\"accelppp_interface\", m[\"interface\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule11_accelppp_radius_accounting.json
Normal file
5
rules/rule11_accelppp_radius_accounting.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "accelppp_radius_accounting",
|
||||||
|
"description": "accel-ppp: RADIUS Accounting-Request (session start/stop/alive/interim)",
|
||||||
|
"source": "rule \"accelppp_radius_accounting\"\nwhen\n contains(to_string($message.message), \"Accounting-Request\")\nthen\n set_field(\"vendor\", \"accel-ppp\");\n set_field(\"event_type\", \"radius_accounting\");\n let m = regex(\"(\\\\S+): send \\\\[RADIUS\\\\(\\\\d+\\\\) Accounting-Request.*?<NAS-Identifier \\\"(\\\\S+)\\\">.*?<Acct-Status-Type (\\\\S+)>.*?<Framed-IP-Address (\\\\S+)>\", to_string($message.message), [\"interface\",\"nas_identifier\",\"acct_status_type\",\"framed_ip\"]);\n set_field(\"accelppp_interface\", m[\"interface\"]);\n set_field(\"nas_identifier\", m[\"nas_identifier\"]);\n set_field(\"acct_status_type\", m[\"acct_status_type\"]);\n set_field(\"framed_ip\", m[\"framed_ip\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule12_accelppp_radius_server_down.json
Normal file
5
rules/rule12_accelppp_radius_server_down.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "accelppp_radius_server_down",
|
||||||
|
"description": "accel-ppp: RADIUS server not responding (verified string from accel-ppp source: radius/req.c)",
|
||||||
|
"source": "rule \"accelppp_radius_server_down\"\nwhen\n contains(to_string($message.message), \"radius: server\") && contains(to_string($message.message), \"not responding\")\nthen\n set_field(\"vendor\", \"accel-ppp\");\n set_field(\"event_type\", \"radius_server_timeout\");\n set_field(\"severity_tag\", \"critical\");\n let m = regex(\"radius: server\\\\((\\\\d+)\\\\) not responding\", to_string($message.message), [\"server_id\"]);\n set_field(\"radius_server_id\", m[\"server_id\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule13_accelppp_no_radius_servers.json
Normal file
5
rules/rule13_accelppp_no_radius_servers.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "accelppp_no_radius_servers",
|
||||||
|
"description": "accel-ppp: no available RADIUS servers at all - total auth backend outage (verified string from accel-ppp source: radius/req.c)",
|
||||||
|
"source": "rule \"accelppp_no_radius_servers\"\nwhen\n contains(to_string($message.message), \"radius: no available servers\")\nthen\n set_field(\"vendor\", \"accel-ppp\");\n set_field(\"event_type\", \"radius_all_servers_down\");\n set_field(\"severity_tag\", \"critical\");\nend"
|
||||||
|
}
|
||||||
5
rules/rule14_accelppp_auth_failed.json
Normal file
5
rules/rule14_accelppp_auth_failed.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "accelppp_auth_failed",
|
||||||
|
"description": "accel-ppp: PPP authentication failed (verified string from accel-ppp source: ppp/ppp_auth.c)",
|
||||||
|
"source": "rule \"accelppp_auth_failed\"\nwhen\n contains(to_string($message.message), \"authentication failed\")\nthen\n set_field(\"vendor\", \"accel-ppp\");\n set_field(\"event_type\", \"accelppp_auth_failed\");\nend"
|
||||||
|
}
|
||||||
5
rules/rule15_conntrack_table_full.json
Normal file
5
rules/rule15_conntrack_table_full.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "conntrack_table_full",
|
||||||
|
"description": "Linux kernel: nf_conntrack table full, dropping packets - active packet loss on this NAT/firewall host (standard, well-documented kernel message)",
|
||||||
|
"source": "rule \"conntrack_table_full\"\nwhen\n contains(to_string($message.message), \"nf_conntrack: table full, dropping packet\")\nthen\n set_field(\"vendor\", \"linux-conntrack\");\n set_field(\"event_type\", \"conntrack_table_full\");\n set_field(\"severity_tag\", \"critical\");\nend"
|
||||||
|
}
|
||||||
5
rules/rule16_freeradius_login_ok.json
Normal file
5
rules/rule16_freeradius_login_ok.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "freeradius_login_ok",
|
||||||
|
"description": "Standalone FreeRADIUS: successful authentication (default auth_log format: 'Auth: (n) Login OK: [user] (from client X port P)')",
|
||||||
|
"source": "rule \"freeradius_login_ok\"\nwhen\n contains(to_string($message.message), \"Login OK\")\nthen\n set_field(\"vendor\", \"freeradius\");\n set_field(\"event_type\", \"radius_auth_success\");\n let m = regex(\"Login OK: \\\\[([^\\\\]]+)\\\\] \\\\(from client (\\\\S+)\", to_string($message.message), [\"user\",\"client\"]);\n set_field(\"radius_user\", m[\"user\"]);\n set_field(\"radius_client\", m[\"client\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule17_freeradius_login_incorrect.json
Normal file
5
rules/rule17_freeradius_login_incorrect.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "freeradius_login_incorrect",
|
||||||
|
"description": "Standalone FreeRADIUS: rejected authentication (default auth_log format: 'Auth: Login incorrect: [user] (from client X port P)')",
|
||||||
|
"source": "rule \"freeradius_login_incorrect\"\nwhen\n contains(to_string($message.message), \"Login incorrect\")\nthen\n set_field(\"vendor\", \"freeradius\");\n set_field(\"event_type\", \"radius_auth_reject\");\n let m = regex(\"Login incorrect.*?: \\\\[([^\\\\]]+)\\\\] \\\\(from client (\\\\S+)\", to_string($message.message), [\"user\",\"client\"]);\n set_field(\"radius_user\", m[\"user\"]);\n set_field(\"radius_client\", m[\"client\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule18_generic_critical_severity.json
Normal file
5
rules/rule18_generic_critical_severity.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "generic_critical_severity",
|
||||||
|
"description": "Universal fallback: any syslog message (any vendor) with RFC5424/3164 severity Emergency/Alert/Critical (0-2) that wasn't already classified by a more specific rule",
|
||||||
|
"source": "rule \"generic_critical_severity\"\nwhen\n !has_field(\"event_type\") && has_field(\"level\") && to_long($message.level) <= 2\nthen\n set_field(\"event_type\", \"generic_critical_syslog\");\n set_field(\"severity_tag\", \"critical\");\nend"
|
||||||
|
}
|
||||||
5
rules/rule1_juniper_ntp.json
Normal file
5
rules/rule1_juniper_ntp.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "juniper_ntp_unreachable",
|
||||||
|
"description": "Juniper xntpd NTP unreachable",
|
||||||
|
"source": "rule \"juniper_ntp_unreachable\"\nwhen\n contains(to_string($message.message), \"NTP Server\") && contains(to_string($message.message), \"is Unreachable\")\nthen\n set_field(\"vendor\", \"juniper\");\n set_field(\"event_type\", \"ntp_unreachable\");\n let m = regex(\"NTP Server (\\\\S+) is Unreachable\", to_string($message.message), [\"ntp_server\"]);\n set_field(\"ntp_server\", m[\"ntp_server\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule2_juniper_ssh_ok.json
Normal file
5
rules/rule2_juniper_ssh_ok.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "juniper_ssh_login_success",
|
||||||
|
"description": "Juniper sshd Accepted password/keyboard-interactive",
|
||||||
|
"source": "rule \"juniper_ssh_login_success\"\nwhen\n contains(to_string($message.message), \"sshd\") && contains(to_string($message.message), \"Accepted \")\nthen\n set_field(\"vendor\", \"juniper\");\n set_field(\"event_type\", \"ssh_auth_success\");\n let m = regex(\"Accepted (password|keyboard-interactive/pam) for (\\\\S+) from (\\\\S+) port (\\\\d+)\", to_string($message.message), [\"method\",\"user\",\"src_ip\",\"src_port\"]);\n set_field(\"auth_method\", m[\"method\"]);\n set_field(\"auth_user\", m[\"user\"]);\n set_field(\"src_ip\", m[\"src_ip\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule3_juniper_ssh_failed_tagged.json
Normal file
5
rules/rule3_juniper_ssh_failed_tagged.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "juniper_ssh_login_failed_tagged",
|
||||||
|
"description": "Juniper SSHD_LOGIN_FAILED tagged message",
|
||||||
|
"source": "rule \"juniper_ssh_login_failed_tagged\"\nwhen\n contains(to_string($message.message), \"SSHD_LOGIN_FAILED\")\nthen\n set_field(\"vendor\", \"juniper\");\n set_field(\"event_type\", \"ssh_auth_failed\");\n let m = regex(\"Login failed for user '(\\\\S+)' from host '(\\\\S+)'\", to_string($message.message), [\"user\",\"src_ip\"]);\n set_field(\"auth_user\", m[\"user\"]);\n set_field(\"src_ip\", m[\"src_ip\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule4_juniper_ssh_failed_password.json
Normal file
5
rules/rule4_juniper_ssh_failed_password.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "juniper_ssh_failed_password",
|
||||||
|
"description": "Juniper sshd Failed password line",
|
||||||
|
"source": "rule \"juniper_ssh_failed_password\"\nwhen\n contains(to_string($message.message), \"Failed password for\")\nthen\n set_field(\"vendor\", \"juniper\");\n set_field(\"event_type\", \"ssh_auth_failed\");\n let m = regex(\"Failed password for (\\\\S+) from (\\\\S+) port (\\\\d+)\", to_string($message.message), [\"user\",\"src_ip\",\"src_port\"]);\n set_field(\"auth_user\", m[\"user\"]);\n set_field(\"src_ip\", m[\"src_ip\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule5_juniper_ui_config_error.json
Normal file
5
rules/rule5_juniper_ui_config_error.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "juniper_ui_configuration_error",
|
||||||
|
"description": "Juniper UI_CONFIGURATION_ERROR process/path/statement",
|
||||||
|
"source": "rule \"juniper_ui_configuration_error\"\nwhen\n contains(to_string($message.message), \"UI_CONFIGURATION_ERROR\")\nthen\n set_field(\"vendor\", \"juniper\");\n set_field(\"event_type\", \"config_error\");\n let m = regex(\"Process: (\\\\S+), path: \\\\[(.*?)\\\\], statement: (.*)\", to_string($message.message), [\"process\",\"path\",\"statement\"]);\n set_field(\"config_process\", m[\"process\"]);\n set_field(\"config_path\", m[\"path\"]);\n set_field(\"config_statement\", m[\"statement\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule6_olt_privilege_mode.json
Normal file
5
rules/rule6_olt_privilege_mode.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "olt_privilege_mode",
|
||||||
|
"description": "BDCOM/OLT-style CLI: user entered privilege mode",
|
||||||
|
"source": "rule \"olt_privilege_mode\"\nwhen\n contains(to_string($message.message), \"enter privilege mode\")\nthen\n set_field(\"vendor\", \"bdcom_olt\");\n set_field(\"event_type\", \"cli_privilege_mode\");\n let m = regex(\"User (\\\\S+) enter privilege mode from vty (\\\\d+), level = (\\\\d+)\", to_string($message.message), [\"user\",\"vty\",\"level\"]);\n set_field(\"cli_user\", m[\"user\"]);\n set_field(\"cli_vty\", m[\"vty\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule7_olt_logout.json
Normal file
5
rules/rule7_olt_logout.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "olt_cli_logout",
|
||||||
|
"description": "BDCOM/OLT-style CLI: user logged out from vty",
|
||||||
|
"source": "rule \"olt_cli_logout\"\nwhen\n contains(to_string($message.message), \"logouted from\")\nthen\n set_field(\"vendor\", \"bdcom_olt\");\n set_field(\"event_type\", \"cli_logout\");\n let m = regex(\"User (\\\\S+) logouted from (\\\\S+) on vty (\\\\d+)\", to_string($message.message), [\"user\",\"from_ip\",\"vty\"]);\n set_field(\"cli_user\", m[\"user\"]);\n set_field(\"cli_vty\", m[\"vty\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule8_olt_arp_move.json
Normal file
5
rules/rule8_olt_arp_move.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "olt_ip_arp_moved",
|
||||||
|
"description": "BDCOM/OLT-style: IP ARP moved between MACs (possible duplicate IP / flapping)",
|
||||||
|
"source": "rule \"olt_ip_arp_moved\"\nwhen\n contains(to_string($message.message), \"IP ARP:\") && contains(to_string($message.message), \"moved from\")\nthen\n set_field(\"vendor\", \"bdcom_olt\");\n set_field(\"event_type\", \"arp_moved\");\n let m = regex(\"IP ARP: (\\\\S+) moved from (\\\\S+) to (\\\\S+)\", to_string($message.message), [\"ip\",\"from_mac\",\"to_mac\"]);\n set_field(\"arp_ip\", m[\"ip\"]);\n set_field(\"arp_from_mac\", m[\"from_mac\"]);\n set_field(\"arp_to_mac\", m[\"to_mac\"]);\nend"
|
||||||
|
}
|
||||||
5
rules/rule9_olt_config_write.json
Normal file
5
rules/rule9_olt_config_write.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"title": "olt_config_write",
|
||||||
|
"description": "BDCOM/OLT-style: config file written",
|
||||||
|
"source": "rule \"olt_config_write\"\nwhen\n contains(to_string($message.message), \"is wrote, TID:\")\nthen\n set_field(\"vendor\", \"bdcom_olt\");\n set_field(\"event_type\", \"config_write\");\n let m = regex(\"/(\\\\S+) is wrote, TID:(\\\\S+)\", to_string($message.message), [\"config_file\",\"tid\"]);\n set_field(\"config_file\", m[\"config_file\"]);\n set_field(\"config_tid\", m[\"tid\"]);\nend"
|
||||||
|
}
|
||||||
16
streams/stream1_network.json
Normal file
16
streams/stream1_network.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"title": "Network Equipment",
|
||||||
|
"description": "Syslog from Juniper/ZTE OLT/D-Link (ports 514 and 1514)",
|
||||||
|
"matching_type": "OR",
|
||||||
|
"index_set_id": "__DEFAULT_INDEX_SET_ID__",
|
||||||
|
"remove_matches_from_default_stream": false,
|
||||||
|
"rules": [
|
||||||
|
{"field": "gl2_source_input", "type": 1, "value": "__NETWORK_INPUT_ID_514__", "inverted": false},
|
||||||
|
{"field": "gl2_source_input", "type": 1, "value": "__NETWORK_INPUT_ID_1514__", "inverted": false}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"share_request": {
|
||||||
|
"selected_grantee_capabilities": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
streams/stream2_servers.json
Normal file
15
streams/stream2_servers.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"title": "Servers",
|
||||||
|
"description": "Syslog from RADIUS/accel-ppp servers (port 5140)",
|
||||||
|
"matching_type": "AND",
|
||||||
|
"index_set_id": "__DEFAULT_INDEX_SET_ID__",
|
||||||
|
"remove_matches_from_default_stream": false,
|
||||||
|
"rules": [
|
||||||
|
{"field": "gl2_source_input", "type": 1, "value": "__SERVERS_INPUT_ID__", "inverted": false}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"share_request": {
|
||||||
|
"selected_grantee_capabilities": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue