Tutorial do NGINX: como gerenciar segredos com segurança em contêineres

NGINX | 14 de março de 2023

Este post é um dos quatro tutoriais que ajudam você a colocar em prática os conceitos do Microservices March 2023: comece a disponibilizar microsserviços:

Muitos dos seus microsserviços precisam de segredos para operar com segurança. Exemplos de segredos incluem a chave privada de um certificado SSL/TLS, uma chave de API para autenticação em outro serviço ou uma chave SSH para acesso remoto. O gerenciamento adequado de segredos exige limitar rigorosamente os contextos em que eles são usados apenas aos locais onde são necessários e impedir que sejam acessados quando não houver necessidade. No entanto, essa prática muitas vezes é deixada de lado na pressa do desenvolvimento de aplicações. O resultado? O gerenciamento inadequado de segredos é uma causa comum de vazamentos de informações e ataques.

Visão geral do tutorial

Neste tutorial, mostramos como distribuir e usar com segurança um JSON Web Token (JWT), utilizado por um contêiner cliente para acessar um serviço. Nos quatro desafios deste tutorial, você experimentará quatro métodos diferentes de gerenciamento de segredos para aprender não apenas a gerenciá-los corretamente em seus contêineres, mas também a identificar métodos inadequados:

Embora este tutorial use um JWT como exemplo de segredo, as técnicas se aplicam a qualquer informação de contêineres que você precise manter em sigilo, como credenciais de banco de dados, chaves privadas SSL e outras chaves de API.

O tutorial utiliza dois componentes principais de software:

  • Servidor de API – Um contêiner que executa NGINX Open Source e um código básico em NGINX JavaScript que extrai uma declaração do JWT e retorna o valor de uma das declarações ou, se nenhuma declaração estiver presente, uma mensagem de erro
  • Cliente de API – Um contêiner que executa um código Python muito simples que apenas faz uma solicitação GET ao servidor de API

Assista a este vídeo para ver uma demonstração prática do tutorial.

A maneira mais fácil de fazer este tutorial é se inscrever no Microservices March e usar o laboratório disponibilizado no navegador. Este post fornece instruções para executar o tutorial em seu próprio ambiente.

Pré-requisitos e configuração

Pré-requisitos

Para concluir o tutorial em seu próprio ambiente, você precisa de:

  • Um ambiente compatível com Linux/Unix
  • Conhecimento básico da linha de comando do Linux
  • Um editor de texto como nano ou vim
  • Docker (incluindo Docker Compose e Docker Engine Swarm)
  • curl (já instalado na maioria dos sistemas)
  • git (já instalado na maioria dos sistemas)

Observações:

  • O tutorial usa um servidor de teste que escuta na porta 80. Se você já estiver usando a porta 80, use a opção ‑p para definir outro valor para o servidor de teste ao iniciá-lo com o comando docker run. Em seguida, inclua o sufixo :<port_number> em localhost nos comandos curl.
  • Ao longo do tutorial, o prompt da linha de comando do Linux é omitido para facilitar a cópia e a colagem dos comandos no terminal. O til (~) representa seu diretório pessoal.

Configuração

Nesta seção, você vai clonar o repositório do tutorial, iniciar o servidor de autenticação e enviar solicitações de teste com e sem um token.

Clone o repositório do tutorial

  1. No seu diretório pessoal, crie o diretório microservices-march e clone o repositório do GitHub nele. (Você também pode usar outro nome de diretório e adaptar as instruções conforme necessário.) O repositório inclui arquivos de configuração e versões distintas do aplicativo cliente da API que usam métodos diferentes para obter segredos.
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/auth.git
  1. Exiba o segredo. Ele é um JWT assinado, comumente usado para autenticar clientes de API em servidores.
