feat: Add pfSense hybrid template with OpenVPN monitoring, enhance YAML validation, and remove old DHCP execution plan.

This commit is contained in:
João Pedro Toledo Goncalves 2026-01-04 18:05:01 -03:00
parent 05ffb40a9b
commit fc05977de0
9 changed files with 445 additions and 205 deletions

View File

@ -1,105 +0,0 @@
# Plano de Execução: Monitoramento DHCP Multi-Instância pfSense
Este documento guia o agente de IA na implementação do monitoramento granular de DHCP no pfSense, conforme definido no plano de implementação.
**Arquivo Alvo:** `templates_gold/template_app_pfsense_snmp.yaml`
---
## 1. Enriquecimento da Coleta SNMP (Item)
**Objetivo:** Adicionar a OID de parâmetros (`hrSWRunParameters`) ao item de coleta de software para permitir diferenciar processos `dhcpd`.
- **Ação:** Localizar o item com `key: pfsense.sw.walk`.
- **Alteração:** Adicionar a OID `1.3.6.1.2.1.25.4.2.1.5` à lista `snmp_oid`.
- **Código Atual (Referência):**
```yaml
snmp_oid: walk[1.3.6.1.2.1.25.4.2.1.2,1.3.6.1.2.1.25.4.2.1.7]
```
- **Código Novo:**
```yaml
snmp_oid: walk[1.3.6.1.2.1.25.4.2.1.2,1.3.6.1.2.1.25.4.2.1.7,1.3.6.1.2.1.25.4.2.1.5]
```
- **Atualização do Preprocessing:** Adicionar o passo para mapear a nova OID para um nome JSON (ex: `hrSWRunParameters`).
```yaml
- type: SNMP_WALK_TO_JSON
parameters:
...
- hrSWRunParameters
- 1.3.6.1.2.1.25.4.2.1.5
- '0'
```
## 2. Implementação da Regra de Descoberta (LLD)
**Objetivo:** Criar uma nova regra de descoberta para detectar instâncias individuais do `dhcpd`.
- **Local:** Seção `discovery_rules`.
- **Nova Regra:**
```yaml
- uuid: (gerar_novo_uuid_v4)
name: 'Descoberta de Processos DHCP'
type: DEPENDENT
key: pfsense.dhcp.discovery
delay: '0'
description: 'Descobre instâncias do DHCP Server (dhcpd) diferenciadas por parâmetros (ex: IPv4 vs IPv6).'
master_item:
key: pfsense.sw.walk
filter:
evaltype: AND
conditions:
- macro: '{#HR_SW_NAME}'
value: 'dhcpd'
formulaid: A
lld_macro_paths:
- lld_macro: '{#HR_SW_NAME}'
path: '$.hrSWRunName'
- lld_macro: '{#HR_SW_PARAMS}'
path: '$.hrSWRunParameters'
- lld_macro: '{#HR_SW_STATUS}'
path: '$.hrSWRunStatus'
```
## 3. Protótipos de Itens e Triggers
**Objetivo:** Monitorar o status de cada instância descoberta.
- **Item Prototype:**
```yaml
- uuid: (gerar_novo_uuid_v4)
name: 'Status do Processo DHCP: {#HR_SW_PARAMS}'
type: DEPENDENT
key: 'pfsense.dhcp.process.status[{#HR_SW_PARAMS}]'
delay: '0'
description: 'Status da instância DHCP rodando com argumentos: {#HR_SW_PARAMS}'
valuemap:
name: 'Services status'
preprocessing:
- type: JSONPATH
parameters:
- '$[?(@.hrSWRunName == "{#HR_SW_NAME}" && @.hrSWRunParameters == "{#HR_SW_PARAMS}")].hrSWRunStatus.first()'
master_item:
key: pfsense.sw.walk
tags:
- tag: component
value: application
```
- **Trigger Prototype:**
```yaml
- uuid: (gerar_novo_uuid_v4)
expression: 'last(/PFSense by SNMP/pfsense.dhcp.process.status[{#HR_SW_PARAMS}])=0'
name: '🚨 DHCP Parado: Instância {#HR_SW_PARAMS} não está rodando'
priority: HIGH
description: 'A instância do DHCP com parâmetros "{#HR_SW_PARAMS}" parou de responder.'
```
## 4. Validação Automática (Mandatório)
**Objetivo:** Garantir que o template resultante seja válido e siga padrões (UUIDs únicos, etc).
- **Comando:**
```powershell
python validate_zabbix_template.py templates_gold/template_app_pfsense_snmp.yaml
```
- **Critério de Sucesso:** O script deve retornar `[SUCCESS] YAML Structure & UUIDs are VALID.` sem erros fatais.

