Este tutorial é um dos quatro que colocam em prática conceitos do Microservices de março de 2022: Rede Kubernetes :
Quer orientação detalhada sobre como usar o NGINX para ainda mais casos de uso de rede Kubernetes? Baixe nosso e-book gratuito, Gerenciando o tráfego do Kubernetes com NGINX: Um guia prático .
Você trabalha em TI para uma loja local popular que vende uma variedade de produtos, de travesseiros a bicicletas. Eles estão prestes a lançar sua primeira loja online, mas pediram a um especialista em segurança para fazer um teste de penetração no site antes de torná-lo público. Infelizmente, o especialista em segurança encontrou um problema! A loja online é vulnerável à injeção de SQL . O especialista em segurança conseguiu explorar o site para obter informações confidenciais do seu banco de dados, incluindo nomes de usuário e senhas.
Sua equipe veio até você – o engenheiro do Kubernetes – para salvar o dia. Felizmente, você sabe que a injeção de SQL (assim como outras vulnerabilidades) pode ser mitigada usando ferramentas de gerenciamento de tráfego do Kubernetes. Você já implantou um controlador Ingress para expor o aplicativo e, em uma única configuração, é capaz de garantir que a vulnerabilidade não possa ser explorada. Agora, a loja online pode ser lançada no prazo. Bom trabalho!
Este blog acompanha o laboratório da Unidade 3 de Microsserviços de março de 2022 – Padrão de segurança de microsserviços no Kubernetes , demonstrando como usar o NGINX e o NGINX Ingress Controller para bloquear injeção de SQL.
Para executar o tutorial, você precisa de uma máquina com:
Para aproveitar ao máximo o laboratório e o tutorial, recomendamos que antes de começar você:
Este tutorial usa estas tecnologias:
As instruções para cada desafio incluem o texto completo dos arquivos YAML usados para configurar os aplicativos. Você também pode copiar o texto do nosso repositório GitHub . Um link para o GitHub é fornecido junto com o texto de cada arquivo YAML.
Este tutorial inclui quatro desafios:
Neste desafio, você implanta um cluster minikube e instala o Podinfo como um aplicativo de exemplo que tem vulnerabilidades de segurança.
Implante um cluster minikube . Após alguns segundos, uma mensagem confirma que a implantação foi bem-sucedida.
$ minikube start 
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
Aqui você implanta um aplicativo de comércio eletrônico simples que consiste em dois microsserviços:
Execute estas etapas:
Usando o editor de texto de sua escolha, crie um arquivo YAML chamado 1-app.yaml com o seguinte conteúdo (ou copie do GitHub ).
apiVersion: apps/v1 kind: Deployment 
metadata: 
  name: app 
spec: 
  selector: 
    matchLabels: 
      app: app 
  template: 
    metadata: 
      labels: 
        app: app 
    spec: 
      containers: 
        - name: app 
          image: f5devcentral/microservicesmarch:1.0.3 
          ports: 
            - containerPort: 80 
          env: 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 
            - name: DATABASE_HOSTNAME 
              value: db.default.svc.cluster.local 
--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: app 
spec: 
  ports: 
    - port: 80 
      targetPort: 80 
      nodePort: 30001 
  selector: 
    app: app 
  type: NodePort 
--- 
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: db 
spec: 
  selector: 
    matchLabels: 
      app: db 
  template: 
    metadata: 
      labels: 
        app: db 
    spec: 
      containers: 
        - name: db 
          image: mariadb:10.3.32-focal 
          ports: 
            - containerPort: 3306 
          env: 
            - name: MYSQL_ROOT_PASSWORD 
              value: root 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 
--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: db 
spec: 
  ports: 
    - port: 3306 
      targetPort: 3306 
  selector: 
    app: db 