cat ~/microservices-march/auth/apiclient/token1.jwt"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Embora existam algumas maneiras de usar esse token para autenticação, neste tutorial o aplicativo cliente da API o envia ao servidor de autenticação usando o framework de autorização com token Bearer do OAuth 2.0. Isso envolve adicionar ao JWT o prefixo Authorization: Bearer, como neste exemplo:

"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Compile e inicie o servidor de autenticação

  1. Acesse o diretório do servidor de autenticação:
cd apiserver
  1. Crie a imagem Docker para o servidor de autenticação (observe o ponto final):
docker build -t apiserver .
  1. Inicie o servidor de autenticação e confirme se ele está em execução (a saída está distribuída em várias linhas para facilitar a leitura):
docker run -d -p 80:80 apiserver docker ps
CONTAINER ID   IMAGE       COMMAND                  ...
2b001f77c5cb   apiserver   "nginx -g 'daemon of..." ...  


    ... CREATED         STATUS          ...                                    
    ... 26 seconds ago  Up 26 seconds   ... 


    ... PORTS                                      ...
    ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ...


    ... NAMES
    ... relaxed_proskuriakova

Teste o servidor de autenticação

  1. Verifique se o servidor de autenticação rejeita uma solicitação que não inclui o JWT, retornando 401 Authorization Required:
curl -X GET http://localhost<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.23.3</center>
</body>
</html>
  1. Forneça o JWT usando o cabeçalho Authorization. O código de retorno 200 OK indica que o aplicativo cliente da API foi autenticado com sucesso.
curl -i -X GET -H "Authorization: Bearer `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhostHTTP/1.1 200 OK
Server: nginx/1.23.2
Date: Day, DD Mon YYYY hh:mm:ss TZ
Content-Type: text/html
Content-Length: 64
Last-Modified: Day, DD Mon YYYY hh:mm:ss TZ
Connection: keep-alive
ETag: "63dc0fcd-40"
X-MESSAGE: Success apiKey1
Accept-Ranges: bytes


{ "response": "success", "authorized": true, "value": "999" }

Desafio 1: codifique segredos diretamente no seu aplicativo (só que não!)

Antes de começar este desafio, vamos deixar uma coisa clara: inserir segredos diretamente no código do seu aplicativo é uma péssima ideia! Você verá como qualquer pessoa com acesso à imagem do contêiner pode facilmente encontrar e extrair credenciais inseridas diretamente no código.

Neste desafio, você vai copiar o código do aplicativo cliente da API para o diretório de compilação, compilar e executar o aplicativo, além de extrair o segredo.

Copiar o aplicativo cliente da API

O subdiretório app_versions do diretório apiclient contém diferentes versões do aplicativo cliente de API simples para os quatro desafios, cada uma um pouco mais segura que a anterior (consulte a Visão geral do tutorial para obter mais informações).

  1. Acesse o diretório do cliente da API:
cd ~/microservices-march/auth/apiclient
  1. Copie o aplicativo deste desafio — aquele com um segredo codificado diretamente no código — para o diretório de trabalho:
cp ./app_versions/very_bad_hard_code.py ./app.py
  1. Dê uma olhada no aplicativo:
cat app.pyimport urllib.request
import urllib.error

jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
authstring = "Bearer " + jwt
req = urllib.request.Request("http://host.docker.internal")
req.add_header("Authorization", authstring)
try:
    with urllib.request.urlopen(req) as response:
        the_page = response.read()
        message = response.getheader("X-MESSAGE")
        print("200  " + message)
except urllib.error.URLError as e:
    print(str(e.code) + " s " + e.msg)
  1. O código simplesmente faz uma solicitação a um host local e exibe uma mensagem de sucesso ou um código de falha. A solicitação adiciona o cabeçalho Authorization nesta linha:
req.add_header("Authorization", authstring)
  1. Você percebe mais alguma coisa? Talvez um JWT codificado diretamente no código? Falaremos sobre isso em instantes. Primeiro, vamos compilar e executar o aplicativo.

Compile e execute o aplicativo cliente da API

