Resumo do projeto
Este projeto transforma o meu servidor doméstico num pequeno SOC: o Splunk recebe os logs do Linux
(auth.log, syslog, UFW, Apache, MySQL), deteta tentativas de força bruta em SSH
e responde automaticamente com bloqueio no ufw, registando tudo para auditoria.
O foco está em três pilares: visibilidade (dashboards), deteção (SPL + Threat Intel) e reação (scripts e alertas).
Arquitetura resumida
- Inputs:
linux_secure(SSH),syslog,access_combined(Apache),mysql_error_log. - Dashboards:
- Main Dashboard: panorama de ataques (SSH/HTTP), IPs banidos, Threat Intel.
- Centro Avançado: saúde do Splunk, erros MySQL, top sourcetypes, auditoria.
- Resposta automática:
alerta de brute force → lookup → script
ban_ip.sh→ regra UFW + log no syslog. - Threat Intel:
lookup
threatintel_by_ip.csvcruza IPs externos com feeds maliciosos.
Deteção de força bruta SSH (SPL do alerta)
Pesquisa guardada que alimenta a “caixa-forte” de IPs a banir:
| search index=* sourcetype=linux_secure "sshd" (Failed OR failure OR "Invalid user")
| rex field=_raw "from (?<src_ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
| where isnotnull(src_ip)
| streamstats time_window=1m count as consecutive_failures by src_ip
| where consecutive_failures >= 5
| fields src_ip
| dedup src_ip
| outputlookup ips_a_banir.csv
Esta pesquisa corre a cada minuto como um alerta agendado. Em vez de chamar o script diretamente
com parâmetros frágeis, escreve os IPs num lookup (ips_a_banir.csv) que serve de caixa-forte.
BAN automático via UFW (ban_ip.sh)
Script chamado pelo alerta Run a script, que lê a caixa-forte, aplica whitelist, evita duplicados e regista o histórico:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
DEBUG_LOG="/opt/splunk/var/log/splunk/ban_script.log"
LOOKUP_FILE="/opt/splunk/etc/apps/search/lookups/ips_a_banir.csv"
WHITELIST="/opt/splunk/etc/apps/search/lookups/ip_whitelist.csv"
HISTORY="/opt/splunk/etc/apps/search/lookups/banned_history.csv"
UFW="/usr/sbin/ufw"
LOGGER="/usr/bin/logger"
exec 2>>"$DEBUG_LOG"
echo "----------------------------------------------------" >> "$DEBUG_LOG"
echo "$(date): Início ban_ip.sh. Lendo: $LOOKUP_FILE" >> "$DEBUG_LOG"
mapfile -t WL < <(awk -F',' 'NR>1{print $1}' "$WHITELIST" | tr -d '\r"')
awk -F',' 'NR>1{print $1}' "$LOOKUP_FILE" | tr -d '\r"' | while read -r IP; do
IP="$(echo "$IP" | xargs)"
[[ -z "${IP:-}" ]] && continue
[[ ! "$IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && {
echo "$(date): Ignorado (IPv4 inválido): '$IP'" >> "$DEBUG_LOG"; continue; }
for W in "${WL[@]:-}"; do
if [[ "$IP" == "$W" ]]; then
echo "$(date): Ignorado (whitelist): $IP" >> "$DEBUG_LOG"
continue 2
fi
done
if /usr/bin/sudo $UFW status numbered | grep -qE "DENY IN +$IP"; then
echo "$(date): Já está banido: $IP" >> "$DEBUG_LOG"
continue
fi
echo "$(date): A banir $IP via ufw..." >> "$DEBUG_LOG"
/usr/bin/sudo $UFW insert 1 deny from "$IP" to any comment 'Splunk-Auto-Ban-SSH-Brute-Force' || {
echo "$(date): Falha ao banir $IP" >> "$DEBUG_LOG"
continue
}
$LOGGER -t BAN_SCRIPT "action=ip_banned ip=$IP reason=ssh_bruteforce"
echo "$(date): $IP banido." >> "$DEBUG_LOG"
echo "$(date +%F\ %T),$IP,ban,ban_script" >> "$HISTORY"
done
printf 'src_ip\n' > "$LOOKUP_FILE"
chown splunk:splunk "$LOOKUP_FILE" "$HISTORY"
echo "$(date): Fim ban_ip.sh. Lookup limpo." >> "$DEBUG_LOG"
echo "----------------------------------------------------" >> "$DEBUG_LOG"
exit 0
A whitelist (ip_whitelist.csv) protege IPs de administração; o
banned_history.csv guarda um log simples para auditoria.
Configuração de sudoers (segura)
Permite ao utilizador splunk chamar apenas o que o script precisa, sem senha:
# /etc/sudoers.d/splunk_ufw
splunk ALL=(root) NOPASSWD:/usr/sbin/ufw insert 1 deny from * to any comment Splunk-Auto-Ban-SSH-Brute-Force
splunk ALL=(root) NOPASSWD:/usr/sbin/ufw status numbered
Score de risco + Threat Intel (SPL)
Pesquisa utilizada no painel “IPs Banidos – Geo + Reputação + Score”:
| multisearch
[ search index=* sourcetype=ban_script
| rex max_match=1 "(?<banned_ip>\d{1,3}(?:\.\d{1,3}){3})"
| eval via="ban_script" ]
[ search index=* sourcetype=syslog BAN_SCRIPT
| rex max_match=1 "ip=(?<banned_ip>\d{1,3}(?:\.\d{1,3}){3})"
| eval via="syslog" ]
| where isnotnull(banned_ip)
| stats latest(_time) as when values(via) as via by banned_ip
| iplocation banned_ip
| eval country=coalesce(Country,"Unknown"), city=coalesce(City,"")
| lookup threatintel_by_ip ip as banned_ip OUTPUTNEW ti_desc ti_feed threat_key
| eval rep_flag = if(isnotnull(ti_feed),1,0)
| eval geo_flag = if(country!="PT",1,0)
| eval base=30, rep=rep_flag*50, geo=geo_flag*10
| eval score=base+rep+geo
| convert ctime(when) as when timeformat="%Y-%m-%d %H:%M:%S"
| sort - score - when
| table when banned_ip country city ti_feed ti_desc threat_key via score
Com isto, consigo priorizar IPs banidos que também aparecem em feeds de reputação e que vêm de países inesperados.