Implante o aplicativo e a API:
$ kubectl apply -f 1-app.yaml deployment.apps/app created 
service/app created 
deployment.apps/db created 
service/db created 
Confirme se os pods do Podinfo foram implantados, conforme indicado pelo valor Em execução na coluna STATUS . Pode levar de 30 a 40 segundos para que eles sejam totalmente implantados, então aguarde até que o status de ambos os pods esteja em execução antes de prosseguir para a próxima etapa (reemitindo o comando conforme necessário).
$ kubectl get podsNAME                  READY   STATUS    RESTARTS   AGE 
app-d65d9b879-b65f2   1/1     Running   0          37s 
db-7bbcdc75c-q2kt5    1/1     Running   0          37s 
Abra o aplicativo no seu navegador:
$ minikube service app|-----------|------|-------------|--------------| 
| NAMESPACE | NAME | TARGET PORT |     URL      | 
|-----------|------|-------------|--------------| 
| default   | app  |             | No node port | 
|-----------|------|-------------|--------------| 
😿  service default/app has no node port 
🏃  Starting tunnel for service app. 
|-----------|------|-------------|------------------------| 
| NAMESPACE | NAME | TARGET PORT |          URL           | 
|-----------|------|-------------|------------------------| 
| default   | app  |             | http://127.0.0.1:55446 | 
|-----------|------|-------------|------------------------| 
🎉  Opening service default/app in default browser... 
O aplicativo de exemplo é bastante básico. Inclui uma página inicial com uma lista de itens (por exemplo, travesseiros) e um conjunto de páginas de produtos com detalhes como descrição e preço. Os dados são armazenados no banco de dados MariaDB. Cada vez que uma página é solicitada, uma consulta SQL é emitida no banco de dados.
Ao abrir a página do produto travesseiros , você pode notar que o URL termina em /product/1 . O1 é o ID do produto. Para evitar a inserção direta de código malicioso na consulta SQL, é uma prática recomendada higienizar a entrada do usuário antes de encaminhar solicitações para serviços de backend. Mas e se o aplicativo não estiver configurado corretamente e a entrada não for escapada antes de ser inserida na consulta SQL no banco de dados?
Para descobrir se o aplicativo está escapando corretamente a entrada, execute um experimento simples alterando o ID para um que não exista no banco de dados.
Alterar manualmente o último elemento na URL de1 para-1 . A mensagem de erro ID de produto inválido "-1" indica que o ID do produto não está sendo escapado – em vez disso, a string é inserida diretamente na consulta. Isso não é bom, a menos que você seja um hacker!
Suponha que a consulta ao banco de dados seja algo como:
SELECT * FROM some_table WHERE id = "1"
Para explorar a vulnerabilidade causada por não escapar da entrada, substitua 1 com -1" <consulta_maliciosa> -- // de modo que:
" ) depois-1 conclui a primeira consulta.-- // descarta o restante da consulta.Então, por exemplo, se você alterar o elemento final na URL para ‑1" ou 1-- // , a consulta é compilada para:
SELECT * FROM some_table WHERE id = "-1" OR 1 -- //" 
                                     -------------- 
                                     ^  injected  ^ 
Isso seleciona todas as linhas do banco de dados, o que é útil em um hack. Para descobrir se esse é o caso, altere a terminação do URL para ‑1" . A mensagem de erro resultante fornece informações mais úteis sobre o banco de dados:
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"-1""' at line 1 in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23
Agora, você pode começar a manipular o código injetado na tentativa de ordenar os resultados do banco de dados por ID:
-1" OR 1 ORDER BY id DESC -- //
O resultado é a página do produto do último item no banco de dados.
Forçar o banco de dados a ordenar os resultados é interessante, mas não é especialmente útil se o seu objetivo for hackear. Tentar extrair nomes de usuários e senhas do banco de dados vale muito mais a pena.
É seguro assumir que há uma tabela de usuários no banco de dados com nomes de usuários e senhas. Mas como você estende seu acesso da tabela de produtos para a tabela de usuários?
A resposta é injetar código como este:
-1" UNION SELECT * FROM users -- //
onde
‑1" força o retorno de um conjunto vazio da primeira consulta.UNION força duas tabelas de banco de dados juntas – neste caso, produtos e usuários – o que permite que você obtenha informações (senhas) que não estão na tabela original ( produtos ).SELECT * FROM users seleciona todas as linhas na tabela de usuários .-- // descarta tudo após a consulta maliciosa.Quando você modifica a URL para terminar no código injetado, você recebe uma nova mensagem de erro:
Fatal error: Uncaught mysqli_sql_exception: The used SELECT statements have a different number of columns in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23
Esta mensagem revela que as tabelas de produtos e usuários não têm o mesmo número de colunas, então a instrução UNION não pode ser executada. Mas você pode descobrir o número de colunas por tentativa e erro, adicionando colunas (nomes de campos) uma de cada vez como parâmetros para a instrução SELECT . Um bom palpite para o nome de um campo em uma tabela de usuários é password , então tente isso:
# select 1 column-1" UNION SELECT password FROM users; -- //
# select 2 columns 
-1" UNION SELECT password,password FROM users; -- //
# select 3 columns 
-1" UNION SELECT password,password,password FROM users; -- /
# select 4 columns 
-1" UNION SELECT password,password,password,password FROM users; -- //
# select 5 columns
-1" UNION SELECT password,password,password,password,password FROM users; -- //
A última consulta é bem-sucedida (informando que há cinco colunas na tabela de usuários ) e você vê uma senha de usuário:
Neste ponto você não sabe o nome de usuário que corresponde a esta senha. Mas sabendo o número de colunas na tabela de usuários , você pode usar os mesmos tipos de consulta de antes para expor essas informações. Suponha que o nome do campo relevante seja username . E isso está certo – a consulta a seguir expõe o nome de usuário e a senha da tabela de usuários . O que é ótimo – a menos que este aplicativo esteja hospedado em sua infraestrutura!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //
O desenvolvedor do aplicativo da loja online obviamente precisa prestar mais atenção à higienização da entrada do usuário (como o uso de consultas parametrizadas), mas como um engenheiro do Kubernetes, você também pode ajudar a evitar a injeção de SQL bloqueando o ataque de atingir o aplicativo. Dessa forma, não importa tanto que o aplicativo seja vulnerável.
Há muitas maneiras de proteger seus aplicativos. No restante deste laboratório, nos concentraremos em dois:
Neste desafio, você injeta um contêiner sidecar no pod para fazer proxy de todo o tráfego e negar qualquer solicitação que tenha UNION na URL.
Primeiro, implante o NGINX Open Source como um sidecar e depois teste se ele filtra consultas maliciosas .
Observação: Estamos aproveitando essa técnica apenas para fins ilustrativos. Na realidade, implantar manualmente proxies como sidecars não é a melhor solução (mais sobre isso depois).
Crie um arquivo YAML chamado 2-app-sidecar.yaml com o seguinte conteúdo (ou copie do GitHub ). Aspectos importantes da configuração incluem:
SELECT ou UNION será negada (veja o primeiro bloco de localização na seção ConfigMap ).apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: app 
spec: 
  selector: 
    matchLabels: 
      app: app 
  template: 
    metadata: 
      labels: 
        app: app 
    spec: 
      containers: 
        - name: app 
          image: f5devcentral/microservicesmarch:1.0.3 
          ports: 
            - containerPort: 80 
          env: 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 
            - name: DATABASE_HOSTNAME 
              value: db.default.svc.cluster.local 
        - name: proxy # <-- sidecar 
          image: "nginx" 
          ports: 
            - containerPort: 8080 
          volumeMounts: 
            - mountPath: /etc/nginx 
              name: nginx-config 
      volumes: 
        - name: nginx-config 
          configMap: 
            name: sidecar 