Estamos usando o comando docker compose junto com um arquivo YAML do Docker Compose – isso facilita um pouco a compreensão do que está acontecendo.

(Observe que, na Etapa 2 da seção anterior, você renomeou o arquivo Python do aplicativo cliente da API específico do Desafio 1 (very_bad_hard_code.py) para app.py. Você também fará isso nos outros três desafios. Usar app.py em todos eles simplifica a logística, pois não é necessário alterar o Dockerfile. Isso significa que você precisa incluir o argumento ‑build no comando docker compose para forçar a recriação do contêiner a cada vez.)

O comando docker compose cria o contêiner, inicia o aplicativo, faz uma única solicitação à API e, em seguida, encerra o contêiner, exibindo no console os resultados da chamada à API.

O código 200 Success na penúltima linha da saída indica que a autenticação foi bem-sucedida. O valor apiKey1 é mais uma confirmação, pois mostra que o servidor de autenticação conseguiu decodificar a declaração com esse nome no JWT:

docker compose -f docker-compose.hardcode.yml up -build
...
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0

Então, as credenciais codificadas diretamente funcionaram corretamente no nosso aplicativo cliente de API — o que não é nenhuma surpresa. Mas será que isso é seguro? Talvez sim, já que o contêiner executa esse script apenas uma vez antes de encerrar e não possui um shell?

Na verdade, não — não é nada seguro.

Recupere o segredo da imagem do contêiner

Inserir credenciais diretamente no código as deixa expostas à inspeção por qualquer pessoa que tenha acesso à imagem do contêiner, pois extrair o sistema de arquivos de um contêiner é uma tarefa trivial.

  1. Crie o diretório extract e acesse-o:
mkdir extractcd extract
  1. Liste as informações básicas sobre as imagens de contêiner. A opção --format torna a saída mais legível (e, pelo mesmo motivo, ela é exibida aqui em duas linhas):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID   NAMES                   IMAGE       ...
11b73106fdf8   apiclient-apiclient-1   apiclient   ...
ad9bdc05b07c   exciting_clarke         apiserver   ...


    ... CREATED          STATUS
    ... 6 minutes ago    Exited (0) 4 minutes ago
    ... 43 minutes ago   Up 43 minutes
  1. Extraia a imagem mais recente do apiclient como um arquivo .tar. Para <container_ID>, substitua pelo valor do campo CONTAINER ID na saída acima (11b73106fdf8 neste tutorial):
docker export -o api.tar <container_ID>
  1. Leva alguns segundos para criar o arquivo api.tar, que inclui todo o sistema de arquivos do contêiner. Uma abordagem para encontrar segredos é extrair o arquivo inteiro e analisá-lo, mas há um atalho para encontrar o que provavelmente é interessante: exibir o histórico do contêiner com o comando docker history. (Esse atalho é especialmente útil porque também funciona para contêineres encontrados no Docker Hub ou em outro registro de contêineres e, portanto, talvez você não tenha o Dockerfile, mas apenas a imagem do contêiner).
  2. Exiba o histórico do contêiner:
docker history apiclient
IMAGE         CREATED        ...
9396dde2aad0  8 minutes ago  ...                    
<missing>     8 minutes ago  ...   
<missing>     28 minutes ago ...  
               
    ... CREATED BY                          SIZE ... 
    ... CMD ["python" "./app.py"]           622B ...   
    ... COPY ./app.py ./app.py # buildkit   0B   ... 
    ... WORKDIR /usr/app/src                0B   ...   
             
    ... COMMENT
    ... buildkit.dockerfile.v0
    ... buildkit.dockerfile.v0
    ... buildkit.dockerfile.v0
  1. As linhas da saída estão em ordem cronológica inversa. Elas mostram que o diretório de trabalho foi definido como /usr/app/src, depois o arquivo de código Python do aplicativo foi copiado e executado. Não é preciso ser um grande detetive para deduzir que o código-fonte principal desse contêiner está em /usr/app/src/app.py e, portanto, esse é um local provável para encontrar credenciais.
  2. Com esse conhecimento em mãos, extraia apenas esse arquivo:
