implant alerts

Envia avisos de eventos graves (ban permanente, seção do threat-status em crit, lockdown, fallback do firewall no boot, feed recusado…) para um Telegram ou um webhook HTTPS. Quem configura é o operador do próprio servidor: canais e eventos no config.toml, credenciais num arquivo separado, 0600 root, que nunca aparece em log, saída ou JSON (v2.61.0+).

Uso

implant alerts <SUBCOMANDO> [OPTIONS]
# canal Telegram: o token entra pelo stdin (ou --token-env VAR), nunca em argumento
sudo implant alerts channel add telegram --name ops --chat-id -1001234567890 --token-stdin

# canal webhook: POST HTTPS com corpo JSON; a URL é credencial (stdin ou variável)
sudo implant alerts channel add webhook --name siem --url-env SIEM_URL [--auth-env SIEM_AUTH]

sudo implant alerts subscribe --channel ops --on ban_permanent,threat_crit   # sem --on: todos
sudo implant alerts test --channel ops       # mensagem de teste
implant alerts status                        # config, credenciais (sem valores), últimos envios
implant alerts channel list
sudo implant alerts unsubscribe --channel ops --on threat_crit   # sem --on: todas do canal
sudo implant alerts channel remove ops       # leva credenciais e subscrições junto

Subcomandos

Subcomando O que faz
status Config efetiva, estado das credenciais de cada canal (sem valores) e os últimos envios/falhas
channel add telegram --name, --chat-id e o token por --token-stdin ou --token-env <VAR>
channel add webhook --name, URL por --url-stdin ou --url-env <VAR>, header Authorization opcional por --auth-env <VAR>
channel remove <nome> Remove o canal, as credenciais dele e as subscrições que apontam para ele
channel list Canais, tipo, chat_id, situação das credenciais e eventos assinados
subscribe --channel e --on <evento,…> (sem --on: todos); --template opcional
unsubscribe --channel e --on (sem --on: todas as subscrições do canal)
test --channel <nome> Envia uma mensagem de teste; sai ≠ 0 se o envio falhou
  • channel add com um nome que já existe recusa; --replace troca o canal e as credenciais (rotação de token).
  • Credencial nunca entra por argumento de linha de comando (ficaria no histórico do shell e na lista de processos).
  • --dry-run vale em todos: valida e mostra o que faria, sem gravar nem enviar.
  • A edição do config.toml preserva comentários e o resto do arquivo.

Onde fica cada coisa

O quê Onde Permissão
Canais e subscrições [alerts] do config.toml (/etc/protocolo-implant/config.toml) a do arquivo
Credenciais (bot_token, url, authorization) alerts.secrets, ao lado do config.toml 0600, dono root
Dedupe e teto por hora /var/lib/implant/alerts-state.json 0600
Registro de envios e falhas /var/lib/implant/alerts-events.jsonl —

O alerts.secrets só é lido se for arquivo regular (não symlink), do usuário que roda o implant e sem nenhum acesso para grupo/outros. Fora disso nada é lido nem enviado, e a mensagem diz o que corrigir (chmod 600 / chown root:root). Credencial posta no config.toml (bot_token, url…) é recusada com a indicação do lugar certo. Um [alerts] inválido desliga só os alertas: os outros comandos não leem a seção.

Configuração

[alerts]
enabled = true                 # false silencia sem apagar nada
timeout_seconds = 5            # prazo de cada envio (1..30)
dedupe_window_seconds = 3600   # mesmo evento + mesmo alvo não repete nesta janela
max_per_hour = 20              # teto de alertas por hora

[alerts.channels.ops]
type = "telegram"
chat_id = "-1001234567890"

[alerts.channels.siem]
type = "webhook"

[[alerts.notify]]
on = ["ban_permanent", "threat_crit"]
channel = "ops"
template = "[{{::severity}}] {{::hostname}}: {{::title}}\n{{::detail}}"   # opcional

O channel add e o subscribe escrevem essas seções; editar à mão também vale.

Eventos

Evento Quando Alvo (target)
ban_permanent O auto-ban aplica um ban permanente o IP
threat_crit Uma seção do threat-status entra em crit (só com gravação do snapshot, isto é, sem --no-record) a seção
lockdown_created lockdown create ou ssh-service disable o serviço
emergency_mode_enabled emergency-mode enable desligou alguma feature as features
firewall_boot_fallback Sem confirmação após o boot, o firewall-restore esvaziou os sets de bloqueio boot
firewall_install_reverted Instalação do firewall-restore desfeita por falta de confirmação install
feed_kept_previous O feed-block recusou o conteúdo novo e manteve o anterior feed-block
traefik_redeploy_failed O redeploy do Traefik pedido pelo traefik-accesslog --enable ao orquestrador falhou (warn com volta ao anterior, crit sem volta) o Traefik
feed_refused O feed-block recusou o feed sem conteúdo anterior (nada aplicado) feed-block

O threat_crit só sai quando alguém roda implant threat-status (o comando não tem timer próprio): quem consulta o estado periodicamente dispara o alerta.

Template

Variáveis: {{::hostname}} ([server] hostname do config, ou o do sistema), {{::event}}, {{::target}}, {{::severity}} (info, warn, crit), {{::title}}, {{::detail}} e {{::ts}} (RFC 3339). Variável desconhecida é recusada ao gravar. Padrão: [{{::severity}}] {{::hostname}}: {{::title}} e o detail na linha de baixo. Texto cortado em 3900 caracteres.

Webhook

POST na URL do alerts.secrets (só https://, sem seguir redirect), Content-Type: application/json, header Authorization se configurado. Corpo:

{
  "event": "ban_permanent",
  "target": "203.0.113.7",
  "severity": "crit",
  "hostname": "web-01",
  "title": "Ban permanente aplicado",
  "detail": "…",
  "ts": "2026-10-18T12:00:00Z",
  "text": "[crit] web-01: Ban permanente aplicado\n…"
}

text é o template renderizado. Sucesso = HTTP 2xx.

Entrega

  • Best-effort: cada envio tem o prazo de timeout_seconds, a chamada inteira no máximo 20 s, sem retry.
  • Nunca derruba quem gerou o evento: falha (rede, HTTP de erro, credencial ausente ou com permissão aberta) vira registro alert_failed, alert_secrets_error ou alert_config_error no log local, nunca erro do comando ou do daemon.
  • Dedupe: o mesmo evento para o mesmo alvo dentro da janela, ou acima do teto por hora, vira alert_suppressed (reason: duplicate | rate_limit). Só conta o envio entregue a pelo menos um canal: um envio que falhou não segura a repetição do mesmo evento.
  • Mensagens de erro passam por um filtro que tira qualquer valor de credencial.

Saída JSON

Envelope 1.0 com -o json:

Envelope Campos
alerts.status config efetiva, canais, últimos envios
alerts.channel.list channels[]: name, type, chat_id, credentials (ok | faltando: … | não verificado), events
alerts.channel.add / .remove action, channel, dry_run, changed, config_path, secrets_path
alerts.subscribe / .unsubscribe idem
alerts.test sent, status, error, text (exit ≠ 0 se o envio falhou)

Nenhum deles traz valor de credencial.

Veja também