Toolkit de segurança para VPS Ubuntu (22.04/24.04), em Bash puro + jq + curl + systemd.
Faz auditoria, hardening guiado e monitoramento em tempo real com alertas
enviados para um webhook do n8n.
Três pilares:
- audit — somente leitura; ~40 checks de configuração com score e relatório (texto + JSON).
- harden — aplica correções sob comando, com backup e rollback; mudanças arriscadas (SSH/UFW) exigem confirmação e têm proteção anti-lockout.
- monitor — daemon que segue o
journalde varre o estado do host, alertando logins SSH, brute force, novos usuários/portas/containers, UFW desativado e alteração de arquivos críticos.
curl -fsSL https://raw.githubusercontent.com/berodcdev/vps-sec/main/install.sh \
| sudo bash -s -- --webhook-url "https://SEU-N8N/webhook/SEU-ID"Um comando, idempotente (re-rodar = upgrade). Funciona em host Docker Compose ou Swarm. Detalhes, flags e a atualização automática logo abaixo.
One-line (repositório público no GitHub) — troque berodcdev/vps-sec:
curl -fsSL https://raw.githubusercontent.com/berodcdev/vps-sec/main/install.sh \
| sudo bash -s -- --webhook-url "https://SEU-N8N/webhook/SEU-ID"A URL do webhook é sempre fornecida no momento da instalação (nunca fica no código).
Se você omitir --webhook-url e rodar num terminal interativo, o instalador pergunta.
O install.sh detecta que roda via pipe e baixa o tarball do próprio repositório.
Você pode fixar o repo/branch por flag (--repo usuario/repo --branch main) ou por
variável de ambiente (VPS_SEC_REPO, VPS_SEC_BRANCH).
A partir de um clone local (para desenvolvimento):
git clone https://github.com/berodcdev/vps-sec.git
sudo ./vps-sec/install.sh --webhook-url "https://..."O instalador é idempotente (re-rodar = upgrade). Ele instala em /opt/vps-sec, cria
/etc/vps-sec/config (chmod 600), habilita os serviços systemd, cria os baselines,
testa o webhook e roda a primeira auditoria.
Para atualizar depois: sudo vps-sec self-update (rebaixa a última versão do
repositório e reinstala, preservando sua config — inclui reiniciar o monitor).
Atualização automática: o instalador habilita o timer vps-sec-update.timer,
que roda self-update 1×/dia (04:00, com jitter). Ou seja, depois de instalado,
cada host se atualiza sozinho a partir do repositório — corrigir algo passa a ser
só publicar no main. Confira com systemctl list-timers vps-sec-update.timer.
Requisitos: Ubuntu/Debian com systemd. Dependências (jq, curl) são instaladas
automaticamente. Nada de firewall é instalado ou alterado na instalação — UFW e
fail2ban são apenas reportados pelo audit e aplicados via harden.
vps-sec audit # auditar agora (exit: 0 limpo · 1 findings · 2 crítico)
vps-sec audit --json # saída JSON (para automação)
vps-sec harden --dry-run # ver o que seria corrigido
vps-sec harden --yes # aplicar só as correções SEGURAS, sem perguntar
vps-sec harden --allow-risky # habilita perguntar as correções ARRISCADAS
vps-sec harden --only SSH-001 # aplicar o fix de um check específico
vps-sec harden --confirm-ssh # confirmar mudança de SSH (cancela o rollback automático)
vps-sec rollback --list # listar backups
vps-sec rollback last # reverter o último harden
vps-sec monitor status # estado do monitor
vps-sec monitor log # últimos eventos
vps-sec baseline update # atualizar os baselines (após mudanças legítimas)
vps-sec attackers # histórico de IPs que tentaram força bruta
vps-sec attackers --json --top 50 # o mesmo em JSON, para automação
vps-sec test-webhook # enviar evento de teste ao n8n
vps-sec digest --now # gerar/enviar o resumo diário./tests/run.sh # shellcheck + suítes + contratos do audit (precisa de Docker)As suítes rodam num container ubuntu:24.04 porque precisam de bash 5 e utilitários
GNU — o bash 3.2 do macOS não executa o monitor. É o gate antes de qualquer commit.
SAFE (aplicáveis com --yes, reversíveis, não derrubam acesso):
fail2ban + jail sshd · unattended-upgrades · sysctl endurecido · permissões de
authorized_keys/shadow/docker.sock · limites de log no daemon.json ·
endurecimento leve do SSH (drop-in) · travar contas sem senha.
RISKY (exigem digitar CONFIRMO, nunca aplicadas só com --yes):
PermitRootLogin no · PasswordAuthentication no · ufw enable · política default do UFW.
Ao mudar SSH, o vps-sec: faz backup → grava num drop-in sshd_config.d/99-vps-sec.conf
→ valida com sshd -t → agenda um rollback automático em 5 minutos → faz reload
(não derruba a sessão atual) → e instrui você a testar em um novo terminal.
Se você conseguir logar, rode vps-sec harden --confirm-ssh para cancelar o rollback.
Se não confirmar em 5 minutos, a mudança é revertida sozinha e um alerta
harden_rollback é enviado. Antes de ufw enable, a porta do SSH é sempre liberada.
O Docker insere regras direto no iptables e ignora o UFW por padrão. Uma porta
publicada em 0.0.0.0 fica exposta mesmo com UFW "ativo". O audit detecta isso
(DKR-001). A correção estrutural recomendada é publicar serviços internos em
127.0.0.1:porta atrás de um reverse proxy. O vps-sec não usa "iptables": false
nem scripts de terceiros (causas comuns de "o Docker parou depois do hardening").
Além da infra, o audit inspeciona os serviços do stack via docker inspect/exec
(sem precisar das suas credenciais) e reporta riscos comuns — tudo advisory
(sem correção automática, pois moram em compose/stacks gerenciadas fora da ferramenta):
- Redis (
RDS-*): sem senha (crítico se a porta 6379 estiver exposta), 6379 publicada no host, protected-mode off. - Postgres (
PG-*):POSTGRES_HOST_AUTH_METHOD=trust, senha padrão/fraca, 5432 publicada no host, imagem:latest. Senhas nunca são impressas. - Traefik (
TRF-*):api.insecure=true(dashboard sem auth), 8080 exposto,exposedByDefaultnão desligado, docker.sock montado read-write (prefira:ro). - n8n (
N8N-*):N8N_ENCRYPTION_KEYnão fixado, sem autenticação,N8N_SECURE_COOKIE=false. - Portainer (
PTR-*): 9000 (HTTP) publicado direto em vez de atrás do Traefik.
Configure BACKUP_WATCH no config com os caminhos dos seus backups e a idade máxima
aceitável (caminho:dias). O audit alerta (BKP-001) se um backup está velho ou
ausente. Se você tem volumes de dados (Postgres/n8n) mas não configurou BACKUP_WATCH,
o audit avisa (BKP-002).
BACKUP_WATCH="/var/backups/pg:2 /var/backups/n8n:2"O monitor detecta e alerta no n8n quando um container do baseline cai
(container_down), fica unhealthy (container_unhealthy) ou entra em loop de
reinício (container_restart_loop) — útil para saber na hora se o Postgres ou o n8n
parou. Se o daemon do Docker morrer (docker_daemon_down) ou todos os serviços
sumirem de uma vez (containers_all_down), você recebe um alerta crítico agregado em
vez de silêncio ou de uma enxurrada. Ambos exigem três varreduras seguidas, para um
systemctl restart docker de rotina não virar alarme.
A identidade de cada serviço é estável entre deploys: usa a label do Compose
(projeto/serviço) ou do Swarm (com.docker.swarm.service.name), caindo para o nome
do container só quando não há nenhuma. Assim, recriar/reagendar um container (deploy,
task do Swarm) não gera falso container_down/new_docker_container.
O serviço vps-sec-monitor roda dois loops:
- journald em tempo real (
journalctl -f --cursor-file, sobrevive a rotação e restart): login SSH, brute force (agregado por janela), novo usuário, falha de sudo. - state-scan (a cada 60s): nova porta em escuta, novo container, UFW desativado, integridade (sha256) de arquivos críticos.
Anti-flood: alertas repetidos da mesma chave são deduplicados por cooldown
(15 min) e há um teto global por hora — ao estourar, um único alert_storm é enviado.
Brute force nunca vira enxurrada: já é agregado em um alerta com a contagem.
Uma novidade = um alerta. Subiu um container, instalou o Tailscale, abriu uma
porta? Você recebe um aviso e o monitor absorve a mudança no baseline — nada de
repetição a cada 15 minutos até você atualizar o baseline na mão. O resumo do que
mudou nas últimas 24h vai no digest diário (state_changes_24h). Para o
comportamento antigo (insistir até confirmação manual), use MONITOR_AUTOLEARN="no".
Brute force não vira enxurrada. Um host com a 22 aberta é varrido por botnets sem
parar — alertar um por IP seria uma torneira aberta, e pior: encheria o teto por hora e
faria alertas de verdade serem descartados. As falhas são consolidadas em um alerta por
hora, com o total, quantos IPs distintos, quantos cruzaram o limiar e o top 5. Se o
volume passar de 3× o que aquele host costuma ver, sobe para critical.
O alerta ainda informa o estado do fail2ban e quantos IPs daquela janela estão
efetivamente banidos. Se o fail2ban estiver ausente, parado ou com a jail caída, a
severidade sobe para critical e a mensagem afirma o problema — em vez de pedir
"confirme que está ativo", já que o agente sabe a resposta.
Todo IP atacante vai para um histórico permanente (vps-sec attackers), com
primeira aparição, última aparição, total de tentativas e quantas vezes foi banido.
Os dez maiores reincidentes acompanham o digest diário.
A exceção que nunca é agregada nem silenciada: ssh_login_after_burst. Quando um IP
que estava tentando força bruta consegue autenticar, o alerta sai como critical, sem
passar por filtro, dedup ou teto. É o único sinal que o anti-flood jamais pode engolir.
Uma porta só é reportada depois de aparecer em duas varreduras seguidas. Isso filtra
os sockets UDP de saída (DNS, NTP, STUN do Tailscale), que o ss mostra como se fossem
listeners porque UDP não tem estado de conexão — eles vivem segundos e usam porta
aleatória, então antes geravam um alerta cada. Um listener de verdade permanece e é
reportado no ciclo seguinte.
A severidade de uma porta nova acompanha a exposição real: 0.0.0.0/[::] é alto,
um bind na rede do Tailscale/Docker é médio, e loopback é info (não notifica no
default). Sockets dual-stack contam como um só — 0.0.0.0:8080 e [::]:8080 são o
mesmo serviço, não dois alertas.
Se o webhook estiver fora do ar, os alertas vão para um spool em disco e são reenviados depois. O digest diário (08:00) também serve de heartbeat: se um host parar de enviá-lo, o n8n pode alertar que o host/agente caiu.
{
"schema_version": 1,
"event_type": "ssh_login_success",
"severity": "info",
"hostname": "vps-n8n-01",
"timestamp": "2026-07-21T15:04:05Z",
"agent_version": "0.1.0",
"event_id": "abc123...",
"details": { "user": "deploy", "ip": "203.0.113.7", "method": "publickey" },
"suggested_action": "Se não reconhece este acesso, rotacione chaves e revise o host",
"suppressed_since_last": 0
}event_type possíveis: ssh_login_success, ssh_auth_burst, new_user,
user_deleted, sudo_auth_failure, ssh_login_after_burst, file_integrity, new_listening_port, new_docker_container,
container_down, container_unhealthy, container_restart_loop,
ufw_disabled, audit_finding, harden_applied, harden_rollback, alert_storm,
digest, agent_start, test.
Os findings de aplicação (RDS-*, PG-*, TRF-*, N8N-*, PTR-*) e de backup
(BKP-*) não têm event_type próprio: quando são critical/high, viajam dentro de
audit_finding.details.check_id.
- Webhook (POST) → recebe o JSON.
- Switch por
event_type(ou IF porseverity). - Roteie:
critical/high→ notificação imediata (Telegram/Discord/email);digest→ uma mensagem-resumo por host; demais → log/planilha. - Heartbeat: um workflow com Schedule diário que verifica se cada host
enviou o
digestnas últimas ~26h; se faltar, alerta "host silencioso".
| Caminho | Conteúdo |
|---|---|
/opt/vps-sec/ |
código |
/etc/vps-sec/config |
configuração (chmod 600, root) |
/var/lib/vps-sec/ |
baselines, spool de alertas, dedup, cursor do journal |
/var/log/vps-sec/ |
relatórios de audit, log do monitor |
/var/backups/vps-sec/<ts>/ |
backups do harden (retenção: 20) |
sudo vps-sec uninstall # remove código e serviços; preserva config/logs/backups
sudo /opt/vps-sec/uninstall.sh --purge # remove tudo (mantém só os backups)O hardening já aplicado não é revertido pela desinstalação. Reverta antes com
vps-sec rollback last se necessário.
- Config lida como root só se pertencer a root e não for gravável por outros.
- URL do webhook enviada via stdin do
curl(invisível emps), nunca logada. - Linhas de log são tratadas como dados (sem
eval); payloads montados comjq --arg. - Serviço systemd com
NoNewPrivileges,ProtectHome,PrivateTmp.