Backend Seguro — Rate Limit com Spring Boot em múltiplas instâncias
Introdução
Hoje em dia é muito comum termos aplicações que funcionam online, em navegadores ou dispositivos móveis. Com isso é necessário proteger todos os serviços para que não ocorram vazamentos e acessos indevidos que poderiam prejudicar a imagem da solução ou até elevando consideravelmente o custo de cloud.
Através da técnica de Rating Limit é possível impor limites na quantidade de requisições por um intervalo de tempo para um cliente/ip. Essa camada de segurança protege as APIs contra:
- DDoS: Ataque de negação de serviço que tem o objetivo de deixar seu sistema offline.
- Brute Force: Ataque de força bruta muito utilizado para descobrir senhas de acessos.
- Uso excessivo da aplicação: Normalmente feito por bots.
Em uma arquitetura de micro serviço feita em java/kotlin e com o framework Spring é possível de maneira simples, sem aumento significativo da perfomance, com pouco código adicionado e sem nenhuma modificação na regra de negócio, implementar de forma distribuída e em todas as instancias o Rate Limiting.
Conhecendo o Hazelcast: Dados distribuídos
O Hazelcast é uma ferramenta open-source focada em computação distribuída e é feita em Java. Isso mesmo, através de um pequeno jar na dependência do projeto temos uma maneira muito simples de distribuir o processamento de dados entre os nós.
Os dados ficam compartilhados em memória entre todas as instâncias da aplicação através de maps, por isso é uma ferramenta muito utilizada para cache compartilhado e com alta performance.
A ferramenta Hazelcast é disponibilizada em duas versões sendo uma community e outra enterprise, que possui alguns recursos mais elaborados. Ainda assim, no escopo desse artigo a versão adotada será a community, de fácil acesso a todos.
Por padrão e para facilitar a configuração o Hazelcast utiliza UDP para realizar discovery de máquinas na rede e assim adicionar ao cluster automaticamente por multicast, porém se preferir também é possível configurar a comunicação via TCP/IP.
Para o rate limiting implementado neste artigo o Hazelcast será utilizado no escopo de cache em memória para compartilhamento entre os nós do mapa das requisições feitas para cada cliente conforme a topologia abaixo.
Conhecendo o Bucket4J: Rate limiting
O Bucket4J é uma biblioteca feita em Java que implementa o rate limit através do token-bucket algorithm.
"Podemos considerar um Token Bucket como um balde furado em que a fila de pacotes pode ser considerada como o volume do balde. Independente de como cheguem os dados na fila, a saída é sempre uniforme em vazão e latência. Se a capacidade da fila for ultrapassada (volume de pacotes for maior que o volume do balde) então os pacotes serão descartados, o balde transborda os pacotes em excesso." Referencia: https://pt.wikipedia.org/wiki/Token_bucket
Também nesse artigo será utilizada a biblioteca bucket4j-spring-boot-starter para uma fácil integração do Bucket4j com o Spring Boot através da configuração de properties.
Primeiro passo: Adicionando as dependências no Spring Boot
Conforme dito anteriormente é necessário habilitar o cache do Spring e adicionar o Hazelcast e também o Bucket4J como dependências ao pom.xml do projeto, conforme exemplo do trecho abaixo.
Segundo passo: Configuração do cache
Para habilitar o cache no Spring Boot é necessário adicionar em uma classe de configuração a anotação @EnableCaching
.
Para o Hazelcast, deve-se criar o mapa de cache que será compartilhado entre os nós, através da criação do arquivo hazelcast.xml no resources da aplicação.
O valor do XML deve seguir as informações abaixo:
Para que o Spring reconheça o arquivo de configuração do Hazelcast é preciso adicionar no application.properties a propriedade:
spring.cache.hazelcast.config=classpath:hazelcast.xml
Já para o Bucket4J funcionar com as versões mais nova do Spring Boot tem que habilitar a sobrecarga de beans de um mesmo qualifier, através da propriedade abaixo:
spring.main.allow-bean-definition-overriding=true
Ficará assim:
Terceiro passo: Regra padrão do limite de requisições na aplicação por IP
Para a configuração de uma regra default é preciso habilitar algumas propriedades no application.properties da aplicação, neste exemplo do artigo temos uma regra de 5 acessos por minuto para um mesmo IP e após excedidas as requisições ocorre um bloqueio de 1 minuto.
Dica adicional: Se a aplicação tem uma capacidade limitada de recursos é possível utilizar o rate limiting para bloquear o excedente para todas as requisições de origens distintas, para isso basta remover a propriedade
bucket4j.filters[0].rate-limits[0].expression
Para testar a configuração é necessário ter pelo menos um Controller, segue abaixo o exemplo:
Teste do limite de requisições no 'cluster' da aplicação
Após o build realizado com sucesso e para simular um cluster da aplicação na própria máquina local será necessário iniciar várias instâncias da aplicação em portas distintas como no exemplo abaixo:
- Instância 1:
java -jar -Dserver.port=8080 rate-limit-java.jar
- Instância 2:
java -jar -Dserver.port=8081 rate-limit-java.jar
- Instância 3:
java -jar -Dserver.port=8082 rate-limit-java.jar
A cada nova instância que finaliza o carregamento é exibido nos logs das aplicações o novo nó no cluster do Hazelcast, conforme demostrado abaixo.
Para a o teste das requisições serão feitas 6 chamadas entre instâncias (portas) distintas para o endpoint "/" em um intervalo de menos de 1 minuto.
1. Requisição na instância de número 1 (porta 8080):
2. Requisição na instância de número 2 (porta 8081):
3. Requisição na instância de número 3 (porta 8082):
E assim segue até a sexta requisição quando ocorre o bloqueio.
Quarto passo: Regra de limite com validação usando Beans e regex por URL (opcional)
Além da restrição de acesso de todos os endpoints da aplicação por IP é possível limitar especificamente algumas URLs para um bloqueio mais restritivo. É muito útil para recursos mais sensíveis, por exemplo nos serviços vinculados à autenticação para evitar tentativa de força bruta, através da regex da propriedade do exemplo: bucket4j.filters[0].url=/auth/.*
Também é possível com o Bucket4J bloquear um usuário com muitos acessos mesmo tendo IPs distintos, através da utilização da execução de beans. Exemplo da propriedade:bucket4j.filters[0].rate-limits[0].expression
É importante observar que o Bucket4J tem uma configuração padrão que a primeira regra encontrada será a utilizada, portanto é recomendado colocar a regra global como sendo a de menor prioridade e para isso é preciso renomear as propriedades iniciadas porbucket4j.filters[0]
para bucket4j.filters[1]
.
Para as duas regras restritivas o application.properties ficará conforme o trecho abaixo:
A simulação do teste é exatamente igual a etapa anterior, apenas trocando o endpoint final.
Exemplo de requisição:
curl -i -H "Content-Type: application/json" -X POST -d '{"oldPassword": "admin123", "newPassword": "s3cr3t"}' http://localhost:8080/auth/reset-password
Na quarta execução é bloqueado por 5 minutos conforme regra especificada no properties.
Quinto passo: Métricas de bloqueios (opcional)
Com o Bucket4j é possível obter métricas dos acessos negados pelo rate limiting através do Actuator que juntamente com o Prometheus pode criar um painel dashboard em um Grafana para monitorar e gerar alertas sobre tentativas de acessos indevidos ou possíveis tentativas de ataque ao sistema.
O foco deste artigo é apenas a liberação dos dados do Bucket4j com o Actuator, mas para entender como configurar o Prometheus com o Grafana no Spring Boot veja a recomendação de leitura aqui.
Para habilitar o Actuator deve adicionar a dependência no pom.xml conforme exemplo abaixo:
E também no application.properties deve adicionar a seguinte configuração:
Após a subida da aplicação e execução das requisições é possível obter as métricas dos bloqueios do Actuator conforme imagem abaixo:
Código final
O código utilizado nesse artigo pode ser clonado em: https://github.com/christianoguedes/rate-limit-java
Conclusão
O Rate Limiting é uma técnica essencial para proteger qualquer sistema e o Bucket4J juntamente com o Hazelcast são uma alternativa simples para o Spring Boot sem ter que "poluir" o código, nem ter que alterar nenhuma regra de negócio e são também de fácil escalabilidade em instâncias distribuídas.
Também é possível ter um grande poder de personalização das regras de bloqueios com a execução dos beans, algo que normalmente não é possível em um rate limiting externo a aplicação.
É sempre importante levar em conta que a cyber segurança é composta por camadas e o rate limiting é apenas uma dessas etapas e técnicas. Portanto é recomendado que a aplicação tenha outras medidas como por exemplo bloqueio por captcha nas retentativas de autenticação e na recuperação de senha.
Referências para aprender mais
- Hazelcast usado como cache no Spring
- Bucket4J com Spring MVC
- Bucket4J Starter para Spring Boot
- Métricas com uso do Actuator, Prometheus e Grafana no Spring Boot
- Rate Limiting para NGINX
Qualquer comentário ou dúvidas estou no LinkedIn ou no GitHub.