Buzeli
buzeliSoluções Digitais
Segurança

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.

Copiar
ps aux --sort=-%cpu | head -12

O 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:

Copiar
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  31M

131 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:

Copiar
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:

Copiar
# 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 imediatamente

2. Banir o range do crawler

Copiar
# 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.

Copiar
# 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 crashado

A 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.

Copiar
# 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-reload

Com `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.