tar --extract --file=api.tar usr/app/src/app.py
  1. Exiba o conteúdo do arquivo e, assim, teremos obtido acesso ao JWT “seguro”:
cat usr/app/src/app.py
...
jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
...

Desafio 2: passar segredos como variáveis de ambiente (de novo, não!)

Se você concluiu a Unidade 1 do Microservices March 2023 (Aplique o aplicativo de doze fatores a arquiteturas de microsserviços), já conhece o uso de variáveis de ambiente para transmitir dados de configuração aos contêineres. Se não participou, não se preocupe: ela está disponível sob demanda após a inscrição.

Neste desafio, você passa segredos como variáveis de ambiente. Assim como o método do Desafio 1, não recomendamos este método! Ele não é tão ruim quanto inserir segredos diretamente no código, mas, como você verá, apresenta algumas vulnerabilidades.

Há quatro maneiras de passar variáveis de ambiente para um contêiner:

  • Use a instrução ENV em um Dockerfile para fazer a substituição de variáveis (definir a variável para todas as imagens criadas). Por exemplo:
ENV PORT $PORT
  • Use a opção ‑e no comando docker run. Por exemplo:
docker run -e PASSWORD=123 mycontainer
  • Use a chave environment em um arquivo YAML do Docker Compose.
  • Use um arquivo .env que contenha as variáveis.

Neste desafio, você usa uma variável de ambiente para definir o JWT e examina o contêiner para verificar se o JWT está exposto.

Passe uma variável de ambiente

  1. Volte para o diretório do cliente da API:
cd ~/microservices-march/auth/apiclient
  1. Copie o aplicativo deste desafio — aquele que usa variáveis de ambiente — para o diretório de trabalho, substituindo o arquivo app.py do Desafio 1:
cp ./app_versions/medium_environment_variables.py ./app.py
  1. Dê uma olhada no aplicativo. Nas linhas relevantes da saída, o segredo (JWT) é lido como uma variável de ambiente no contêiner local:
cat app.py...
jwt = ""
if "JWT" in os.environ:
    jwt = "Bearer " + os.environ.get("JWT")
...
  1. Como explicado acima, há várias maneiras de inserir a variável de ambiente no contêiner. Para manter a consistência, continuaremos usando o Docker Compose. Exiba o conteúdo do arquivo YAML do Docker Compose, que usa a chave environment para definir a variável de ambiente JWT:
cat docker-compose.env.yml---
version: "3.9"
services:
  apiclient:
    build: .
    image: apiclient
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - JWT
  1. Execute o aplicativo sem definir a variável de ambiente. O código 401 Não autorizado na penúltima linha da saída confirma que a autenticação falhou porque o aplicativo cliente da API não enviou o JWT:
docker compose -f docker-compose.env.yml up -build...
apiclient-apiclient-1  | 401 Unauthorized
apiclient-apiclient-1 exited with code 0
  1. Para simplificar, defina a variável de ambiente localmente. Não há problema em fazer isso neste ponto do tutorial, pois essa não é a questão de segurança que nos preocupa no momento:
export JWT=`cat token1.jwt`
  1. Execute o contêiner novamente. Agora, o teste é concluído com sucesso, exibindo a mesma mensagem do Desafio 1:
docker compose -f docker-compose.env.yml up -build
... 
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0

Então, pelo menos agora, a imagem base não contém o segredo, e podemos fornecê-lo em tempo de execução, o que é mais seguro. Mas ainda há um problema.

Examine o contêiner

  1. Exiba informações sobre as imagens de contêiner para obter o ID do contêiner do aplicativo cliente da API (a saída está distribuída em duas linhas para facilitar a leitura):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID   NAMES                   IMAGE      ...
