Este é o segundo post da nossa série sobre a implantação do NGINX Open Source e do NGINX Plus como gateway de API.
- Parte 1 fornece instruções detalhadas de configuração para diversos casos de uso.
- Publicado originalmente em 2018, este artigo foi atualizado para refletir as práticas recomendadas atuais de configuração de APIs, usando blocos
locationaninhados para encaminhar solicitações em vez de regras de reescrita.
Este post amplia esses casos de uso e analisa uma série de medidas de proteção que podem ser aplicadas para proteger e tornar mais seguros os serviços de API de backend em produção:
- Limitação de taxa
- Imposição de métodos de solicitação específicos
- Aplicação de controle de acesso granular
- Controlando o tamanho das solicitações
- Validando corpos de requisição
- Parte 3 explica como implantar o NGINX Open Source e o NGINX Plus como um gateway de API para serviços gRPC.
Observação: Salvo indicação em contrário, todas as informações desta publicação se aplicam tanto ao NGINX Open Source quanto ao NGINX Plus. Para facilitar a leitura, no restante do blog, ambos serão chamados simplesmente de “NGINX”.
Limitação de taxa
Ao contrário dos clientes baseados em navegador, clientes individuais de API podem impor cargas enormes às suas APIs, chegando até mesmo a consumir uma parcela tão grande dos recursos do sistema que outros clientes de API ficam efetivamente impedidos de acessá-las. Essa ameaça não vem apenas de clientes mal-intencionados: um cliente de API com comportamento inadequado ou com falhas pode entrar em um loop e sobrecarregar o backend. Para evitar isso, aplicamos um limite de requisições a fim de garantir o uso justo por parte de 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. Normalmente, utiliza-se o endereço IP do cliente, mas, quando a autenticação está habilitada para a API, o ID do cliente autenticado é um atributo mais confiável e preciso.
Os próprios limites de taxa são definidos no arquivo de configuração de nível superior do gateway de API e podem ser aplicados globalmente, por API ou até mesmo por URI.
Carregando exemplo de código…
Neste exemplo, a diretiva limit_req_zone na linha 4 define um limite de 10 solicitações por segundo para cada endereço IP de cliente ($binary_remote_addr), e a diretiva na 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 a recursos distintos.
Em seguida, no trecho de configuração abaixo, usamos a diretiva limit_req para aplicar o primeiro limite de taxa na seção de políticas da “API de Armazém” descrita na Parte 1<.htmla>. Por padrão, o NGINX envia a resposta 503 (Serviço Indisponível) quando o limite de taxa é excedido. No entanto, é útil que os clientes da API saibam explicitamente que excederam seu limite de taxa, para que possam ajustar seu comportamento. Para isso, usamos a diretiva limit_req_status para enviar a resposta 429 (Muitas Solicitações ) em vez disso.
Carregando exemplo de código…
Você pode usar parâmetros adicionais na diretiva limit_req para ajustar como o NGINX aplica os limites de taxa. Por exemplo, é possível colocar as solicitações em uma fila 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 como ajustar os limites de taxa, consulte Limitação de taxa com o NGINX e o NGINX Plus em nosso blog.
Aplicação de limites a métodos de solicitação específicos
Em APIs RESTful, o método HTTP (ou verbo) é uma parte importante de cada chamada de API e tem grande relevância para a definição da API. Tomemos como exemplo o serviço de preços da nossa API Warehouse:
GET/api/warehouse/pricing/item001retorna o preço do item001PATCH/api/warehouse/pricing/item001altera o preço do item001
Podemos atualizar as definições de roteamento de URI na API do armazém para aceitar apenas esses dois métodos HTTP nas solicitações ao serviço de preços (e apenas o método GET nas solicitações ao serviço de estoque).
Carregando exemplo de código…
Com essa configuração, as solicitações ao serviço de preços que usam métodos diferentes dos listados na linha 22 (e as solicitações ao serviço de inventário que usam um método diferente daquele indicado na linha 13) são rejeitadas e não são encaminhadas aos serviços de backend. O NGINX envia a resposta 405 (Método não permitido) para informar ao cliente da API a natureza exata do erro, conforme mostrado no rastreamento de console a seguir. Quando for necessária uma política de segurança de divulgação mínima, a diretiva error_page poderá ser usada para converter essa resposta em um erro menos informativo, por exemplo, 400 (Solicitação inválida).
$ 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":"Method not allowed"}Aplicando o controle de acesso granular
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 os atributos desse ID para implementar um controle de acesso granular.
Aqui, mostramos dois exemplos desse tipo.
- O primeiro exemplo controla o acesso a um recurso específico da API, ampliando uma configuração apresentada na Parte 1 e usando a autenticação por chave de API para verificar se um determinado cliente da API está na lista de permissões.
- O segundo exemplo determina quais métodos HTTP um cliente tem permissão para usar. Ele implementa o método de autenticação JWT mencionado na Parte 1, usando uma declaração personalizada para identificar clientes de API qualificados. (Observe que o suporte a JWT é exclusivo do NGINX Plus.)
Naturalmente, outros métodos de autenticação podem ser aplicados a esses exemplos de casos de uso, como a autenticação HTTP Basic e a introspecção de token OAuth 2.0.
Controle de acesso a recursos específicos
Digamos que queremos permitir que apenas “clientes de infraestrutura” acessem o recurso de auditoria do serviço de inventário da API Warehouse. Com a autenticação por chave de API habilitada, usamos um bloco map para criar uma lista de permissões com os nomes dos clientes de infraestrutura, de modo que a variável $is_infrastructure seja avaliada como 1 quando uma chave de API correspondente for usada.
Carregando exemplo de código…
Na definição da API do Warehouse, adicionamos um bloco location para o recurso de auditoria do inventário (linhas 15–20). O bloco if garante que somente clientes de infraestrutura possam acessar o recurso.
Carregando exemplo de código…
Observe que a diretiva location na linha 15 usa o modificador = (sinal de igual) para estabelecer uma correspondência exata com o recurso de auditoria. As correspondências exatas têm precedência sobre as definições padrão de prefixo de caminho usadas para os outros recursos. O rastreamento a seguir mostra como, com essa configuração, um cliente que não está na lista de permissões não consegue acessar o recurso de auditoria do inventário. A chave de API exibida pertence a client_two (conforme definido na Parte 1).
$ curl -H "apikey: QzVV6y1EmQFbbxOfRCwyJs35" https://api.example.com/api/warehouse/inventory/audit{"status":403,"message":"Forbidden"}Controlando o acesso a métodos específicos
Conforme definido acima, o serviço de preços aceita os métodos GET e PATCH, que permitem aos clientes, respectivamente, consultar e alterar o preço de um item específico. (Também poderíamos optar por permitir os métodos POST e DELETE, para oferecer o gerenciamento completo do ciclo de vida dos dados de preços.) Nesta seção, ampliamos esse caso de uso para controlar quais métodos cada usuário pode executar. Com a autenticação JWT habilitada para a API Warehouse, as permissões de cada cliente são codificadas como declarações personalizadas. Os JWTs emitidos para administradores autorizados a alterar os dados de preços incluem a declaração "admin":true. Agora, ampliamos nossa lógica de controle de acesso para que somente administradores possam fazer alterações.
Carregando exemplo de código…
Este bloco map, adicionado ao final de api_gateway.conf, recebe o método da solicitação ($request_method) como entrada e gera uma nova variável, $admin_permitted_method. Métodos somente leitura são sempre permitidos (linhas 62–64), mas o acesso a operações de gravação depende do valor da declaração admin no JWT (linha 65). Agora, ampliamos a configuração da nossa API de Armazém para garantir que somente administradores possam fazer alterações nos preços.
Carregando exemplo de código…
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). Vale lembrar que a autenticação JWT é exclusiva do NGINX Plus.
Controlando o tamanho das solicitações
As APIs HTTP geralmente usam o corpo da solicitação para conter instruções e dados a serem processados pelo serviço de API de backend. Isso se aplica tanto às APIs XML/SOAP quanto às APIs JSON/REST. Consequentemente, o corpo da solicitação pode representar um vetor de ataque aos serviços de API de backend, que podem estar 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. Esse limite pode ser aumentado para APIs que lidam especificamente com cargas úteis grandes, como no processamento de imagens, mas, para a maioria das APIs, definimos um valor menor.
Carregando exemplo de código…
A diretiva client_max_body_size na linha 7 limita o tamanho do corpo da requisição. Com essa configuração, podemos comparar o comportamento do gateway de API ao receber duas requisições PATCH diferentes para o serviço de preços. O primeiro comando curl envia um pequeno trecho 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/item001HTTP/1.1 204 No Content
Server: nginx/1.19.5
Connection: keep-alive
$ curl -iX PATCH -d@/etc/services https://api.example.com/api/warehouse/pricing/item001
HTTP/1.1 413 Request Entity Too Large
Server: nginx/1.19.5
Content-Type: application/json
Content-Length: 45
Connection: close
{"status":413,"message":"Payload too large"}Validando corpos de requisição
[Editor – O caso de uso a seguir é um dos vários disponíveis para o módulo JavaScript do NGINX. Para ver a lista completa, consulte Casos de uso do módulo JavaScript do NGINX.]
Além de serem vulneráveis a ataques de estouro de buffer causados por corpos de solicitação grandes, os serviços de API de backend podem ser suscetíveis a corpos que contenham dados inválidos ou inesperados. Para aplicações que exigem JSON formatado corretamente no corpo da solicitação, podemos usar o módulo JavaScript do NGINX<.htmla> para verificar se os dados JSON são analisados sem erros antes de encaminhá-los ao 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 da função que valida os dados JSON.
Carregando exemplo de código…
A diretiva js_set define uma nova variável, $json_validated, que é avaliada chamando a função parseRequestBody.
Carregando exemplo de código…
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 de upstream de destino dessa solicitação será retornado (linha 8). Se não for possível analisar o corpo da solicitação (causando uma exceção), será retornado o endereço de um servidor local (linha 11). A diretiva return preenche a variável $json_validated para que possamos usá-la para determinar para onde enviar a solicitação.
Carregando exemplo de código…
Na seção de roteamento de URI da API Warehouse, modificamos a diretiva proxy_pass na linha 22. Ela encaminha a solicitação ao serviço de API de backend, como nas configurações da API Warehouse abordadas nas seções anteriores, mas agora usa a variável $json_validated como endereço de destino. Se o corpo da solicitação do cliente tiver sido analisado com sucesso como JSON, encaminharemos a solicitação ao grupo upstream definido na linha 15. No entanto, se ocorrer uma exceção, usaremos o valor retornado de 127.0.0.1:10415 para enviar uma resposta de erro ao cliente.
Carregando exemplo de código…
Quando as solicitações são encaminhadas por proxy para este servidor virtual, o NGINX envia a resposta 415 (Tipo de mídia não compatível) ao cliente.
Com essa configuração completa implementada, o NGINX encaminha as solicitações ao serviço de API de backend somente se elas tiverem corpos JSON formatados corretamente.
$ curl -iX POST -d '{"sku":"item002","price":85.00}' https://api.example.com/api/warehouse/pricingHTTP/1.1 201 Created
Server: nginx/1.19.5
Location: /api/warehouse/pricing/item002
$ curl -X POST -d 'item002=85.00' https://api.example.com/api/warehouse/pricing
{"status":415,"message":"Unsupported media type"}Uma observação sobre a variável $request_body Variable
A função JavaScript parseRequestBody usa a variável $request_body para analisar o JSON. No entanto, por padrão, o NGINX não preenche essa variável e simplesmente transmite o corpo da solicitação ao backend sem criar cópias intermediárias. Ao usar a diretiva mirror na 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.
Carregando exemplo de código…
As diretivas nas linhas 17 e 19 controlam como o NGINX processa internamente o corpo da solicitação. Definimos client_body_buffer_size com o mesmo tamanho de 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 em disco, mas aumenta o uso de memória. Para a maioria dos casos de uso de gateway de API com corpos de solicitação pequenos, essa é uma boa solução de compromisso.
Como 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, portanto a enviamos para um local “sem saída” (/_get_request_body) que definimos no bloco server da configuração de nível superior do gateway de API.
Carregando exemplo de código…
Este local não faz nada além de enviar a resposta 204 (Sem Conteúdo). Como essa resposta está relacionada a uma solicitação espelhada, ela é ignorada e, portanto, adiciona uma sobrecarga insignificante ao processamento da solicitação original do cliente.
Resumo
Neste segundo post da nossa série sobre a implantação do NGINX Open Source e do NGINX Plus como gateway de API, abordamos o desafio de proteger os serviços de API de backend em um ambiente de produção contra clientes mal-intencionados e com comportamento inadequado. O NGINX usa, para gerenciar o tráfego de APIs, a mesma tecnologia utilizada para operar e proteger os sites mais acessados da Internet atualmente.
Confira o outro post desta série:
- Parte 1 explica como configurar o NGINX em alguns casos de uso essenciais de gateway de API.
- Parte 3 explica como implantar o NGINX como um gateway de API para serviços gRPC.
Para experimentar o NGINX Plus como gateway de API, inicie sua avaliação gratuita de 30 dias hoje mesmo ou entre em contato conosco para conversar sobre seus casos de uso. Durante o período de avaliação, use o conjunto completo de arquivos de configuração do nosso repositório de Gists no GitHub.
Sobre o autor

