Esta é a segunda postagem do blog em nossa série sobre a implantação do NGINX Open Source e do NGINX Plus como um gateway de API.
A Parte 1 fornece instruções detalhadas de configuração para vários casos de uso.
Esta postagem amplia esses casos de uso e analisa uma série de salvaguardas que podem ser aplicadas para proteger e garantir serviços de API de backend em produção:
Publicado originalmente em 2018, este post foi atualizado para refletir as melhores práticas atuais para configuração de API, usando blocos de localização
aninhados para rotear solicitações em vez de reescrever regras.
Observação : Exceto quando indicado, todas as informações nesta postagem se aplicam ao NGINX Open Source e ao NGINX Plus. Para facilitar a leitura, o restante do blog se refere simplesmente a “NGINX”.
Ao contrário dos clientes baseados em navegador, os clientes de API individuais podem colocar cargas enormes em suas APIs, até mesmo consumindo uma proporção tão alta de recursos do sistema que outros clientes de API são efetivamente bloqueados. Não são apenas clientes maliciosos que representam essa ameaça: um cliente de API com comportamento inadequado ou com bugs pode entrar em um loop que sobrecarrega o backend. Para nos proteger contra isso, aplicamos um limite de taxa para garantir o uso justo por cada cliente e proteger os recursos dos serviços de backend.
O NGINX pode aplicar limites de taxa com base em qualquer atributo da solicitação. O endereço IP do cliente normalmente é usado, mas quando a autenticação é habilitada para a API, o ID do cliente autenticado é um atributo mais confiável e preciso.
Os limites de taxa em si são definidos no arquivo de configuração do gateway de API de nível superior e podem ser aplicados globalmente, por API ou até mesmo por URI.
Neste exemplo, a diretiva limit_req_zone
na linha 4 define um limite de taxa de 10 solicitações por segundo para cada endereço IP do cliente ( $binary_remote_addr
), e a da linha 5 define um limite de 200 solicitações por segundo para cada ID de cliente autenticado ( $http_apikey
). Isso ilustra como podemos definir vários limites de taxa independentemente de onde eles são aplicados. Uma API pode aplicar vários limites de taxa ao mesmo tempo ou aplicar limites de taxa diferentes para recursos diferentes.
Em seguida, no seguinte snippet de configuração, usamos a diretiva limit_req
para aplicar o primeiro limite de taxa na seção de política da “API do Warehouse” descrita na Parte 1<.htmla>. Por padrão, o NGINX envia o 503
(Serviço
indisponível)
resposta quando o limite de taxa for excedido. No entanto, é útil que os clientes da API saibam explicitamente que excederam seu limite de taxa, para que possam modificar seu comportamento. Para isso, usamos a diretiva limit_req_status
para enviar o 429
(
Muitas
solicitações)
resposta em vez disso.
Você pode usar parâmetros adicionais na diretiva limit_req
para ajustar como o NGINX aplica limites de taxa. Por exemplo, é possível enfileirar solicitações em vez de rejeitá-las imediatamente quando o limite é excedido, dando tempo para que a taxa de solicitações fique abaixo do limite definido. Para obter mais informações sobre o ajuste fino dos limites de taxa, consulte Limitação de taxa com NGINX e NGINX Plus em nosso blog.
Com APIs RESTful, o método (ou verbo) HTTP é uma parte importante de cada chamada de API e muito significativo para a definição da API. Tomemos como exemplo o serviço de precificação da nossa API Warehouse:
GET
/api/warehouse/pricing/item001
retorna o preço do item001PATCH
/api/warehouse/pricing/item001
altera o preço do item001Podemos atualizar as definições de roteamento de URI na API do Warehouse para aceitar apenas esses dois métodos HTTP em solicitações ao serviço de preços (e apenas o método GET
em solicitações ao serviço de inventário).
Com essa configuração em vigor, solicitações ao serviço de preços usando métodos diferentes dos listados na linha 22 (e ao serviço de inventário diferentes do da linha 13) são rejeitadas e não são passadas aos serviços de backend. O NGINX envia o 405
(Método
não
permitido)
resposta para informar o cliente da API sobre a natureza precisa do erro, conforme mostrado no rastreamento do console a seguir. Quando uma política de segurança de divulgação mínima é necessária, a diretiva error_page
pode ser usada para converter esta resposta em um erro menos informativo, por exemplo400
(
Pedido ruim)
.
$ curl https://api.example.com/api/warehouse/pricing/item001 {"sku":"item001","price":179.99} $ curl -X DELETE https://api.example.com/api/warehouse/pricing/item001 {"status":405,"message":"Método não permitido"}
A Parte 1 desta série descreveu como proteger APIs contra acesso não autorizado habilitando opções de autenticação, como chaves de API e JSON Web Tokens (JWTs) . Podemos usar o ID autenticado ou atributos do ID autenticado para executar um controle de acesso detalhado.
Aqui mostramos dois exemplos.
É claro que outros métodos de autenticação são aplicáveis a esses casos de uso de exemplo, como autenticação HTTP Basic e introspecção de token OAuth 2.0 .
Digamos que queremos permitir que apenas “clientes de infraestrutura” acessem o recurso de auditoria do serviço de inventário da Warehouse API. Com a autenticação de chave de API habilitada, usamos um bloco de mapa
para criar uma lista de permissões de nomes de clientes de infraestrutura para que a variável $is_infrastructure
seja avaliada como1
quando uma chave de API correspondente é usada.
Na definição da API do Warehouse, adicionamos um bloco de localização
para o recurso de auditoria de inventário ( linhas 15–20 ). O bloco if
garante que somente clientes de infraestrutura possam acessar o recurso.
Observe que a diretiva de localização
na linha 15 usa o modificador =
(sinal de igual) para fazer uma correspondência exata no recurso de auditoria . Correspondências exatas têm precedência sobre as definições de prefixo de caminho padrão usadas para os outros recursos. O rastreamento a seguir mostra como, com essa configuração em vigor, um cliente que não está na lista de permissões não consegue acessar o recurso de auditoria de inventário. A chave de API mostrada pertence ao client_two (conforme definido na Parte 1 ).
$ curl -H "chave de API: QzVV6y1EmQFbbxOfRCwyJs35" https://api.example.com/api/warehouse/inventory/audit {"status":403,"message":"Proibido"}
Conforme definido acima , o serviço de preços aceita os métodos GET
e PATCH
, que permitem que os clientes obtenham e modifiquem o preço de um item específico, respectivamente. (Também poderíamos optar por permitir os métodos POST
e DELETE
para fornecer gerenciamento completo do ciclo de vida dos dados de preços.) Nesta seção, expandimos esse caso de uso para controlar quais métodos usuários específicos podem emitir. Com a autenticação JWT habilitada para a API do Warehouse, as permissões para cada cliente são codificadas como declarações personalizadas. Os JWTs emitidos para administradores autorizados a fazer alterações nos dados de preços incluem a declaração "admin":true
. Agora estendemos nossa lógica de controle de acesso para que somente administradores possam fazer alterações.
Este bloco de mapa
, adicionado ao final de api_gateway.conf , recebe o método de solicitação ( $request_method
) como entrada e produz uma nova variável, $admin_permitted_method
. Métodos somente leitura são sempre permitidos (linhas 62–64), mas o acesso às operações de gravação depende do valor da declaração admin no JWT (linha 65). Agora estendemos nossa configuração da API do Warehouse para garantir que somente administradores possam fazer alterações de preços.
A API do Warehouse exige que todos os clientes apresentem um JWT válido (linha 7). Também verificamos se as operações de gravação são permitidas avaliando a variável $admin_permitted_method
(linha 25). Observe novamente que a autenticação JWT é exclusiva do NGINX Plus.
As APIs HTTP geralmente usam o corpo da solicitação para conter instruções e dados para o serviço de API de backend processar. Isso vale tanto para APIs XML/SOAP quanto para APIs JSON/REST. Consequentemente, o corpo da solicitação pode representar um vetor de ataque aos serviços de API de backend, que podem ser vulneráveis a ataques de estouro de buffer ao processar corpos de solicitação muito grandes.
Por padrão, o NGINX rejeita solicitações com corpos maiores que 1 MB. Isso pode ser aumentado para APIs que lidam especificamente com grandes cargas úteis, como processamento de imagens, mas para a maioria das APIs definimos um valor menor.
A diretiva client_max_body_size
na linha 7 limita o tamanho do corpo da solicitação. Com essa configuração em vigor, podemos comparar o comportamento do gateway de API ao receber duas solicitações PATCH
diferentes para o serviço de preços. O primeiro comando curl
envia um pequeno pedaço de dados JSON, enquanto o segundo comando tenta enviar o conteúdo de um arquivo grande ( /etc/services ).
$ curl -iX PATCH -d '{"price":199.99}' https://api.example.com/api/warehouse/pricing/item001 HTTP/1.1 204 Nenhum conteúdo Servidor: nginx/1.19.5 Conexão: keep-alive $ curl -iX PATCH -d@/etc/services https://api.example.com/api/warehouse/pricing/item001 HTTP/1.1 413 Entidade da solicitação muito grande Servidor: nginx/1.19.5 Tipo de conteúdo: application/json Comprimento do conteúdo: 45 Conexão: fechar {"status":413,"message":"Carga muito grande"}
[ Editor – O caso de uso a seguir é um dos vários para o módulo NGINX JavaScript. Para uma lista completa, consulte Casos de uso para o módulo JavaScript NGINX . ]
Além de serem vulneráveis a ataques de estouro de buffer com corpos de solicitação grandes, os serviços de API de backend podem ser suscetíveis a corpos que contêm dados inválidos ou inesperados. Para aplicativos que exigem JSON formatado corretamente no corpo da solicitação, podemos usar o módulo JavaScript NGINX<.htmla> para verificar se os dados JSON são analisados sem erros antes de enviá-los por proxy para o serviço de API de backend.
Com o módulo JavaScript instalado , usamos a diretiva js_import
para referenciar o arquivo que contém o código JavaScript para a função que valida dados JSON.
A diretiva js_set
define uma nova variável, $json_validated
, que é avaliada chamando a função parseRequestBody
.
A função parseRequestBody
tenta analisar o corpo da solicitação usando o método JSON.parse
(linha 6). Se a análise for bem-sucedida, o nome do grupo upstream pretendido para esta solicitação será retornado (linha 8). Se o corpo da solicitação não puder ser analisado (causando uma exceção), um endereço de servidor local será retornado (linha 11). A diretiva return
preenche a variável $json_validated
para que possamos usá-la para determinar para onde enviar a solicitação.
Na seção de roteamento de URI da API do Warehouse, modificamos a diretiva proxy_pass
na linha 22. Ele passa a solicitação para o serviço de API de backend como nas configurações da API do Warehouse discutidas nas seções anteriores, mas agora usa a variável $json_validated
como endereço de destino. Se o corpo do cliente foi analisado com sucesso como JSON, então usamos o proxy para o grupo upstream definido na linha 15. Entretanto, se houver uma exceção, usaremos o valor retornado de 127.0.0.1:10415
para enviar uma resposta de erro ao cliente.
Quando as solicitações são enviadas por proxy para este servidor virtual, o NGINX envia o 415
(
Tipo de mídia
não suportado )
resposta ao cliente.
Com essa configuração completa em vigor, o NGINX envia solicitações por proxy para o serviço de API de backend somente se eles tiverem corpos JSON formatados corretamente.
$ curl -iX POST -d '{"sku":"item002","price":85.00}' https://api.example.com/api/warehouse/pricing HTTP/1.1 201 Servidor criado: nginx/1.19.5 Localização: /api/warehouse/pricing/item002 $ curl -X POST -d 'item002=85.00' https://api.example.com/api/warehouse/pricing {"status":415,"message":"Tipo de mídia não suportado"}
$request_body
A função JavaScript parseRequestBody
usa a variável $request_body
para executar a análise JSON. No entanto, o NGINX não preenche essa variável por padrão e simplesmente transmite o corpo da solicitação para o backend sem fazer cópias intermediárias. Ao usar a diretiva mirror
dentro da seção de roteamento de URI (linha 16), criamos uma cópia da solicitação do cliente e, consequentemente, preenchemos a variável $request_body
.
As diretivas nas linhas 17 e 19 controlam como o NGINX manipula o corpo da solicitação internamente. Definimos client_body_buffer_size
para o mesmo tamanho que client_max_body_size
para que o corpo da solicitação não seja gravado no disco. Isso melhora o desempenho geral ao minimizar as operações de E/S de disco, mas às custas da utilização adicional de memória. Para a maioria dos casos de uso de gateway de API com corpos de solicitação pequenos, esse é um bom compromisso.
Conforme mencionado, a diretiva mirror
cria uma cópia da solicitação do cliente. Além de preencher $request_body
, não precisamos dessa cópia, então a enviamos para um local “beco sem saída” ( /_get_request_body
) que definimos no bloco do servidor
na configuração do gateway de API de nível superior.
Este local não faz nada mais do que enviar o 204
(Não
Contente)
resposta. Como essa resposta está relacionada a uma solicitação espelhada, ela é ignorada e adiciona uma sobrecarga insignificante ao processamento da solicitação original do cliente.
Nesta segunda postagem de blog da nossa série sobre a implantação do NGINX Open Source e do NGINX Plus como um gateway de API, focamos no desafio de proteger serviços de API de backend em um ambiente de produção contra clientes maliciosos e mal-comportados. O NGINX usa a mesma tecnologia para gerenciar o tráfego de API que é usada para alimentar e proteger os sites mais movimentados da Internet atualmente .
Confira o outro post desta série:
Para experimentar o NGINX Plus como um gateway de API, comece hoje mesmo seu teste gratuito de 30 dias ou entre em contato conosco para discutir seus casos de uso . Durante o teste, use o conjunto completo de arquivos de configuração do nosso repositório GitHub Gist .
"Esta postagem do blog pode fazer referência a produtos que não estão mais disponíveis e/ou não têm mais suporte. Para obter as informações mais atualizadas sobre os produtos e soluções F5 NGINX disponíveis, explore nossa família de produtos NGINX . O NGINX agora faz parte do F5. Todos os links anteriores do NGINX.com redirecionarão para conteúdo semelhante do NGINX no F5.com."