--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: app 
spec: 
  ports: 
    - port: 80 
      targetPort: 8080 # <-- the traffic is routed to the proxy 
      nodePort: 30001 
  selector: 
    app: app 
  type: NodePort 
--- 
apiVersion: v1 
kind: ConfigMap 
metadata: 
  name: sidecar 
data: 
  nginx.conf: |- 
    events {} 
    http { 
      server { 
        listen 8080 default_server; 
        listen [::]:8080 default_server; 
        location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" { 
            deny all; 
        } 
        location / { 
            proxy_pass http://localhost:80/; 
        } 
      } 
    } 
--- 
apiVersion: apps/v1 
kind: Deployment 
metadata: 
  name: db 
spec: 
  selector: 
    matchLabels: 
      app: db 
  template: 
    metadata: 
      labels: 
        app: db 
    spec: 
      containers: 
        - name: db 
          image: mariadb:10.3.32-focal 
          ports: 
            - containerPort: 3306 
          env: 
            - name: MYSQL_ROOT_PASSWORD 
              value: root 
            - name: MYSQL_USER 
              value: dan 
            - name: MYSQL_PASSWORD 
              value: dan 
            - name: MYSQL_DATABASE 
              value: sqlitraining 
--- 
apiVersion: v1 
kind: Service 
metadata: 
  name: db 
spec: 
  ports: 
    - port: 3306 
      targetPort: 3306 
  selector: 
    app: db
Implante o sidecar:
$ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configured 
service/app configured 
configmap/sidecar created 
deployment.apps/db unchanged 
service/db unchanged 
Teste se o sidecar está filtrando o tráfego retornando ao aplicativo e tentando a injeção de SQL novamente. O NGINX bloqueia a solicitação antes que ela chegue ao aplicativo!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- // 
Proteger seu aplicativo como no Desafio 3 é interessante como uma experiência educacional, mas não o recomendamos para produção porque:
Uma solução muito melhor é usar o NGINX Ingress Controller para estender a mesma proteção a todos os seus aplicativos! Os controladores de entrada podem ser usados para centralizar todos os tipos de recursos de segurança, desde o bloqueio de solicitações, como um firewall de aplicativo da Web (WAF), até autenticação e autorização.
Neste desafio, você implanta o NGINX Ingress Controller , configura o roteamento de tráfego e verifica se o filtro bloqueia a injeção de SQL .
A maneira mais rápida de instalar o NGINX Ingress Controller é com o Helm .
Adicione o repositório NGINX ao Helm:
$ helm repo add nginx-stable https://helm.nginx.com/stable  
Baixe e instale o NGINX Open Source‑based NGINX Ingress Controller , que é mantido pela F5 NGINX. Observe o parâmetro enableSnippets=true : snippets são usados para configurar o NGINX para bloquear a injeção de SQL. A linha final de saída confirma a instalação bem-sucedida.
$ helm install main nginx-stable/nginx-ingress \ --set controller.watchIngressWithoutClass=true
--set controller.service.type=NodePort \ 
--set controller.service.httpPort.nodePort=30005 \ 
--set controller.enableSnippets=true
NAME: main  
LAST DEPLOYED: Day Mon DD hh:mm:ss YYYY  
NAMESPACE: default  
STATUS: deployed  
REVISION: 1  
TEST SUITE: None  
NOTES: The NGINX Ingress Controller has been installed.  
Confirme se o pod do NGINX Ingress Controller foi implantado, conforme indicado pelo valor Em execução na coluna STATUS . 
$ kubectl get pods   NAME                                READY   STATUS  ...
main-nginx-ingress-779b74bb8b-mtdkr 1/1     Running ...
      ... RESTARTS   AGE 
      ... 0          18s