Publicações relacionadas no blog

Como o F5 NGINX One aprimora o gerenciamento e a observabilidade
Descubra como o F5 NGINX One aprimora o gerenciamento e a observabilidade de ambientes híbridos e multicloud ao oferecer um console web centralizado baseado em SaaS, com insights em tempo real, conformidade automatizada e gerenciamento simplificado de configurações.

Do OpenAPI ao NGINX como gateway de API usando uma API declarativa
Descubra como transformar uma definição de esquema OpenAPI em uma configuração NGINX totalmente funcional, operando como um API Gateway com a segurança de um Web Application Firewall e um Portal do Desenvolvedor, usando uma abordagem declarativa de API.

Como gerenciar picos repentinos de tráfego e sobrecarga do servidor
Picos de tráfego e sobrecarga de servidores são preocupações sérias para varejistas online em qualquer época, mas especialmente durante a “Cyber Season” de compras de fim de ano. Detalhamos como você pode aproveitar as soluções de entrega de aplicações de nível de produção da NGINX para se preparar para aumentos repentinos de tráfego.

Distribuição segura de chaves privadas SSL com o NGINX
Descrevemos três maneiras progressivamente mais seguras de proteger chaves privadas SSL ao configurar o NGINX para processar tráfego HTTPS: permitir acesso de leitura apenas ao usuário root, criptografar as chaves com senhas armazenadas separadamente e distribuir as senhas a partir de um repositório central.
Agrupando solicitações de API em lotes com NGINX Plus e o módulo JavaScript do NGINX
O módulo JavaScript do NGINX lançado com o NGINX Plus R15 oferece suporte a subsolicitações, possibilitando uma classe totalmente nova de casos de uso para o NGINX Plus. Este exemplo completo também usa a API do NGINX Plus e os módulos de chave-valor para converter solicitações de API dos clientes em solicitações separadas para as APIs dos microsserviços que compõem um aplicativo.
Como superar o esgotamento de portas efêmeras no NGINX e no NGINX Plus
Confira dicas para evitar o esgotamento de portas efêmeras com ajustes no kernel do Linux, conexões keepalive e conexões vinculadas dinamicamente a endereços IP locais.