6b20c75830df   apiclient-apiclient-1   apiclient  ...
ad9bdc05b07c   exciting_clarke         apiserver  ...


    ... CREATED             STATUS
    ... 6 minutes ago       Exited (0) 6 minutes ago
    ... About an hour ago   Up About an hour
  1. Inspecione o contêiner do aplicativo cliente da API. Em <container_ID>, substitua pelo valor do campo CONTAINER ID na saída acima (neste caso, 6b20c75830df). O comando docker inspect permite inspecionar todos os contêineres iniciados, estejam eles em execução ou não. E esse é o problema: mesmo que o contêiner não esteja em execução, a saída expõe o JWT no array Env, salvo de forma insegura na configuração do contêiner.
docker inspect <container_ID>...
"Env": [
  "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...",
  "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "LANG=C.UTF-8",
  "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
  "PYTHON_VERSION=3.11.2",
  "PYTHON_PIP_VERSION=22.3.1",
  "PYTHON_SETUPTOOLS_VERSION=65.5.1",
  "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...",
  "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..."
]

Desafio 3: use segredos locais

A esta altura, você já aprendeu que inserir segredos diretamente no código e usar variáveis de ambiente não é tão seguro quanto você (ou sua equipe de segurança) precisa.

Para aumentar a segurança, você pode tentar usar os segredos locais do Docker para armazenar informações confidenciais. Novamente, esse não é o método mais recomendado, mas é importante entender como ele funciona. Mesmo que você não use o Docker em produção, o principal aprendizado é como dificultar a extração do segredo de um contêiner.

No Docker, os segredos são disponibilizados a um contêiner por meio do ponto de montagem do sistema de arquivos /run/secrets/ onde há um arquivo separado contendo o valor de cada segredo.

Neste desafio, você vai passar um segredo armazenado localmente para o contêiner usando o Docker Compose e, em seguida, verificar se o segredo não está visível no contêiner quando esse método é usado.

Passe um segredo armazenado localmente para o contêiner

  1. Como você já deve imaginar, comece acessando o diretório apiclient:
cd ~/microservices-march/auth/apiclient
  1. Copie o aplicativo deste desafio — aquele que usa segredos de dentro de um contêiner — para o diretório de trabalho, substituindo o arquivo app.py do Desafio 2:
cp ./app_versions/better_secrets.py ./app.py
  1. Dê uma olhada no código Python, que lê o valor do JWT do arquivo /run/secrets/jot. (E sim, provavelmente deveríamos verificar se o arquivo contém apenas uma linha. Quem sabe no Microservices March 2024?)
cat app.py...
jotfile = "/run/secrets/jot"
jwt = ""
if os.path.isfile(jotfile):
    with open(jotfile) as jwtfile:
        for line in jwtfile:
            jwt = "Bearer " + line
...
  1. Certo, então como vamos criar esse segredo? A resposta está no arquivo docker-compose.secrets.yml.
  2. Dê uma olhada no arquivo do Docker Compose, no qual o arquivo de segredo é definido na seção secrets e depois referenciado pelo serviço apiclient:
cat docker-compose.secrets.yml---
version: "3.9"
secrets:
  jot:
    file: token1.jwt
services:
  apiclient:
    build: .
    extra_hosts:
      - "host.docker.internal:host-gateway"
    secrets:
      - jot

Verifique se o segredo não está visível no contêiner

  1. Execute o aplicativo. Como tornamos o JWT acessível dentro do contêiner, a autenticação é bem-sucedida e exibe a mensagem já conhecida:
docker compose -f docker-compose.secrets.yml up -build...
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0
  1. Exiba informações sobre as imagens de contêiner, anotando o ID do contêiner do aplicativo cliente da API (para ver um exemplo de saída, consulte a Etapa 1 em Examinar o contêiner do Desafio 2):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  1. Inspecione o contêiner do aplicativo cliente da API. Em <container_ID>, substitua pelo valor do campo CONTAINER ID na saída da etapa anterior. Ao contrário da saída na Etapa 2 de Examinar o contêiner, não há uma linha JWT= no início da seção Env:
