Primeiro, confirme que é mesmo um ataque
O erro mais caro durante uma indisponibilidade é tratar a coisa errada. "O site está lento" é um sintoma compartilhado por uma inundação de verdade, uma implantação que subiu uma consulta sem índice, um cron job que começou a despejar o banco de dados no mesmo minuto de cada hora, um crawler que descobriu a sua busca facetada, um link que chegou à página inicial de algum lugar grande, e um disco que encheu. Cada uma dessas situações parece idêntica vista de um navegador, e as respostas são mutuamente exclusivas: você não quer estar limitando a taxa de clientes de verdade porque uma migração esqueceu um índice.
Três perguntas separam essas hipóteses em menos de um minuto. O volume de tráfego está genuinamente anormal, ou o tráfego está normal e o servidor é que está lento? Uma inundação aparece como uma mudança abrupta em pacotes ou requisições por segundo; uma implantação ruim aparece como volume de requisições normal e um tempo de resposta que desabou. Alguma coisa mudou do seu lado na última hora? Confira o log de implantação antes do firewall — incidentes autoinfligidos superam ataques em uma margem enorme em infraestrutura pequena. A carga está espalhada pelo site inteiro, ou concentrada em um único caminho? Inundações de verdade costumam ser indiscriminadas ou mirar a página inicial; um endpoint caro sendo martelado por uma centena de clientes está mais para abuso do que para um DDoS, e tem uma correção muito mais barata.
Responda a essas perguntas, e então continue. O resto deste guia parte do princípio de que a resposta foi: o volume está anormal, nada mudou do seu lado, e a máquina está afogada.
SP·02A triagem de cinco minutos
Só existem dois modos de falha que importam, e eles exigem respostas opostas. Ou o cano está cheio — os pacotes chegam mais rápido do que o seu uplink ou o seu kernel conseguem processar, e o seu servidor está perdendo tráfego antes que qualquer parte do seu software o veja — ou o cano está bem e quem está esgotada é a aplicação, porque requisições bem formadas estão chegando mais rápido do que ela consegue responder. Confundir os dois desperdiça a hora: ajustar o nginx não faz nada contra um link saturado, e comprar mais largura de banda não faz nada contra uma inundação de requisições.
Distinga os dois comparando dois números. Olhe os contadores da interface e a divisão de CPU ao mesmo tempo. Se os bytes de rx estão travados perto da velocidade da sua porta, se os contadores de dropped ou overrun estão subindo, e se o tempo está indo para interrupções de software em vez de para a sua aplicação, a inundação está na camada 3 ou 4 e é um problema de capacidade. Se a largura de banda está normal mas o pool de workers está saturado, as conexões estão enfileirando, e o log de acesso está cheio de requisições com aparência plausível, é camada 7 e é um problema de filtragem.
Depois classifique o caso de camada 3/4 mais um passo adiante, porque os subtipos se comportam de forma diferente. Uma inundação SYN aparece como dezenas de milhares de sockets semiabertos em SYN-RECV; o kernel lida bem com isso assim que os syncookies estão ativados. Uma inundação de UDP ou de amplificação aparece como um volume de entrada enorme em portas que você nem escuta — reflexões de DNS, NTP, memcached, CLDAP — e nada que você rode ajuda, porque o estrago já está feito no momento em que os pacotes chegam à sua NIC. Uma inundação de fragmentação ou de pacotes brutos aparece como uma taxa alta de pacotes por segundo com largura de banda modesta, o que esgota a CPU em vez do link. Anote qual delas você tem antes de tocar em qualquer arquivo de configuração.
O que você não consegue corrigir de dentro da máquina
Esta é a parte que a maioria dos artigos pula, e é a parte que decide se a sua hora vai ser produtiva. Uma regra de firewall no alvo não salva um uplink saturado. O seu DROP no iptables roda na máquina na outra ponta do cano — o pacote já cruzou o link de trânsito, já consumiu a largura de banda que você está pagando, e já deslocou o pacote de um usuário de verdade. Descartá-lo localmente protege a sua aplicação de desperdiçar ciclos, o que vale alguma coisa, e não protege a sua largura de banda de absolutamente nada.
O teto honesto para um único servidor é, a grosso modo, o menor de dois números: a velocidade da porta em que ele está conectado, e os pacotes por segundo que a CPU dele consegue classificar. Uma porta de 1 Gbps fica cheia em 1 Gbps não importa quão elegante seja o seu conjunto de regras, e uma inundação modesta de pacotes pequenos pode esgotar alguns núcleos com o tratamento de interrupções bem antes de o número de largura de banda parecer alarmante. Passado esse ponto, a única coisa que ajuda é um dispositivo mais a montante que tenha mais capacidade do que o ataque e descarte o tráfego antes que ele chegue ao seu link. É isso que é scrubbing, e é por isso que toda máquina da nossa frota fica atrás de 1.5 Tbps de mitigação sempre ativa, em vez de um firewall maior.
O corolário importa da mesma forma: se você não tem proteção a montante, a resposta do seu provedor a um ataque volumétrico grande é aplicar um null-route no seu endereço, porque a alternativa é degradar todos os outros clientes naquele link. Isso não é maldade, é aritmética — e significa que o atacante vence tornando você caro, e não quebrando alguma coisa. Saber de antemão se a mitigação está incluída no seu plano ou é um extra pago que você nunca ativou é uma checagem de cinco minutos que vale a pena fazer hoje, e não durante o incidente. A nossa vem incluída em todo plano, que é o único arranjo que ajuda às 3 da manhã.
SP·04Camada 7: a inundação que parece tráfego
Uma inundação na camada de aplicação é mais difícil porque cada requisição individual é legítima. Uma inundação HTTP bem construída completa o handshake TCP, negocia o TLS, envia um GET / válido com um user agent plausível, e lê a resposta. Nenhum pacote nela está malformado. O que te mata é a aritmética: uma requisição custa quase nada para o atacante enviar e custa para você uma consulta ao banco de dados, a renderização de um template e cem milissegundos de um worker que agora não está atendendo mais ninguém.
Os sinais estão no seu próprio log de acesso, e geralmente são óbvios assim que você procura por eles, em vez de só olhar para eles. Query strings para invalidar cache — milhares de requisições para /?1234567, cada uma uma URL única que derrota todo cache que você tem — são a assinatura mais comum de todas. Uma distribuição de user-agent sem cauda longa: tráfego de verdade é uma mistura bagunçada de centenas de builds de navegador, e uma inundação costuma ser três strings repetidas um milhão de vezes, ou uma que nenhum usuário de verdade usa. Um campo de referrer idêntico em todo lugar. Requisições que pulam completamente os seus arquivos estáticos — um navegador de verdade busca o CSS, as fontes e as imagens depois do HTML; um cliente de inundação pede o HTML e vai embora. E uma distribuição de origem uniforme demais: uma botnet espalhada por dez mil endereços residenciais, cada um enviando duas requisições por segundo, parece popularidade até você notar que a taxa por endereço é suspeitosamente uniforme.
Depois existe a variante que quase não precisa de tráfego nenhum: o ataque lento. Umas poucas centenas de conexões que abrem, mandam um cabeçalho a cada vinte segundos e nunca terminam vão ocupar todos os workers que você tem enquanto o seu gráfico de largura de banda continua reto. O ajuste não é um limite de taxa — a taxa de requisições é minúscula — é timeouts agressivos de cabeçalho e corpo, e é por isso que eles aparecem já no primeiro bloco de configuração abaixo, em vez de virem como um adendo.
SP·05Quatro controles, em ordem de efeito por minuto
Sob pressão, faça primeiro a coisa de maior alavancagem. A ordem abaixo não é arbitrária; ela é, a grosso modo, decrescente em quanta carga cada item alivia por minuto da sua atenção.
- Sirva algo barato. Um micro-cache de trinta segundos na frente da sua aplicação transforma mil requisições idênticas por segundo em um acerto na origem e 999 leituras de memória. É a maior alavanca isolada contra quase toda inundação HTTP, custa um bloco de diretiva, e para tráfego anônimo é quase sempre seguro. Adicione
proxy_cache_lockpara que uma falha de cache não mande uma manada em disparada para o backend. - Limite a concorrência e encurte os timeouts.
limit_connpor endereço, mais timeouts apertados de cabeçalho, corpo e keepalive, mata ataques lentos de vez e impede que um único cliente estacione o seu pool de workers. Este é o controle que custa menos para os usuários de verdade. - Limite a taxa, com uma margem de rajada.
limit_reqcom uma rajada sensata é preciso, mas mais lento de ajustar, e é o controle que gera falsos positivos se você configurá-lo no pânico em vez de a partir da sua própria baseline. Você precisa saber a sua taxa normal de requisições por segundo por cliente antes de poder escolher um número — e é por isso que o post-mortem no final deste guia importa mais do que parece. - Bloqueie, de forma estreita e relutante. Descartar redes específicas funciona quando as origens estão concentradas e não faz nada quando não estão. Também envelhece mal: cada bloqueio que você adiciona durante um incidente é um cliente que você pode estar recusando silenciosamente daqui a três meses. Use um set com timeout para que as regras expirem sozinhas.
Repare no que não está na lista: banir endereços IP individuais manualmente, reiniciar o servidor web repetidamente, e desativar o firewall para "ver se ajuda". O primeiro é lento demais para fazer diferença contra uma origem distribuída, o segundo joga fora toda conexão quente que você tinha, e o terceiro é como um incidente vira uma invasão.
SP·06Não brigue com isso a partir da máquina que guarda os seus dados
Todo controle acima vale mais quando roda em algum lugar que não seja a máquina que guarda o seu banco de dados. Se o seu edge é um nó separado, a inundação termina em uma máquina cujo único trabalho é terminar inundações: ela faz cache, limita a taxa e descarta com o endereço real do cliente em mãos, e a origem só chega a ver o pequeno restante já filtrado, através de um túnel privado. Quando o edge cai, você o substitui em 15 min e não perde nada, porque não tem nada nele. Quando a origem cai, você tem uma indisponibilidade e uma restauração.
Essa separação também fecha o bypass que torna a maior parte da mitigação decorativa. Se a origem ainda tem um listener público, um atacante que encontre o endereço dela — através de DNS passivo, uma entrada de transparência de certificados, um registro MX ou uma pré-visualização de link — consegue mirar por trás de todo controle que você configurou e atingir a aplicação diretamente. Isso é comum o bastante para valer a pena tratar como o estado padrão de qualquer site "protegido", até prova em contrário. Construir a versão que aguenta é um guia à parte: um proxy reverso offshore sem nenhum listener público na origem.
Uma ressalva sobre o meio de um incidente: isso é arquitetura, não primeiros socorros. Levantar um edge, mudar o DNS e reconstruir um túnel enquanto está sob ataque é um trabalho de duas horas feito mal sob pressão, e só a mudança de DNS já não faz efeito antes do tempo que o seu TTL disser. Se você já tem isso pronto, use. Se não tem, passe pela hora com os controles que você já tem, e construa isso na semana calma depois — que é exatamente quando ninguém constrói.
SP·07O dia em que você é o refletor, não o alvo
Existe uma segunda versão desse incidente em que o seu servidor não é a vítima e ninguém te avisa. Um resolvedor aberto, um daemon NTP exposto, um memcached sem autenticação em uma interface pública, um respondedor SSDP ou CLDAP dentro de um contêiner — cada um desses responde a uma pequena requisição falsificada com uma resposta muito maior, direcionada a outra pessoa. Do seu lado os sintomas estão invertidos: a largura de banda de saída está alta, a de entrada está modesta, a sua aplicação está bem, e o primeiro sinal real é um aviso de abuso ou uma porta suspensa.
A checagem leva um minuto e pertence ao mesmo runbook, porque é o mesmo comando que você já rodou durante a triagem. O ss -tulpn não deve listar nada vinculado a um endereço público que você não tenha colocado ali de propósito, e serviços UDP merecem suspeita especial porque são os que amplificam. Um resolvedor recursivo precisa estar vinculado ao localhost ou a um endereço de túnel; memcached e Redis nunca podem estar acessíveis pela internet; e qualquer contêiner publicando uma porta com -p 0.0.0.0: acabou de abrir um buraco no firewall que você configurou, porque o Docker escreve as próprias regras antes das suas. Esse último detalhe surpreende as pessoas todas as vezes.
O mesmo formato cobre inundações de saída vindas de uma máquina que já foi comprometida, que é o outro motivo pelo qual um provedor de repente aplica um null-route em um endereço. Se o seu gráfico de saída está alto e a sua aplicação está ociosa, pare de ler configuração e comece a checar processos — isso é uma invasão, não um problema de capacidade, e a resposta é reconstruir a máquina a partir de um backup confiável, e não tentar filtrar o problema.
SP·08Depois que parar: o post-mortem e o kit permanente
Ataques param. Geralmente o atacante enjoa, às vezes a mitigação torna o ataque inútil, ocasionalmente era uma assinatura de booter de duração fixa que simplesmente venceu. A tentação nesse momento é deixar tudo exatamente como está e ir dormir, e é assim que um limite de taxa temporário vira um 429 permanente, esquecido e silencioso para um país inteiro nove meses depois. Gaste vinte minutos fechando o ciclo enquanto está tudo fresco na memória.
Vale a pena produzir três artefatos. Uma baseline: a sua taxa normal de requisições por segundo, a sua taxa normal por cliente, a sua largura de banda normal no pico. Sem esses números, todo limite que você definir no próximo incidente é um chute, e metade deles vai errar na direção que prejudica os clientes. Uma lista de reversão: tudo o que você mudou, com data e motivo, para que a configuração de emergência não vire silenciosamente a configuração permanente. Um runbook com quatro comandos de extensão, guardado em algum lugar que você consegue acessar quando o site está fora do ar — não no servidor, e não só na sua cabeça.
Depois feche as duas lacunas estruturais, porque é aí que essa camada se encaixa na pilha. Mitigação é o que absorve o volume, um edge é o que mantém a inundação longe dos seus dados, e a máquina atrás deles ainda precisa estar devidamente protegida e ter backups fora do site restauráveis, porque o incidente depois deste pode não ser uma inundação de jeito nenhum. Um servidor que sobrevive a um DDoS e perde o disco um mês depois nunca foi resiliente — teve sorte, duas vezes.
SP·09Passo a passo
-
01
Confirme antes de mudar qualquer coisa
Consiga um retrato honesto da máquina antes de tocar em um arquivo de configuração. Você está procurando uma mudança abrupta em pacotes ou requisições, e para onde o tempo está indo — uma aplicação sem CPU por causa de interrupções de software é um incidente muito diferente de uma aplicação esperando por um banco de dados.
# is the box alive, and where is the time going? uptime # load average against your core count vmstat 1 5 # 'in' and 'cs' high, 'id' near zero = packet work mpstat -P ALL 1 3 # %soft pinned on one core = interrupt saturation # is the pipe full, or just busy? ip -s link show eth0 # rx bytes, and the errors/dropped counters ethtool eth0 | grep -i speed # requests per second from your own log, minute by minute tail -n 20000 /var/log/nginx/access.log \ | awk -F'[][]' '{print $2}' | cut -d: -f2-4 | uniq -c | tail -5Antes de concluir que é um ataque, confira o seu próprio log de implantação e a sua tabela de cron. Indisponibilidades autoinfligidas são mais comuns do que inundações em um único VPS, e parecem a mesma coisa vistas de fora.
-
02
Meça o formato do tráfego em sessenta segundos
Agora classifique isso. Três perguntas: quantas origens distintas, qual protocolo e estado, e — se for HTTP — quais caminhos e quais agentes. As respostas decidem qual controle você vai usar, e levam cerca de um minuto para ser coletadas.
# top source addresses on the wire right now timeout 20 tcpdump -nn -i eth0 -c 20000 2>/dev/null \ | awk '{print $3}' | rev | cut -d. -f2- | rev \ | sort | uniq -c | sort -rn | head -20 # TCP state census: a wall of SYN-RECV is a SYN flood ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn # layer 7: talkers, paths, agents over the last 50k requests L=/var/log/nginx/access.log tail -n 50000 $L | awk '{print $1}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | cut -d'"' -f6 | sort | uniq -c | sort -rn | head -10Leia o resultado contra os sinais: query strings únicas em um único caminho, uma lista de user-agent sem cauda longa, nenhuma requisição para os seus arquivos estáticos, ou uma taxa por endereço estranhamente uniforme. Se a largura de banda está alta em portas que você não escuta, pare aqui — isso é uma inundação volumétrica e o passo seis é o único passo que importa.
-
03
Sirva algo barato, e limite as conexões
Maior alavancagem primeiro. Um micro-cache de trinta segundos reduz uma inundação de requisições anônimas idênticas a um único acerto na origem, e timeouts apertados matam ataques lentos que um limite de taxa não consegue enxergar. Coloque os dois no bloco
http, e depois recarregue em vez de reiniciar, para manter as suas conexões quentes.# /etc/nginx/nginx.conf — http block limit_conn_zone $binary_remote_addr zone=perip:10m; limit_req_zone $binary_remote_addr zone=flood:20m rate=10r/s; client_header_timeout 10s; client_body_timeout 10s; send_timeout 10s; keepalive_timeout 20s; reset_timedout_connection on; proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=hot:64m max_size=2g inactive=10m use_temp_path=off;# the server block — cap concurrency, serve the cached copy limit_conn perip 20; location / { proxy_cache hot; proxy_cache_valid 200 301 302 30s; proxy_cache_lock on; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; add_header X-Cache $upstream_cache_status; proxy_pass http://127.0.0.1:8080; }nginx -t && systemctl reload nginx curl -sI https://example.com/ | grep -i x-cache # want: HIT on the second call
-
04
Limite a taxa do cliente real, não do seu próprio proxy
Se tiver alguma coisa na frente do nginx, toda requisição chega a partir de um único endereço, e um limite por endereço vai estrangular o proxy em vez do atacante — ou baní-lo de vez e derrubar o site justamente enquanto você está tentando defendê-lo. Confie no cabeçalho encaminhado só quando ele vier do endereço do proxy, nunca da internet, e só então aplique o limite.
# /etc/nginx/conf.d/realip.conf — the tunnel or edge address only set_real_ip_from 10.66.0.1; real_ip_header X-Forwarded-For; real_ip_recursive off;
# burst absorbs bursty humans; nodelay keeps the page fast for them location / { limit_req zone=flood burst=20 nodelay; limit_req_status 429; } # the expensive paths get a much tighter bucket of their own location ~ ^/(search|login|register|api/) { limit_req zone=flood burst=5; limit_req_status 429; }Depois observe o que você acabou de fazer:
tail -f /var/log/nginx/error.log | grep limiting. Se os endereços sendo limitados parecem ser os seus clientes, a taxa está baixa demais — aumente ela. Um limite que bloqueia usuários de verdade é uma indisponibilidade que você mesmo causou. -
05
Bloqueie de forma estreita, e dê um prazo de expiração a cada bloqueio
Só vale a pena fazer isso quando o passo dois mostrou origens concentradas. Use um set, não mil regras — uma consulta de
ipseté de tempo constante, uma cadeia longa deiptablesé percorrida a cada pacote e se torna a sua própria negação de serviço. Dê um timeout a cada entrada para que a emergência de hoje não vire a lista de bloqueio silenciosa do ano que vem.ipset create flood hash:net timeout 3600 -exist iptables -I INPUT -m set --match-set flood src -j DROP # feed it from the census: /24s you actually verified, not guesses for n in 203.0.113.0/24 198.51.100.0/24; do ipset add flood $n -exist; done # SYN flood: let the kernel do the part it is good at sysctl -w net.ipv4.tcp_syncookies=1 sysctl -w net.ipv4.tcp_max_syn_backlog=8192 sysctl -w net.core.somaxconn=8192 ipset list flood | head -20 # keep a copy of this for the post-mortem
Resista à tentação de bloquear um país inteiro por geolocalização, a menos que você consiga nomear os clientes que está cortando. E nunca faça
ufw disablepara testar uma teoria: uma máquina sem firewall sob ataque ativo é assim que um incidente de largura de banda vira uma invasão. -
06
Escale para a camada que realmente consegue absorver isso
Se os contadores da interface dizem que o link está saturado, ou se os drops estão subindo enquanto a sua CPU está ociosa, você chegou ao teto de qualquer coisa que consiga fazer no servidor. Confirme essa leitura, e então escale em vez de continuar ajustando.
# drops in the stack itself — second column is 'dropped' awk '{print strtonum("0x" $2)}' /proc/net/softnet_stat | paste -sd+ | bc # interface-level loss against link speed ip -s link show eth0 | sed -n '3,6p'Com o scrubbing a montante já em funcionamento, geralmente não há nada a fazer: a detecção é sempre ativa e a inundação é descartada dentro da rede antes de chegar à sua porta — na nossa frota isso é 1.5 Tbps de capacidade na frente de cada plano, então o incidente muitas vezes só fica visível como um gráfico, depois do fato. Se o ataque é uma inundação HTTP bem construída, em vez de uma volumétrica, esse é o caso de usar um escudo de L7, porque inundações de requisições são indistinguíveis de usuários no nível de pacote e precisam ser julgadas mais acima na pilha. Se você não tem mitigação nenhuma, as suas opções realistas são se mudar para trás de um edge que tenha, ou esperar — e planejar a primeira opção antes do próximo ataque.
-
07
Feche o ciclo: verifique, reverta, e depois anote
Verifique de fora da máquina, não a partir de um shell dentro dela. Depois desfaça as medidas de emergência de forma deliberada, mantenha as que sempre foram uma boa ideia, e registre os números para que o próximo incidente comece a partir de uma baseline em vez de um chute.
# from somewhere else entirely: is the site healthy for a normal user? curl -s -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer}s\n' \ https://example.com/ # did you leave a limit that is biting real people? grep -c 'limiting requests' /var/log/nginx/error.log # what is still blocked, and when does it expire? ipset list flood | head -30Mantenha o cache, os timeouts e os syncookies — essas são melhorias permanentes. Reverta os limites de taxa agressivos para a sua baseline medida mais uma margem, e deixe as entradas do ipset expirarem sozinhas. Depois escreva os quatro comandos dos passos um e dois em um runbook guardado em algum lugar que não seja este servidor, ao lado da sua taxa normal de requisições por segundo e da sua largura de banda normal de pico. O próximo ataque vai ser um incidente mais curto só porque esses dois números existem.