View File

@ -1,100 +0,0 @@
# Documentação: PFSense by SNMP
**Template:** PFSense by SNMP
**Descrição:**
Template para monitoramento do pfSense via SNMP
Configuração:
1. Habilite o daemon SNMP em Services na interface web do pfSense: https://docs.netgate.com/pfsense/en/latest/services/snmp.html
2. Configure a regra de firewall para permitir acesso do Zabbix Proxy/Server via SNMP: https://docs.netgate.com/pfsense/en/latest/firewall/index.html#managing-firewall-rules
3. Associe o template ao host.
MIBs usadas:
BEGEMOT-PF-MIB
HOST-RESOURCES-MIB
Gerado originalmente pela ferramenta oficial "Templator", Otimizado para Padrão Gold (Arthur)
## Itens Monitorados
### Itens Globais
- **Coleta Raw (SNMP): Interfaces de Rede PF** (`net.if.pf.walk`)
- **Coleta Raw (SNMP): Interfaces de Rede (IF-MIB)** (`net.if.walk`)
- **Status do servidor DHCP** (`pfsense.dhcpd.status`)
- **Status do servidor DNS** (`pfsense.dns.status`)
- **Estado do processo Nginx (Web)** (`pfsense.nginx.status`)
- **Pacotes com offset incorreto (Bad Offset)** (`pfsense.packets.bad.offset`)
- **Pacotes Fragmentados** (`pfsense.packets.fragment`)
- **Pacotes correspondentes a uma regra de filtro** (`pfsense.packets.match`)
- **Pacotes descartados por limite de memória** (`pfsense.packets.mem.drop`)
- **Pacotes Normalizados** (`pfsense.packets.normalize`)
- **Pacotes Curtos (Short Packets)** (`pfsense.packets.short`)
- **Status de execução do Packet Filter** (`pfsense.pf.status`)
- **Coleta Raw (SNMP): Contadores PF (pfCounter)** (`pfsense.pf_counters.walk`)
- **Contagem de regras de Firewall** (`pfsense.rules.count`)
- **Tabela de Rastreamento: Origens Atuais (Source Tracking)** (`pfsense.source.tracking.table.count`)
- **Tabela de Rastreamento: Limite (Limit)** (`pfsense.source.tracking.table.limit`)
- **Tabela de Rastreamento: Utilização (%)** (`pfsense.source.tracking.table.pused`)
- **Tabela de Estados: Atual (State Table)** (`pfsense.state.table.count`)
- **Tabela de Estados: Limite (Limit)** (`pfsense.state.table.limit`)
- **Tabela de Estados: Utilização (%)** (`pfsense.state.table.pused`)
- **Coleta Raw (SNMP): Software Instalado (hrSWRun)** (`pfsense.sw.walk`)
- **Disponibilidade do Agente SNMP** (`zabbix[host,snmp,available]`)
### Regras de Descoberta (LLD)
#### Descoberta de Processos DHCP (`pfsense.dhcp.discovery`)
- **Protótipos de Itens:**
- Processo DHCP [{#HR_SW_PARAMS}]: Status (`pfsense.dhcp.process.status[{#HR_SW_PARAMS}]`)
- **Protótipos de Triggers:**
- [HIGH] **🚨 DHCP Parado: Instância {#HR_SW_PARAMS} não está rodando**
#### Descoberta de interfaces de rede (`pfsense.net.if.discovery`)
- **Protótipos de Itens:**
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de entrada bloqueado (`net.if.in.block.v4.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de entrada bloqueados (`net.if.in.block.v4.pps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de entrada bloqueado (`net.if.in.block.v6.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de entrada bloqueados (`net.if.in.block.v6.pps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de entrada descartados (`net.if.in.discards[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de entrada com erros (`net.if.in.errors[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de entrada permitido (`net.if.in.pass.v4.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de entrada permitidos (`net.if.in.pass.v4.pps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de entrada permitido (`net.if.in.pass.v6.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de entrada permitidos (`net.if.in.pass.v6.pps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Bits recebidos (`net.if.in[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de saída bloqueado (`net.if.out.block.v4.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de saída bloqueados (`net.if.out.block.v4.pps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de saída bloqueado (`net.if.out.block.v6.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de saída descartados (`net.if.out.discards[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes de saída com erros (`net.if.out.errors[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de saída permitido (`net.if.out.pass.v4.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de saída permitidos (`net.if.out.pass.v4.pps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de saída permitido (`net.if.out.pass.v6.bps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de saída permitidos (`net.if.out.pass.v6.pps[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Bits enviados (`net.if.out[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Contagem de referências de regras (`net.if.rules.refs[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Velocidade (`net.if.speed[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Status operacional (`net.if.status[{#SNMPINDEX}]`)
- Interface [{#IFNAME}({#IFALIAS})]: Tipo de interface (`net.if.type[{#SNMPINDEX}]`)
## Alertas (Triggers)
### Triggers Globais
- [AVERAGE] **🚨 DHCP Parado: Servidor DHCP não está em execução**
- [AVERAGE] **🚨 DNS Parado: Servidor DNS (Unbound) não está em execução**
- [AVERAGE] **🚨 WebServer Parado: Nginx não está rodando**
- [WARNING] **🧩 Fragmentação Excessiva IPv4**
- [WARNING] **🛡️ Possível Ataque/Scan: Pico de Bloqueios**
- [HIGH] **🚨 Firewall Desligado: Packet Filter inativo**
- [WARNING] **⚠️ Tabela de Rastreamento Cheia: Uso elevado ({ITEM.LASTVALUE1}%)**
- [HIGH] **⏳ Source Tracking cheia em < 1h (Previsão)**
- [WARNING] **🔥 Tabela de Estados Crítica: Risco de bloqueio ({ITEM.LASTVALUE1}%)**
- [HIGH] **⏳ Tabela de Estados cheia em < 1h (Previsão)**
- [WARNING] **🚨 Falha SNMP: Sem coleta de dados no pfSense**
### Protótipos de Triggers (LLD)
**Regra: Descoberta de interfaces de rede**
- [WARNING] **🐢 Congestionamento: Descartes na interface {#IFNAME}**
- [WARNING] **⚠️ PFSense: Interface [{#IFNAME}({#IFALIAS})]: Alta taxa de erros de entrada**
- [WARNING] **⚠️ PFSense: Interface [{#IFNAME}({#IFALIAS})]: Alta taxa de erros de saída**
- [AVERAGE] **🔌 PFSense: Interface [{#IFNAME}({#IFALIAS})]: Link indisponível**

View File

@ -0,0 +1,50 @@
# INSTRUÇÕES DE INSTALAÇÃO - AGENTE ZABBIX P/ OPENVPN
=====================================================
Para que o monitoramento do OpenVPN funcione no Template Hybrid Gold, você deve realizar os passos abaixo em CADA firewall pfSense.
REQUISITOS
----------
1. Acesso SSH ao pfSense (Opção 8 no console).
2. Pacote "Zabbix Agent" instalado no pfSense (System > Package Manager).
PASSO 1: INSTALAR O SCRIPT DE DESCOBERTA
----------------------------------------
Este script é usado pelo Zabbix para descobrir automaticamente os usuários conectados.
1. Crie a pasta se não existir:
mkdir -p /opt/zabbix/
2. Copie o arquivo 'files/openvpn-discovery.sh' para '/opt/zabbix/openvpn-discovery.sh' no firewall.
(Você pode usar SCP ou criar o arquivo com 'vi' e colar o conteúdo).
3. Dê permissão de execução:
chmod +x /opt/zabbix/openvpn-discovery.sh
PASSO 2: CONFIGURAR O AGENTE
----------------------------
Este arquivo ensina o Zabbix a usar o script acima e ler os logs.
1. Copie o arquivo 'files/userparameter_openvpn.conf' para '/usr/local/etc/zabbix50/zabbix_agentd.conf.d/userparameter_openvpn.conf'
⚠️ NOTA: O caminho pode variar dependendo da versão do pacote Zabbix Agent (ex: zabbix60, zabbix40). Verifique com 'ls /usr/local/etc/'.
PASSO 3: VERIFICAÇÃO DE CAMINHOS DE LOG
---------------------------------------
O script assume que os logs de status do OpenVPN estão no padrão:
/var/log/openvpn/status*.log
Se o seu pfSense estiver configurado para salvar em outro lugar (verifique em OpenVPN > Servers > Edit > Advanced Settings ou Logs), você DEVE editar os dois arquivos (.sh e .conf) e corrigir o caminho antes de instalar.
PASSO 4: REINICIAR O SERVIÇO
----------------------------
Após copiar os arquivos, reinicie o agente para aplicar as mudanças:
service zabbix_agentd restart
TESTE
-----
No terminal do pfSense, teste se o agente consegue ler a versão do OpenVPN:
zabbix_agentd -t openvpn.version
Se retornar [t|2.x.x], está funcionando!

View File

@ -0,0 +1,199 @@
# AI Agent Execution Plan: OpenVPN Hybrid Integration
**Objective**: transform the `pfsense_hybrid_snmp_agent` folder into a production-ready Hybrid solution.
**Context**: The files `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\openvpn-discovery.sh` and `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\userparameter_openvpn.conf` are currently raw copies. The YAML template is the original SNMP version located at `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\template_app_pfsense_snmp.yaml`.
## Execution Strategy & Safety Context
**Goal**: We are building a "Hybrid" Zabbix Template for pfSense that combines standard SNMP monitoring with advanced OpenVPN metrics collected via Zabbix Agent Custom UserParameters.
**Key Features**:
1. **Dynamic Grouping**: Group VPN users by "Company" (Server Name) derived from log filenames.
2. **Security Triggers**: Detect Exfiltration (>10GB/h), Zombie Sessions (>24h), and Session Hijacking (IP Change).
3. **S2S vs User**: Distinguish Site-to-Site tunnels from human users to apply different alert rules via Macros and Discovery Overrides.
**CRITICAL INSTRUCTION**: If any step in this runbook is ambiguous, fails validation, or if you encounter unexpected file structures, **STOP IMMEDIATELY**. Do not guess. Ask the user for clarification before proceeding to the next step.
---
## Phase 1: Script & Agent Configuration
### 1.1. Rewrite `files/openvpn-discovery.sh`
**Path**: `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\openvpn-discovery.sh`
**Logic**:
- Scan `/var/log/openvpn/status*.log`.
- Extract `{#VPN.SERVER}` from filename (Regex: `status_(.*).log` -> Group 1).
- Extract Users, Real IP, Byte Counts from content.
- **Critical**: Output JSON standard for Zabbix LLD.
```bash
#!/bin/sh
# OpenVPN Discovery Script (Arthur's Gold Standard)
# Outputs: {#VPN.USER}, {#VPN.SERVER}, {#VPN.REAL_IP}
JSON_OUTPUT="{\"data\":["
FIRST_ITEM=1
# Loop through all status logs
for logfile in /var/log/openvpn/status*.log; do
[ -e "$logfile" ] || continue
# Extract Server Name from Filename "status_SERVERNAME.log"
# Note: Busybox filename parsing
filename=$(basename "$logfile")
# Remove prefix "status_" and suffix ".log"
server_name=$(echo "$filename" | sed -e 's/^status_//' -e 's/\.log$//')
# Read the file and parse "CLIENT_LIST" lines
# Format: CLIENT_LIST,CommonName,RealAddress,VirtualAddress,BytesReceived,BytesSent,Since,Since(time_t),Username,ClientID,PeerID
while IFS=, read -r type common_name real_address virtual_address bytes_rx bytes_tx since since_unix username client_id peer_id; do
if [ "$type" = "CLIENT_LIST" ] && [ "$common_name" != "Common Name" ]; then
# Extract IP only from RealAddress (IP:PORT)
real_ip=$(echo "$real_address" | cut -d: -f1)
# Append to JSON
if [ $FIRST_ITEM -eq 0 ]; then JSON_OUTPUT="$JSON_OUTPUT,"; fi
JSON_OUTPUT="$JSON_OUTPUT{\"{#VPN.USER}\":\"$common_name\",\"{#VPN.SERVER}\":\"$server_name\",\"{#VPN.REAL_IP}\":\"$real_ip\"}"
FIRST_ITEM=0
fi
done < "$logfile"
done
JSON_OUTPUT="$JSON_OUTPUT]}"
echo "$JSON_OUTPUT"
```
### 1.2. Update `files/userparameter_openvpn.conf`
**Path**: `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\userparameter_openvpn.conf`
**Logic**:
- Simplify. The discovery script now does the heavy lifting of finding users.
- The Items need to fetch data. Since we have multiple files, `grep` needs to search ALL of them.
- **Optimization**: `grep -h` (no filename) to search all generic status logs.
```conf
UserParameter=openvpn.discovery,/opt/zabbix/openvpn-discovery.sh
# Fetch raw metrics for a specific user (Usernames must be unique across servers or we grab the first match)
UserParameter=openvpn.user.bytes_received.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f6
UserParameter=openvpn.user.bytes_sent.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f7
UserParameter=openvpn.user.connected_since[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f9
UserParameter=openvpn.user.real_address.new[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f3 | cut -d: -f1
UserParameter=openvpn.user.status[*],if grep -q "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null; then echo 1; else echo 0; fi
UserParameter=openvpn.version,openvpn --version 2>&1 | head -1 | awk '{print $2}'
```
---
## Phase 2: Template Configuration (YAML)
**Target**: `templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold.yaml`
### 2.1. Add Macros
Append to `macros` section:
```yaml
- macro: '{$VPN.S2S.PATTERN}'
value: '^S2S_'
description: 'Regex para identificar túneis Site-to-Site.'
- macro: '{$VPN.DATA.LIMIT}'
value: '10737418240'
description: 'Limite de Download (10GB) para alerta de Exfiltração.'
- macro: '{$VPN.WORK.START}'
value: '080000'
- macro: '{$VPN.WORK.END}'
value: '180000'
- macro: '{$VPN.ZOMBIE.LIMIT}'
value: '86400'
description: 'Tempo máximo (24h) para considerar sessão zumbi.'
```
### 2.2. Add Discovery Rule
**Name**: `Descoberta de Usuários OpenVPN`
**Key**: `openvpn.discovery`
**Type**: `ZABBIX_ACTIVE` (Preferred for Agents behind NAT/Firewall)
**Overrides**:
```yaml
overrides:
- name: 'Site-to-Site (S2S)'
step: '1'
filter:
conditions:
- macro: '{#VPN.USER}'
value: '{$VPN.S2S.PATTERN}'
operator: REGEXP
operations:
- operationobject: ITEM_PROTOTYPE
operator: REGEXP
value: 'Stats Year|Forecast'
status: ENABLED
- operationobject: TRIGGER_PROTOTYPE
operator: REGEXP
value: 'Exfiltração|Horário|Zombie|IP Change'
status: DISABLED
- operationobject: ITEM_PROTOTYPE
operator: LIKE
value: ''
tags:
- tag: Type
value: S2S
- name: 'User (Colaborador)'
step: '2'
filter:
conditions:
- macro: '{#VPN.USER}'
value: '{$VPN.S2S.PATTERN}'
operator: NOT_REGEXP
operations:
- operationobject: ITEM_PROTOTYPE
operator: REGEXP
value: 'Stats Year|Forecast'
status: DISABLED
- operationobject: TRIGGER_PROTOTYPE
operator: REGEXP
value: 'Exfiltração|Horário|Zombie|IP Change'
status: ENABLED
- operationobject: ITEM_PROTOTYPE
operator: LIKE
value: ''
tags:
- tag: Type
value: User
```
### 2.3. Add Item Prototypes (Selected)
**Common Tags**: `Company: {{#VPN.SERVER}.regsub("(?:CLIENT_|S2S_)?(.*)", "\1")}`
1. **Download Total (Raw)**
- Key: `openvpn.user.bytes_received.total[{#VPN.USER}]`
- Type: ZABBIX_ACTIVE
- Units: B
2. **Download Rate (Calculated/Dependent?)**
- Actually, simpler to let Zabbix standard "Simple Change" preprocessing handle rate on a Dependent Item, or just use `Change per Second` preprocessing.
- **Decision**: Create `openvpn.user.bytes_received.rate[{#VPN.USER}]` dependent on Total, with `Change per Second`.
3. **Real IP (Inventory)**
- Key: `openvpn.user.real_address.new[{#VPN.USER}]`
- Populates Host Inventory field? No, this is an item prototype. Just keep as value.
**Reporting Metrics (Calculated)**
- **Stats Week**: `trendsum(//openvpn.user.bytes_received.total[{#VPN.USER}], 1w:now/w)`
- **Stats Month**: `trendsum(..., 1M:now/M)`
- **Forecast**: `(last(Month) / dayat()) * dayofmonth(end)` (Simplified)
### 2.4. Add Triggers
1. **Exfiltração**: `(last(Total) - last(Total, 1h)) > {$VPN.DATA.LIMIT}`
2. **IP Change**: `diff(RealIP)=1 and last(Status)=1`
3. **Zombie**: `last(ConnectedSince) < now() - {$VPN.ZOMBIE.LIMIT}`
### 2.5. Add Dashboard
Insert `dashboards:` block at root (or modify existing if any).
- Use `type: svggraph` for Trends.
- Use `type: tophosts` for Tables (Columns: Name, Latest Value of Metric).
---
## Phase 3: Validation
1. **Lint**: `validate_zabbix_template.py`.
2. **Docs**: `generate_template_docs.py`.

View File

@ -0,0 +1,34 @@
#!/bin/sh
# OpenVPN Discovery Script (Arthur's Gold Standard)
# Outputs: {#VPN.USER}, {#VPN.SERVER}, {#VPN.REAL_IP}
JSON_OUTPUT="{\"data\":["
FIRST_ITEM=1
# Loop through all status logs
for logfile in /var/log/openvpn/status*.log; do
[ -e "$logfile" ] || continue
# Extract Server Name from Filename "status_SERVERNAME.log"
# Note: Busybox filename parsing
filename=$(basename "$logfile")
# Remove prefix "status_" and suffix ".log"
server_name=$(echo "$filename" | sed -e 's/^status_//' -e 's/\.log$//')
# Read the file and parse "CLIENT_LIST" lines
# Format: CLIENT_LIST,CommonName,RealAddress,VirtualAddress,BytesReceived,BytesSent,Since,Since(time_t),Username,ClientID,PeerID
while IFS=, read -r type common_name real_address virtual_address bytes_rx bytes_tx since since_unix username client_id peer_id; do
if [ "$type" = "CLIENT_LIST" ] && [ "$common_name" != "Common Name" ]; then
# Extract IP only from RealAddress (IP:PORT)
real_ip=$(echo "$real_address" | cut -d: -f1)
# Append to JSON
if [ $FIRST_ITEM -eq 0 ]; then JSON_OUTPUT="$JSON_OUTPUT,"; fi
JSON_OUTPUT="$JSON_OUTPUT{\"{#VPN.USER}\":\"$common_name\",\"{#VPN.SERVER}\":\"$server_name\",\"{#VPN.REAL_IP}\":\"$real_ip\"}"
FIRST_ITEM=0
fi
done < "$logfile"
done
JSON_OUTPUT="$JSON_OUTPUT]}"
echo "$JSON_OUTPUT"

View File

@ -0,0 +1,16 @@
# OpenVPN UserParameters for Zabbix Agent (Arthur's Gold Standard)
# Compatible with: Zabbix 7.0+
# Installation: Copy to /usr/local/etc/zabbix72/zabbix_agentd.conf.d/
UserParameter=openvpn.discovery,/opt/zabbix/openvpn-discovery.sh
# Fetch raw metrics for a specific user (Usernames must be unique across servers or we grab the first match)
UserParameter=openvpn.user.bytes_received.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f5
UserParameter=openvpn.user.bytes_sent.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f6
UserParameter=openvpn.user.connected_since[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f8
UserParameter=openvpn.user.real_address.new[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f3 | cut -d: -f1
UserParameter=openvpn.user.status[*],if grep -q "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null; then echo 1; else echo 0; fi
# General OpenVPN Instance Metrics
UserParameter=openvpn.version,openvpn --version 2>&1 | head -1 | awk '{print $2}'
UserParameter=openvpn.user.count,grep -h "^CLIENT_LIST" /var/log/openvpn/status*.log 2>/dev/null | grep -v "Common Name" | wc -l

View File

@ -0,0 +1,73 @@
# Documentação: PFSense Hybrid: SNMP + OpenVPN Agent
**Template:** PFSense Hybrid: SNMP + OpenVPN Agent
**Descrição:**
Template Híbrido para monitoramento do pfSense.
SNMP: Monitoramento de interfaces, firewall, serviços (DHCP, DNS, Nginx).
Agent: Monitoramento avançado de OpenVPN (usuários, túneis S2S, tráfego).
Requisitos:
1. Habilite o daemon SNMP em Services na interface web do pfSense.
2. Instale o Zabbix Agent e configure os UserParameters OpenVPN (ver INSTRUCOES_AGENTE.txt).
3. Associe o template ao host.
MIBs: BEGEMOT-PF-MIB, HOST-RESOURCES-MIB
Keys Agent: openvpn.*
Gerado pelo Padrão Gold (Arthur) - ITGuys
## Itens Monitorados
### Itens Globais
- **Coleta Raw (SNMP): Interfaces de Rede PF** (`net.if.pf.walk`)
- **Coleta Raw (SNMP): Interfaces de Rede (IF-MIB)** (`net.if.walk`)
- **Status do servidor DHCP** (`pfsense.dhcpd.status`)
- **Status do servidor DNS** (`pfsense.dns.status`)
- **Estado do processo Nginx (Web)** (`pfsense.nginx.status`)
- **Pacotes com offset incorreto (Bad Offset)** (`pfsense.packets.bad.offset`)
- **Pacotes Fragmentados** (`pfsense.packets.fragment`)
- **Pacotes correspondentes a uma regra de filtro** (`pfsense.packets.match`)
- **Pacotes descartados por limite de memória** (`pfsense.packets.mem.drop`)
- **Pacotes Normalizados** (`pfsense.packets.normalize`)
- **Pacotes Curtos (Short Packets)** (`pfsense.packets.short`)
- **Status de execução do Packet Filter** (`pfsense.pf.status`)
- **Coleta Raw (SNMP): Contadores PF (pfCounter)** (`pfsense.pf_counters.walk`)
- **Contagem de regras de Firewall** (`pfsense.rules.count`)
- **Tabela de Rastreamento: Origens Atuais (Source Tracking)** (`pfsense.source.tracking.table.count`)
- **Tabela de Rastreamento: Limite (Limit)** (`pfsense.source.tracking.table.limit`)
- **Tabela de Rastreamento: Utilização (%)** (`pfsense.source.tracking.table.pused`)
- **Tabela de Estados: Atual (State Table)** (`pfsense.state.table.count`)
- **Tabela de Estados: Limite (Limit)** (`pfsense.state.table.limit`)
- **Tabela de Estados: Utilização (%)** (`pfsense.state.table.pused`)
- **Coleta Raw (SNMP): Software Instalado (hrSWRun)** (`pfsense.sw.walk`)
- **Disponibilidade do Agente SNMP** (`zabbix[host,snmp,available]`)
### Regras de Descoberta (LLD)
#### Descoberta de Usuários OpenVPN (`openvpn.discovery`)
- **Protótipos de Itens:**
- OpenVPN [{#VPN.USER}]: Download Total (Bytes) (`openvpn.user.bytes_received.total[{#VPN.USER}]`)
- OpenVPN [{#VPN.USER}]: Upload Total (Bytes) (`openvpn.user.bytes_sent.total[{#VPN.USER}]`)
- OpenVPN [{#VPN.USER}]: IP Real (`openvpn.user.real_address.new[{#VPN.USER}]`)
- OpenVPN [{#VPN.USER}]: Status (`openvpn.user.status[{#VPN.USER}]`)
- OpenVPN [{#VPN.USER}]: Tempo Conectado (Unix) (`openvpn.user.connected_since[{#VPN.USER}]`)
## Alertas (Triggers)
### Triggers Globais
- [AVERAGE] **🚨 DHCP Parado: Servidor DHCP não está em execução**
- [AVERAGE] **🚨 DNS Parado: Servidor DNS (Unbound) não está em execução**
- [AVERAGE] **🚨 WebServer Parado: Nginx não está rodando**
- [WARNING] **🧩 Fragmentação Excessiva IPv4**
- [WARNING] **🛡️ Possível Ataque/Scan: Pico de Bloqueios**
- [HIGH] **🚨 Firewall Desligado: Packet Filter inativo**
- [WARNING] **⚠️ Tabela de Rastreamento Cheia: Uso elevado ({ITEM.LASTVALUE1}%)**
- [HIGH] **⏳ Source Tracking cheia em < 1h (Previsão)**
- [WARNING] **🔥 Tabela de Estados Crítica: Risco de bloqueio ({ITEM.LASTVALUE1}%)**
- [HIGH] **⏳ Tabela de Estados cheia em < 1h (Previsão)**
- [WARNING] **🚨 Falha SNMP: Sem coleta de dados no pfSense**
### Protótipos de Triggers (LLD)
**Regra: Descoberta de Usuários OpenVPN**

View File

@ -190,10 +190,83 @@ def validate_dashboard_references(content, graph_names):
return errors return errors
def check_duplicate_yaml_keys(file_path):
"""
Check for duplicate YAML keys at the same level (e.g., two 'macros:' sections).
This is a Zabbix import killer - YAML parsers silently merge, but Zabbix rejects.
Uses regex-based scanning since yaml.safe_load silently handles duplicates.
Returns list of errors.
Note: Only checks for duplicates at template-level (indent 4) since nested
keys like 'triggers:' can legitimately appear multiple times in different
item contexts.
"""
errors = []
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
except Exception as e:
errors.append(f"[FILE ERROR] Could not read file: {e}")
return errors
# Track keys at template level (indent 4) only
# Key: key_name -> list of line numbers
template_level_keys = {}
# Critical keys that should never be duplicated at template level
critical_keys = {'macros', 'items', 'discovery_rules', 'dashboards',
'graphs', 'valuemaps', 'value_maps'}
for line_num, line in enumerate(lines, 1):
# Skip comments and empty lines
stripped = line.lstrip()
if not stripped or stripped.startswith('#'):
continue
# Calculate indentation (spaces before content)
indent = len(line) - len(line.lstrip())
# Only check template-level keys (indent 4 for " macros:")
if indent != 4:
continue
# Match YAML key pattern: "key:" or "key: value"
import re
match = re.match(r'^(\s*)([a-zA-Z_][a-zA-Z0-9_]*):', line)
if match:
key_name = match.group(2)
# Only track critical keys
if key_name in critical_keys:
if key_name not in template_level_keys:
template_level_keys[key_name] = []
template_level_keys[key_name].append(line_num)
# Report duplicates
for key_name, line_numbers in template_level_keys.items():
if len(line_numbers) > 1:
lines_str = ', '.join(map(str, line_numbers))
errors.append(f"[DUPLICATE KEY] '{key_name}:' appears {len(line_numbers)} times at template level (lines: {lines_str})")
return errors
def validate_yaml(file_path): def validate_yaml(file_path):
print(f"Validating {file_path}...") print(f"Validating {file_path}...")
print("=" * 60) print("=" * 60)
# ========== 0. Check for duplicate YAML keys (pre-parse) ==========
print("\n[0/5] Checking for duplicate YAML keys...")
duplicate_key_errors = check_duplicate_yaml_keys(file_path)
if duplicate_key_errors:
print(f" ❌ Found {len(duplicate_key_errors)} duplicate key issues")
for err in duplicate_key_errors:
print(f"{err}")
return False
else:
print(" ✅ No duplicate YAML keys detected")
try: try:
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
content = yaml.safe_load(f) content = yaml.safe_load(f)