docker inspect <container_ID>
"Env": [
  "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "LANG=C.UTF-8",
  "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
  "PYTHON_VERSION=3.11.2",
  "PYTHON_PIP_VERSION=22.3.1",
  "PYTHON_SETUPTOOLS_VERSION=65.5.1",
  "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...",
  "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..."
]
  1. Até aqui, tudo bem, mas nosso segredo está no sistema de arquivos do contêiner em /run/secrets/jot. Talvez possamos extraí-lo de lá usando o mesmo método descrito em Recuperar o segredo da imagem do contêiner do Desafio 1.
  2. Acesse o diretório extract (que você criou durante o Desafio 1) e exporte o contêiner para um arquivo tar:
cd extractdocker export -o api2.tar <container_ID>
  1. Procure o arquivo de segredos no arquivo tar:
tar tvf api2.tar | grep jot-rwxr-xr-x  0 0      0           0 Mon DD hh:mm run/secrets/jot
  1. Ops, o arquivo que contém o JWT está visível. Não dissemos que incorporar segredos ao contêiner era “seguro”? Será que a situação está tão ruim quanto no Desafio 1?
  2. Vamos ver: extraia o arquivo de segredos do arquivo tar e confira seu conteúdo:
tar --extract --file=api2.tar run/secrets/jotcat run/secrets/jot
  1. Boas notícias! Não há nenhuma saída do comando cat, o que significa que o arquivo run/secrets/jot no sistema de arquivos do contêiner está vazio – não há nenhum segredo ali! Mesmo que haja um artefato de segredo em nosso contêiner, o Docker é inteligente o suficiente para não armazenar dados confidenciais no contêiner.

Dito isso, embora essa configuração de contêiner seja segura, ela tem uma limitação. Ela depende da existência de um arquivo chamado token1.jwt no sistema de arquivos local quando você executa o contêiner. Se você renomear o arquivo, a tentativa de reiniciar o contêiner falhará. (Você pode testar isso renomeando [sem excluir!] o arquivo token1.jwt e executando novamente o comando docker compose da Etapa 1.)

Então, estamos no meio do caminho: o contêiner usa segredos de uma forma que os protege contra comprometimentos fáceis, mas o segredo ainda está desprotegido no host. Não é recomendável armazenar segredos sem criptografia em um arquivo de texto simples. É hora de adotar uma ferramenta de gerenciamento de segredos.

Desafio 4: use um gerenciador de segredos

Um gerenciador de segredos ajuda você a gerenciar, recuperar e alternar segredos ao longo de seus ciclos de vida. Há muitos gerenciadores de segredos disponíveis, e todos eles cumprem uma finalidade semelhante:

  • Armazene segredos com segurança
  • Controle o acesso
  • Distribua-os em tempo de execução
  • Habilite a rotação de segredos

Suas opções para o gerenciamento de segredos incluem:

Para simplificar, este desafio usa o Docker Swarm, mas os princípios são os mesmos para muitos gerenciadores de segredos.

Neste desafio, você vai criar um segredo no Docker, copiar o segredo e o código do cliente da API, implantar o contêiner, verificar se consegue extrair o segredo, e rotacionar o segredo.

Configurar um segredo do Docker

  1. Como já é de praxe, acesse o diretório apiclient:
cd ~/microservices-march/auth/apiclient
  1. Inicialize o Docker Swarm:
docker swarm init
Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
...
  1. Crie um segredo e armazene-o em token1.jwt:
docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27
  1. Exiba informações sobre o segredo. Observe que o valor do segredo (o JWT) não é exibido:
docker secret inspect jot[
  {
    "ID": "qe26h73nhb35bak5fr5east27",
    "Version": {
      "Index": 11
    },
    "CreatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
    "UpdatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
    "Spec": {
      "Name": "jot",
      "Labels": {}
    }
  }
]

Use um segredo do Docker

Usar o segredo do Docker no código do aplicativo cliente da API é exatamente igual a usar um segredo criado localmente – você pode lê-lo no sistema de arquivos /run/secrets/. Basta alterar o qualificador do segredo no arquivo YAML do Docker Compose.

  1. Dê uma olhada no arquivo YAML do Docker Compose. Observe o valor true no campo external, que indica que estamos usando um segredo do Docker Swarm:
cat docker-compose.secretmgr.yml---
version: "3.9"
secrets:
  jot:
    external: true
services:
  apiclient:
    build: .
    image: apiclient
    extra_hosts:
      - "host.docker.internal:host-gateway"
    secrets:
      - jot
  1. Portanto, podemos esperar que esse arquivo do Compose funcione com o código existente do nosso aplicativo cliente de API. Bem, quase. Embora o Docker Swarm (ou qualquer outra plataforma de orquestração de contêineres) ofereça muito valor adicional, ele também traz certa complexidade extra. Como o docker compose não funciona com segredos externos, teremos que usar alguns comandos do Docker Swarm, em especial docker stack deploy. O Docker Stack oculta a saída do console, então precisamos gravá-la em um log e depois inspecioná-lo. Para facilitar, também usamos um loop contínuo while True para manter o contêiner em execução.
  2. Copie o aplicativo deste desafio – aquele que usa um gerenciador de segredos – para o diretório de trabalho, substituindo o arquivo app.py do Desafio 3. Ao exibir o conteúdo de app.py, vemos que o código é praticamente idêntico ao do Desafio 3. A única diferença é a adição do loop while True:
cp ./app_versions/best_secretmgr.py ./app.pycat ./app.py
...
while True:
    time.sleep(5)
    try:
        with urllib.request.urlopen(req) as response:
            the_page = response.read()
            message = response.getheader("X-MESSAGE")
            print("200 " + message, file=sys.stderr)
    except urllib.error.URLError as e:
        print(str(e.code) + " " + e.msg, file=sys.stderr)

Implante o contêiner e verifique os logs

  1. Crie o contêiner (nos desafios anteriores, o Docker Compose cuidava disso):
docker build -t apiclient .
  1. Implante o contêiner:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
Creating network secretstack_default
Creating service secretstack_apiclient
  1. Liste os contêineres em execução, anotando o ID do contêiner de secretstack_apiclient (como antes, a saída está distribuída em várias linhas para facilitar a leitura).
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID  ...  
20d0c83a8b86  ... 
ad9bdc05b07c  ... 

    ... NAMES                                             ...  
    ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ...  
    ... exciting_clarke                                   ...                                 

    ... IMAGE              CREATED          STATUS
    ... apiclient:latest   31 seconds ago   Up 30 seconds
    ... apiserver          2 hours ago      Up 2 hours
  1. Exiba o arquivo de log do Docker; em <container_ID>, substitua pelo valor do campo CONTAINER ID na saída da etapa anterior (neste caso, 20d0c83a8b86). O arquivo de log mostra uma série de mensagens de sucesso, pois adicionamos o loop while True ao código da aplicação. Pressione Ctrl+c para encerrar o comando.
docker logs -f <container_ID>200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
...
^c

Tente acessar o segredo

Sabemos que nenhuma variável de ambiente confidencial está definida (mas você sempre pode verificar com o comando docker inspect, como na Etapa 2 de Examinar o contêiner do Desafio 2).

Do Desafio 3, também sabemos que o arquivo /run/secrets/jot está vazio, mas você pode verificar:

cd extractdocker export -o api3.tar  tar --extract --file=api3.tar run/secrets/jot cat run/secrets/jot