Crie um arquivo YAML chamado 3-ingress.yaml com o seguinte conteúdo (ou copie do GitHub ). Ele define o manifesto do Ingress necessário para rotear o tráfego para o aplicativo (não por meio do proxy sidecar desta vez). Observe o bloco annotations: onde um snippet é usado para personalizar a configuração do NGINX Ingress Controller com o mesmo bloco location da definição ConfigMap no Desafio 3: ele rejeita qualquer solicitação que inclua (entre outras sequências de caracteres) SELECT ou UNION .
apiVersion: v1 kind: Service 
metadata: 
  name: app-without-sidecar 
spec: 
  ports: 
    - port: 80 
      targetPort: 80 
  selector: 
    app: app 
--- 
apiVersion: networking.k8s.io/v1 
kind: Ingress 
metadata: 
  name: entry 
  annotations: 
    nginx.org/server-snippets: | 
      location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" { 
          deny all; 
      } 
spec: 
  ingressClassName: nginx 
  rules: 
    - host: "example.com" 
      http: 
        paths: 
          - backend: 
              service: 
                name: app-without-sidecar 
                port: 
                  number: 80 
            path: / 
            pathType: Prefix 
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar created 
ingress.networking.k8s.io/entry created 
Inicie um contêiner BusyBox descartável para emitir uma solicitação ao pod do NGINX Ingress Controller com o nome de host correto.
$ kubectl run -ti --rm=true busybox --image=busybox$ wget --header="Host: example.com" -qO- main-nginx-ingress 
<!DOCTYPE html> 
<html lang="en"> 
<head> 
# ...
Tente a injeção de SQL. O403 O código de status proibido confirma que o NGINX bloqueia o ataque!
$ wget --header="Host: example.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20username,username,password,password,username%20FROM%20users%20where%2 0id=1%20--%20//' 
wget: server returned error: HTTP/1.1 403 Forbidden 
O Kubernetes não é seguro por padrão. Um controlador Ingress pode mitigar vulnerabilidades de injeção de SQL (e muitas outras). Mas tenha em mente que o tipo de funcionalidade semelhante a WAF que você acabou de implementar com o NGINX Ingress Controller não substitui um WAF real, nem é um substituto para a arquitetura segura de aplicativos. Um hacker experiente ainda pode fazer o hack UNION funcionar com algumas pequenas alterações no código. Para mais informações sobre este tópico, consulte o Guia do Pentester para Injeção de SQL (SQLi) .
Dito isso, um controlador Ingress ainda é uma ferramenta poderosa para centralizar a maior parte da sua segurança, levando a maior eficiência e segurança, incluindo casos de uso de autenticação e autorização centralizados (mTLS, logon único) e até mesmo um WAF robusto como o F5 NGINX App Protect WAF .
A complexidade dos seus aplicativos e arquitetura pode exigir um controle mais refinado. Se sua organização exige Zero Trust e criptografia de ponta a ponta , considere uma malha de serviços como a sempre gratuita F5 NGINX Service Mesh para controlar a comunicação entre serviços no cluster Kubernetes (tráfego leste-oeste). Exploramos malhas de serviço na Unidade 4, Estratégias avançadas de implantação do Kubernetes .
Para obter detalhes sobre como obter e implantar o NGINX Open Source, visite nginx.org .
Para experimentar o NGINX Ingress Controller baseado no NGINX Plus com o NGINX App Protect, comece hoje mesmo seu teste gratuito de 30 dias ou entre em contato conosco para discutir seus casos de uso .
Para experimentar o NGINX Ingress Controller baseado no NGINX Open Source, consulte as versões do NGINX Ingress Controller em nosso repositório do GitHub ou baixe um contêiner pré-criado do DockerHub .
"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."