131 crashes de PHP-FPM em 10 minutos: como um crawler travou o servidor via systemd-coredump (não via PHP)
Publicado em 27 de abril de 2026
O alerta de CPU alto que não era ataque de CPU
O Grafana disparou alerta de CPU alto em um WordPress publisher. Conectei ao servidor e rodei o diagnóstico inicial.
ps aux --sort=-%cpu | head -12O output mostrou 7 processos `systemd-coredump` no topo, consumindo coletivamente 47% de CPU. Não havia processos PHP-FPM em CPU alto. Não havia processos nginx anormais. O ataque de CPU era o handler de crash, não o PHP.
Quando você vê CPU alto em systemd-coredump, o incidente já aconteceu. O systemd está comprimindo os restos do que quebrou. A pergunta real é: o que gerou 131 core dumps em 10 minutos?
Os 131 core dumps e os 4 GB de disco
A verificação do diretório de core dumps confirmou a dimensão do problema:
ls -lh /var/lib/systemd/coredump/ | grep 'php-fpm' | wc -l
# 131
du -sh /var/lib/systemd/coredump/
# 4.0G
ls -lh /var/lib/systemd/coredump/ | grep 'php-fpm' | head -5
# core.php-fpm.33.abc1234.1234567890.xz 31M
# core.php-fpm.33.abc1235.1234567891.xz 31M
# core.php-fpm.33.abc1236.1234567892.xz 31M131 arquivos de core dump, cada um com aproximadamente 31 MB após compressão pelo systemd-coredump, somando 4 GB. Todos com UID 33 — que é o `www-data`, o usuário dos workers PHP-FPM. O disco havia saltado de 39% para 50% de uso em menos de 10 minutos.
systemd-coredump comprime cada core dump em background usando lz4 ou xz. Com 131 crashes simultâneos, havia 7 processos de compressão rodando em paralelo, consumindo praticamente metade da capacidade de CPU do servidor enquanto tentavam processar a fila de dumps.
A causa raiz: crawler do range <crawler-range>/24
A investigação no nginx error.log revelou o padrão:
grep '<crawler-prefix>' /var/log/nginx/error.log | head -10
# 2026/03/03 14:20:15 [error] upstream timed out (110) GET /post-sobre-caes client: <crawler-IP-1>
# 2026/03/03 14:20:17 [error] upstream timed out (110) GET /outro-post client: <crawler-IP-1>
# 2026/03/03 14:20:19 [error] upstream timed out (110) GET /tag/animais client: <crawler-IP-1>
# ...95 entradas de timeout do mesmo range
# 2026/03/03 14:33:04 [error] ModSecurity: Access denied 403 GET /.env client: <crawler-IP-2>A partir das 14:20, um crawler do range `<crawler-range>/24` iniciou varredura paralela contra posts, páginas de tag (`/tag/`) e páginas de categoria (`/categoria/`) do WordPress. O site usava GoCache CDN, mas esse range de URLs — tags e categorias — não estava com cache quente. Cada requisição chegou ao PHP-FPM sem cache.
Por que o PHP-FPM crashou
O WordPress Publisher tinha um servidor de 8 vCPUs com PHP-FPM configurado com múltiplos workers. Sob bombardeio de requisições paralelas a páginas sem cache, os workers começaram a colidir — múltiplos workers tentando gerar a mesma página de tag com conteúdo pesado simultaneamente, esgotando memória antes de finalizar. Um worker PHP-FPM que esgota `memory_limit` gera SIGSEGV e produz um core dump.
A tentativa de acesso ao `/.env` às 14:33 confirmou o perfil do crawler: não era um bot de indexação legítimo. Era varredura de reconhecimento com tentativa de exposição de credenciais.
O loop de degradação
A sequência que levou ao load 15.18 em um servidor de 8 vCPUs:
14:20 — Crawler inicia requisições paralelas a /tag/ e /categoria/
14:20-14:30 — Workers PHP-FPM esgotam memória e crasham (131 ocorrências)
14:20+ — systemd-coredump gera 131 arquivos de ~31 MB cada = 4 GB no disco
14:30 — Load atinge 15.18 (7x acima do normal de ~0.5 para este servidor)
14:31 — Crawler recua / workers param de crashar
14:38 — Load cai para 0.82
14:44 — Load normalizado: 0.02
As ações de resposta
1. Limpar os core dumps (prioridade: liberar CPU e disco)
Com 7 processos de compressão ainda rodando, a primeira ação foi interromper o ciclo e liberar os recursos:
# Remover todos os core dumps do php-fpm
sudo rm -f /var/lib/systemd/coredump/core.php-fpm.*
# Resultado:
# Disco: 50% → 39%
# CPU systemd-coredump: 47% → 0%
# Load começou a cair imediatamente2. Banir o range do crawler
# Bloquear o range agressor via iptables
sudo iptables -I INPUT -s <CRAWLER_CIDR> -j DROP -m comment --comment 'crawler-ban: php-fpm crash 2026-03-03'
# Verificar que a regra foi aplicada
sudo iptables -L INPUT -n | grep '<crawler-prefix>'O servidor normalizou completamente em 6 minutos após a limpeza dos dumps e o ban do range. Load de 15.18 para 0.02. PHP-FPM com 10 workers saudáveis. Site respondendo em 0,32s.
O diagnóstico correto vs o diagnóstico inicial
O alerta foi 'CPU alto'. O diagnóstico inicial natural seria: ataque volumétrico, processo travado, PHP consumindo CPU. Nenhum desses estava certo.
# Estado dos processos durante o incidente (reconstituído via logs)
# top 5 por CPU no pico (14:30 BRT):
#
# PID USER %CPU COMMAND
# 12341 systemd 9.2 systemd-coredump ← compressão dump 1
# 12342 systemd 8.8 systemd-coredump ← compressão dump 2
# 12343 systemd 8.7 systemd-coredump ← compressão dump 3
# 12344 systemd 7.9 systemd-coredump ← compressão dump 4
# 12345 systemd 7.4 systemd-coredump ← compressão dump 5
# ...
# Nenhum processo php-fpm em CPU alto — porque eles já tinham crashadoA CPU alta estava no handler de crash, não no PHP. Esse é um padrão de misdiagnóstico comum: o systemd-coredump processa os dumps em background e aparece no topo do ps/top como se fosse o atacante, quando na verdade é o serviço de limpeza. O atacante já terminou o trabalho dele.
Por que o cache frio em /tag/ e /categoria/ foi decisivo
O GoCache CDN estava configurado em modo forward para essas URLs. Em condições normais, as páginas de tag e categoria são acessadas organicamente por poucos usuários e o cache fica quente. Um crawler que acessa centenas de URLs únicas de tag em paralelo não vai encontrar cache — cada URL é nova para o CDN.
Com cache frio, cada requisição chegou ao PHP-FPM. Uma página de tag em WordPress pode ser pesada — múltiplas queries ao banco (posts da tag, sidebar, relacionados), PHP renderizando template completo. Sob 50+ requisições paralelas a tags diferentes, a pressão de memória nos workers é proporcional ao número de requisições simultâneas.
A combinação de crawler agressivo + cache frio + systemd-coredump habilitado cria uma falha silenciosa que parece ataque de CPU mas é cascade de crash. O servidor não está sobrecarregado por tráfego — ele está sobrecarregado limpando os rastros dos crashes.
Proteções que faltavam e o que implementar
Este servidor tinha ModSecurity ativo (o acesso ao /.env foi bloqueado com 403), mas as seguintes proteções estavam ausentes ou desabilitadas:
Bot mitigation no GoCache: status false. Com bot mitigation ativo, o crawler teria sido bloqueado na borda antes de chegar ao origin.
Rate limit no GoCache: status false. Throttling de requisições por IP na borda é a primeira linha de defesa contra crawlers agressivos.
CrowdSec bouncer: container Docker rodando, mas bouncer do host inativo. O CrowdSec detecta o padrão de varredura e bane automaticamente — sem bouncer ativo, a detecção não gera bloqueio.
pm.max_children do PHP-FPM: sem limite configurado para evitar que um crash em loop esgote recursos. Configurar SystemMaxUse no coredump.conf também limita o impacto em disco.
# Limitar o tamanho total de core dumps no systemd
# /etc/systemd/coredump.conf
[Coredump]
Storage=external
Compress=yes
ProcessSizeMax=2G
ExternalSizeMax=2G
MaxUse=1G # máx 1 GB total em /var/lib/systemd/coredump/
KeepFree=1G # manter pelo menos 1 GB livre no filesystem
# Aplicar sem reboot:
sudo systemctl daemon-reloadCom `MaxUse=1G`, o systemd-coredump descarta dumps antigos automaticamente quando o limite é atingido — evitando que um ataque de 131 crashes encha o disco e prolongue a crise com CPU de compressão.
Estado final e lição
Seis minutos após identificar a causa real, o servidor estava normalizado. A sequência:
rm -f /var/lib/systemd/coredump/core.php-fpm.* — 4 GB liberados, CPU normalizada
iptables -I INPUT -s <crawler-range>/24 -j DROP — crawler banido
O servidor voltou ao estado anterior ao incidente sem nenhum reinício necessário. Os workers PHP-FPM saudáveis que não tinham crashado continuaram servindo tráfego normalmente durante todo o processo de resposta.
Quando o ps/top mostra systemd-coredump no topo com CPU alto, não tente matar o systemd-coredump. Identifique o processo que crashou (o UID do dump aponta para o usuário), descubra o que causou o crash, e só então limpe os dumps. Matar o handler sem entender a causa deixa o disco lotado e o incidente sem diagnóstico.