Sucesso! Você não consegue obter o segredo do contêiner nem lê-lo diretamente do segredo do Docker.

Rotacionar o segredo

É claro que, com os privilégios adequados, podemos criar um serviço e configurá-lo para registrar o segredo no log ou defini-lo como uma variável de ambiente. Além disso, você talvez tenha percebido que a comunicação entre o cliente e o servidor da nossa API não é criptografada (texto simples).

Portanto, o vazamento de segredos ainda é possível com praticamente qualquer sistema de gerenciamento de segredos. Uma forma de limitar os possíveis danos resultantes é fazer a rotação (substituição) dos segredos regularmente.

Com o Docker Swarm, você só pode excluir e depois recriar os segredos (o Kubernetes permite a atualização dinâmica de segredos). Também não é possível excluir segredos vinculados a serviços em execução.

  1. Liste os serviços em execução:
docker service lsID             NAME                    MODE         ... 
sl4mvv48vgjz   secretstack_apiclient   replicated   ... 


    ... REPLICAS   IMAGE              PORTS
    ... 1/1        apiclient:latest
  1. Exclua o serviço secretstack_apiclient.
docker service rm secretstack_apiclient
  1. Exclua o segredo e recrie-o com um novo token:
docker secret rm jot docker secret create jot ./token2.jwt
  1. Recrie o serviço:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
  1. Consulte o ID do contêiner de apiclient (para ver um exemplo de saída, consulte a Etapa 3 em Implantar o contêiner e verificar os logs):
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  1. Exiba o arquivo de log do Docker, que mostra uma série de mensagens de sucesso. Para <container_ID>, substitua pelo valor do campo CONTAINER ID na saída da etapa anterior. Pressione Ctrl+c para sair do comando.
docker logs -f <container_ID>200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
...
^c

Viu a mudança de apiKey1 para apiKey2? Você fez a rotação do segredo.

Neste tutorial, o servidor de API ainda aceita ambos os JWTs, mas, em um ambiente de produção, você pode descontinuar JWTs mais antigos exigindo determinados valores para as declarações no JWT ou verificando as datas de expiração dos JWTs.

Observe também que, se você estiver usando um sistema de gerenciamento de segredos que permita a atualização do seu segredo, seu código precisará relê-lo com frequência para obter os novos valores.

Limpeza

Para remover os objetos que você criou neste tutorial:

  1. Exclua o serviço secretstack_apiclient.
docker service rm secretstack_apiclient
  1. Exclua o segredo.
docker secret rm jot
  1. Saia do swarm (supondo que você tenha criado um swarm apenas para este tutorial).
docker swarm leave --force
  1. Encerre o contêiner apiserver em execução.
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
  1. Exclua os contêineres indesejados listando-os e, em seguida, removendo-os.
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>
  1. Exclua todas as imagens de contêiner indesejadas listando-as e removendo-as.
docker image list docker image rm <image_ID>

Próximos passos

Você pode usar este blog para seguir o tutorial em seu próprio ambiente ou testá-lo em nosso laboratório baseado em navegador (cadastre-se aqui). Para saber mais sobre como expor serviços do Kubernetes, acompanhe as outras atividades da Unidade 2: Introdução ao gerenciamento de segredos em microsserviços.

Para saber mais sobre autenticação JWT de nível de produção com o NGINX Plus, consulte nossa documentação e leia Autenticação de clientes de API com JWT e NGINX Plus em nosso blog.

Banner com os dizeres “Microservices March 2023: inscreva-se gratuitamente, registre-se hoje”


Compartilhar

Sobre o autor

Publicações relacionadas no blog

Como o F5 NGINX One aprimora o gerenciamento e a observabilidade
NGINX | 10/10/2024

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
NGINX | 17/06/2024

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
NGINX | 14/07/2021

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
NGINX | 02/04/2019

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
NGINX | 24/05/2018

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
NGINX | 19/04/2016

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.