BLOG | NGINX

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

NGINX-Parte-de-F5-horiz-preto-tipo-RGB
Robert Haynes Miniatura
Robert Haynes
Publicado em 14 de março de 2023

Este post é um dos quatro tutoriais que ajudam você a colocar em prática os conceitos do Microservices de março de 2023: Comece a fornecer microsserviços :

Muitos dos seus microsserviços precisam de segredos para operar com segurança. Exemplos de segredos incluem a chave privada para um certificado SSL/TLS, uma chave API para autenticação em outro serviço ou uma chave SSH para login remoto. O gerenciamento adequado de segredos exige limitar rigorosamente os contextos em que os segredos são usados apenas aos lugares onde eles precisam estar e impedir que os segredos sejam acessados, exceto quando necessário. Mas essa prática é frequentemente ignorada na correria do desenvolvimento de aplicativos. O resultado? O gerenciamento inadequado de segredos é uma causa comum de vazamento de informações e explorações.

Visão geral do tutorial

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

Embora este tutorial use um JWT como um segredo de exemplo, as técnicas se aplicam a qualquer coisa para contêineres que você precise manter em segredo, 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 executando o NGINX Open Source e algum código JavaScript básico do NGINX que extrai uma reivindicação do JWT e retorna um valor de uma das reivindicações ou, se nenhuma reivindicação estiver presente, uma mensagem de erro
  • Cliente API – Um contêiner executando um código Python muito simples que simplesmente faz uma solicitação GET ao servidor API

Assista a este vídeo para uma demonstração do tutorial em ação.

A maneira mais fácil de fazer este tutorial é se registrar no Microservices March e usar o laboratório baseado em navegador fornecido. Esta postagem 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:

  • Um ambiente compatível com Linux/Unix
  • Familiaridade básica com a 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)

Notas:

  • O tutorial usa um servidor de teste escutando na porta 80. Se você já estiver usando a porta 80, use o sinalizador ‑p para definir um valor diferente para o servidor de teste ao iniciá-lo com o comando docker run . Em seguida, inclua o :<número_da_porta> sufixo em host local no enrolar comandos.
  • Ao longo do tutorial, o prompt na linha de comando do Linux é omitido para facilitar o corte e a colagem dos comandos no seu terminal. O til ( ~ ) representa seu diretório inicial.

Configurar

Nesta seção, você clona o repositório do tutorial , inicia o servidor de autenticação e envia solicitações de teste com e sem um token.

Clonar o repositório do tutorial

  1. No seu diretório inicial, crie o diretório microservices-march e clone o repositório GitHub nele. (Você também pode usar um nome de diretório diferente e adaptar as instruções adequadamente.) O repositório inclui arquivos de configuração e versões separadas 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
  2. Exiba o segredo. É um JWT assinado, comumente usado para autenticar clientes de API em servidores.

    gato ~/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 passa para o servidor de autenticação usando a estrutura de autorização de token portador do OAuth 2.0 . Isso envolve prefixar o JWT com Autorização: Portador como neste exemplo:

"Autorização: Portador eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Crie e inicie o servidor de autenticação

  1. Alterar para o diretório do servidor de autenticação:

    cd apiserver
  2. Crie a imagem do Docker para o servidor de autenticação (observe o ponto final):

    docker build -t apiserver .
  3. Inicie o servidor de autenticação e confirme se ele está em execução (a saída é distribuída em várias linhas para legibilidade):

    docker run -d -p 80:80 apiserver docker ps COMANDO DE IMAGEM DE ID DO CONTAINER ...
    2b001f77c5cb apiserver "nginx -g 'daemon de..." ... ... STATUS CRIADO ... ... 26 segundos atrás Up 26 segundos ... ... PORTOS ... ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ... ... NOMES ... relaxado_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 401Autorização necessária :

    curl -X OBTER http://localhost<html>
    <head><title>Autorização 401 necessária</title></head>
    <body>
    <center><h1>Autorização 401 necessária</h1></center>
    <hr><center>nginx/1.23.3</center>
    </body>
    </html>
  2. Forneça o JWT usando o cabeçalho Authorization . O200 O código de retorno OK indica que o aplicativo cliente da API foi autenticado com sucesso.

    curl -i -X GET -H "Autorização: Portador `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhost HTTP/1.1 200 OK Servidor: nginx/1.23.2 Data: Dia , DD Seg AAAA hh : mm : ss TZ Tipo de conteúdo: text/html Comprimento do conteúdo: 64 Última modificação: Dia , DD Seg AAAA hh : mm : ss TZ Conexão: keep-alive ETag: "63dc0fcd-40" MENSAGEM X: Sucesso apiKey1 Accept-Ranges: bytes { "response": "success", "authorized": true, "value": "999" }

Desafio 1: Segredos de hardcode em seu aplicativo (não!)

Antes de começar esse desafio, vamos deixar claro: codificar segredos no seu aplicativo é uma péssima ideia! Você verá como qualquer pessoa com acesso à imagem do contêiner pode facilmente encontrar e extrair credenciais codificadas.

Neste desafio, você copia o código do aplicativo cliente da API para o diretório de compilação, cria e executa o aplicativo e extrai o segredo .

Copie 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 ligeiramente mais segura que a anterior (consulte Visão geral do tutorial para obter mais informações).

  1. Alterar para o diretório do cliente da API:

    cd ~/microservices-março/auth/apiclient
  2. Copie o aplicativo para este desafio – aquele com um segredo codificado – para o diretório de trabalho:

    cp ./app_versions/código_rígido_muito_ruim.py ./app.py
  3. Dê uma olhada no aplicativo:

    cat app.py import urllib.request import urllib.error jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" authstring = "Portador " + jwt req = urllib.request.Request("http://host.docker.internal") req.add_header("Autorização", authstring) try: with urllib.request.urlopen(req) como resposta: 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)

    O código simplesmente faz uma solicitação a um host local e imprime uma mensagem de sucesso ou um código de falha.

    A solicitação adiciona o cabeçalho Authorization nesta linha:

    req.add_header("Autorização", authstring)

    Você notou mais alguma coisa? Talvez um JWT codificado? Chegaremos a isso em um minuto. Primeiro, vamos construir e executar o aplicativo.

Crie e execute o aplicativo cliente da API

Estamos usando o comando docker compose junto com um arquivo YAML do Docker Compose – isso torna um pouco mais fácil entender o que está acontecendo.

(Observe que na Etapa 2 da seção anterior você renomeou o arquivo Python para o 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 sempre simplifica a logística porque você não precisa alterar o Dockerfile . Isso significa que você precisa incluir o argumento ‑build no comando docker compose para forçar uma reconstrução do contêiner a cada vez.)

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

O200 O código de sucesso na penúltima linha da saída indica que a autenticação foi bem-sucedida. O valor apiKey1 é uma confirmação adicional, porque mostra que o servidor de autenticação foi capaz de decodificar a reivindicação desse nome no JWT:

docker compose -f docker-compose.hardcode.yml up -build ... apiclient-apiclient-1 | 200 Sucesso apiKey1 apiclient-apiclient-1 saiu com código 0

Portanto, as credenciais codificadas funcionaram corretamente para nosso aplicativo cliente de API – o que não é surpreendente. Mas é seguro? Talvez sim, já que o contêiner executa esse script apenas uma vez antes de sair e não tem um shell?

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

Recupere o segredo da imagem do contêiner

A codificação rígida de credenciais as deixa abertas para inspeção por qualquer pessoa que possa acessar a imagem do contêiner, porque extrair o sistema de arquivos de um contêiner é um exercício trivial.

  1. Crie o diretório de extração e altere para ele:

    mkdir extraircd extrair
  2. Liste informações básicas sobre as imagens do contêiner. O sinalizador --format torna a saída mais legível (e a saída é distribuída em duas linhas aqui pelo mesmo motivo):

    docker ps -a --format "tabela {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" ID DO CONTAINER NOMES IMAGEM ...
    11b73106fdf8 apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... STATUS CRIADO ... 6 minutos atrás Saiu (0) 4 minutos atrás ... 43 minutos atrás Up 43 minutos
  3. Extraia a imagem apiclient mais recente como um arquivo .tar . Para <ID_do_container>, substitua o valor do RECIPIENTE EU IA campo na saída acima (11b73106fdf8 neste tutorial):

    docker export -o api.tar <ID_do_container>

    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 acontece que há um atalho para encontrar o que provavelmente é interessante: exibir o histórico do contêiner com o comando docker history . (Este atalho é especialmente útil porque também funciona para contêineres que você encontra no Docker Hub ou em outro registro de contêiner e, portanto, podem não ter o Dockerfile , mas apenas a imagem do contêiner).

  4. Exibir o histórico do contêiner:

    docker history apiclient IMAGEM CRIADA ...
    9396dde2aad0 8 minutos atrás ...  8 minutos atrás ...  28 minutos atrás ... ... CRIADO POR TAMANHO ... ... CMD ["python" "./app.py"] 622B ... ... COPIAR ./app.py ./app.py # buildkit 0B ... ... WORKDIR /usr/app/src 0B ... ... COMENTÁRIO ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0

    As linhas de saída estão em ordem cronológica inversa. Eles mostram que o diretório de trabalho foi definido como /usr/app/src e, em seguida, o arquivo de código Python para o aplicativo foi copiado e executado. Não é preciso ser um grande detetive para deduzir que o código-base principal deste contêiner está em /usr/app/src/app.py e, portanto, esse é um local provável para credenciais.

  5. Armado com esse conhecimento, extraia apenas esse arquivo:

    tar --extract --file=api.tar usr/app/src/app.py
  6. Exiba o conteúdo do arquivo e, assim, teremos acesso ao JWT “seguro”:

    cat usr/app/src/app.py ... jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" ...

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

Se você concluiu a Unidade 1 de Microsserviços de março de 2023 (Aplicar o aplicativo Twelve‑Factor às arquiteturas de microsserviços), está familiarizado com o uso de variáveis de ambiente para passar dados de configuração para contêineres. Se você perdeu, não se preocupe: ele estará disponível mediante solicitação após o registro .

Neste desafio, você passa segredos como variáveis de ambiente. Assim como o método do Desafio 1 , não recomendamos este! Não é tão ruim quanto codificar segredos, mas, como você verá, tem algumas fraquezas.

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:

    PORTA ENV $PORTA
  • Use o sinalizador ‑e no comando docker run . Por exemplo:

    docker run -e SENHA=123 meucontainer
  • Use a chave de ambiente em um arquivo YAML do Docker Compose.
  • Use um arquivo .env contendo as variáveis.

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

Passar uma Variável de Ambiente

  1. Retorne ao diretório do cliente da API:

    cd ~/microservices-março/auth/apiclient
  2. Copie o aplicativo para este desafio – aquele que usa variáveis de ambiente – para o diretório de trabalho, sobrescrevendo o arquivo app.py do Desafio 1:

    cp ./app_versions/medium_environment_variables.py ./app.py
  3. Dê uma olhada no aplicativo. Nas linhas relevantes de saída, o segredo (JWT) é lido como uma variável de ambiente no contêiner local:

    cat app.py ... jwt = "" se "JWT" em os.environ: jwt = "Portador " + os.environ.get("JWT") ...
  4. Conforme explicado acima, há várias maneiras de colocar a variável de ambiente no contêiner. Para manter a consistência, estamos 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 --- versão: Serviços "3.9": apiclient: build: . image: apiclient extra_hosts: - "host.docker.internal:host-gateway" environment: - JWT
  5. Execute o aplicativo sem definir a variável de ambiente. O401 Código não autorizado na penúltima linha da saída confirma que a autenticação falhou porque o aplicativo cliente da API não passou no JWT:

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 401 O apiclient-apiclient-1 não autorizado saiu com o código 0
  6. Para simplificar, defina a variável de ambiente localmente. Não há problema em fazer isso neste ponto do tutorial, já que não é uma questão de segurança que está em questão agora:

    exportar JWT=`cat token1.jwt`
  7. Execute o contêiner novamente. Agora o teste foi bem-sucedido, com a mesma mensagem do Desafio 1:

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 200 Sucesso apiKey1 apiclient-apiclient-1 saiu com o código 0

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

Examine o recipiente

  1. Exibir informações sobre as imagens do contêiner para obter o ID do contêiner para o aplicativo cliente da API (a saída é distribuída em duas linhas para legibilidade):

    docker ps -a --format "tabela {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" ID DO CONTAINER NOMES IMAGEM ...
    6b20c75830df apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... STATUS CRIADO ... 6 minutos atrás Saiu (0) 6 minutos atrás ... Cerca de uma hora atrás Up Cerca de uma hora atrás
  2. Inspecione o contêiner para o aplicativo cliente da API. Para <ID_do_container>, substitua o valor do RECIPIENTE EU IA campo na saída acima (aqui 6b20c75830df).

    O comando docker inspect permite que você inspecione 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 na matriz Env , salvo de forma insegura na configuração do contêiner.

    Inspeção do docker <ID_do_container>...
    "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", "VERSÃO_PYTHON=3.11.2", "VERSÃO_PYTHON_PIP=22.3.1", "VERSÃO_PYTHON_SETUPTOOLS=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

Agora você já aprendeu que codificar segredos e usar variáveis de ambiente não é tão seguro quanto você (ou sua equipe de segurança) precisa.

Para melhorar a segurança, você pode tentar usar segredos locais do Docker para armazenar informações confidenciais. Novamente, este não é o método padrão ouro, mas é bom entender como ele funciona. Mesmo que você não use o Docker em produção, o importante é como você pode dificultar a extração do segredo de um contêiner.

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

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

Passe um segredo armazenado localmente para o contêiner

  1. Como você deve ter imaginado, comece mudando para o diretório apiclient :

    cd ~/microservices-março/auth/apiclient
  2. Copie o aplicativo para este desafio – aquele que usa segredos de dentro de um contêiner – para o diretório de trabalho, sobrescrevendo o arquivo app.py do Desafio 2:

    cp ./app_versions/better_secrets.py ./app.py
  3. Dê uma olhada no código Python, que lê o valor JWT do arquivo /run/secrets/jot . (E sim, provavelmente deveríamos verificar se o arquivo tem apenas uma linha. Talvez em Microservices em março de 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 ...

    OK, então como vamos criar esse segredo? A resposta está no arquivo docker-compose.secrets.yml .

  4. Dê uma olhada no arquivo Docker Compose, onde o arquivo secreto é definido na seção secrets e então referenciado pelo serviço apiclient :

    cat docker-compose.secrets.yml --- versão: "3.9" segredos: jot: arquivo: token1.jwt serviços: apiclient: build: . extra_hosts: - "host.docker.internal:host-gateway" segredos: - 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 com a mensagem agora familiar:

    docker compose -f docker-compose.secrets.yml up -build ... apiclient-apiclient-1 | 200 Sucesso apiKey1 apiclient-apiclient-1 saiu com o código 0
  2. Exibir informações sobre as imagens do contêiner, observando o ID do contêiner para o aplicativo cliente da API (para obter um exemplo de saída, consulte a Etapa 1 em Examinar o contêiner do Desafio 2):

    docker ps -a --format "tabela {{.ID}}\t{{.Nomes}}\t{{.Imagem}}\t{{.RunningFor}}\t{{.Status}}"
  3. Inspecione o contêiner para o aplicativo cliente da API. Para <ID_do_container>, substitua o valor do RECIPIENTE EU IA campo na saída da etapa anterior. Diferentemente da saída na Etapa 2 de Examinar o contêiner , não há nenhuma linha JWT= no início da seção Env :

    inspeção do docker <ID_do_container>
    "Env": [
    "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "LANG=C.UTF-8",
    "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
    "VERSÃO_PYTHON=3.11.2",
    "VERSÃO_PYTHON_PIP=22.3.1",
    "VERSÃO_PYTHON_SETUPTOOLS=65.5.1",
    "URL_PYTHON_GET_PIP=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...",
    "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..."
    ]

    Até agora, 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 usado em Recuperar o Segredo da Imagem do Contêiner do Desafio 1.

  4. Mude para o diretório de extração (que você criou durante o Desafio 1) e exporte o contêiner para um arquivo tar :

    cd extractdocker export -o api2.tar <ID_do_container>
  5. Procure o arquivo secrets no arquivo tar :

    tar tvf api2.tar | grep jot -rwxr-xr-x 0 0 0 0 Mon DD hh :mm run/secrets/jot

    Opa, o arquivo com o JWT está visível. Não dissemos que incorporar segredos no contêiner era “seguro”? As coisas estão tão ruins quanto no Desafio 1?

  6. Vamos ver – extraia o arquivo secrets do arquivo tar e observe seu conteúdo:

    tar --extract --file=api2.tar executar/segredos/jotcat executar/segredos/jot

    Boas notícias! Não há 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 para ver lá! Mesmo que haja um artefato secreto em nosso contêiner, o Docker é inteligente o suficiente para não armazenar nenhum dado confidencial no contêiner.

Dito isto, embora essa configuração de contêiner seja segura, ela tem uma deficiência. 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 tentar fazer isso renomeando [não excluindo!] token1.jwt e executando o comando docker compose da Etapa 1 novamente.)

Então estamos na metade do caminho: o contêiner usa segredos de uma forma que os protege de comprometimento fácil, mas o segredo ainda está desprotegido no host. Você não quer segredos armazenados sem criptografia em um arquivo de texto simples. É hora de trazer uma ferramenta de gerenciamento de segredos.

Desafio 4: Use um gerenciador de segredos

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

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

Suas opções para 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ê cria um segredo no Docker , copia o segredo e o código do cliente da API , implanta o contêiner , vê se consegue extrair o segredo e rotaciona o segredo .

Configurar um segredo do Docker

  1. Como já é tradição, vá para o diretório apiclient :

    cd ~/microservices-março/auth/apiclient
  2. Inicializar Docker Swarm:

    docker swarm init Swarm inicializado: o nó atual (t0o4eix09qpxf4ma1rrs9omrm) agora é um gerenciador. ...
  3. Crie um segredo e armazene-o em token1.jwt :

    docker segredo criar jot ./token1.jwt qe26h73nhb35bak5fr5east27
  4. Exibir informações sobre o segredo. Observe que o valor secreto (o JWT) não é exibido:

    docker secret inspect jot [ { "ID": "qe26h73nhb35bak5fr5east27", "Versão": { "Índice": 11 }, "CriadoEm": " AAAA - MM - DD T hh : mm : ss . ms Z", "AtualizadoEm": " AAAA - MM - DD T hh : mm : ss . ms Z", "Especificação": { "Nome": "jot", "Rótulos": {} } } ]

Use um segredo do Docker

Usar o segredo do Docker no código do aplicativo cliente da API é exatamente o mesmo que usar um segredo criado localmente – você pode lê-lo no sistema de arquivos /run/secrets/ . Tudo o que você precisa fazer é alterar o qualificador secreto no seu arquivo YAML do Docker Compose.

  1. Dê uma olhada no arquivo YAML do Docker Compose. Observe o valor true no campo externo , indicando que estamos usando um segredo do Docker Swarm:

    cat docker-compose.secretmgr.yml --- versão: "3.9" segredos: jot: externo: verdadeiro serviços: apiclient: construção: . imagem: apiclient extra_hosts: - "host.docker.internal:host-gateway" segredos: - jot

    Portanto, podemos esperar que este arquivo Compose funcione com nosso código de aplicativo cliente de API existente. Bem, quase. Embora o Docker Swarm (ou qualquer outra plataforma de orquestração de contêineres) traga muito valor extra, ele traz alguma complexidade adicional.

    Como o docker compose não funciona com segredos externos, teremos que usar alguns comandos do Docker Swarm, especialmente o docker stack deploy . O Docker Stack oculta a saída do console, então temos que gravar a saída em um log e depois inspecionar o log.

    Para facilitar as coisas, também usamos um loop while True contínuo para manter o contêiner em execução.

  2. Copie o aplicativo para este desafio – aquele que usa um gerenciador de segredos – para o diretório de trabalho, sobrescrevendo o arquivo app.py do Desafio 3. Exibindo o conteúdo de app.py , vemos que o código é quase idêntico ao código 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 (em desafios anteriores, o Docker Compose cuidou disso):

    docker build -t apiclient .
  2. Implante o contêiner:

    docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack Criando rede secretstack_default Criando serviço secretstack_apiclient
  3. Liste os contêineres em execução, anotando o ID do contêiner para secretstack_apiclient (como antes, a saída é distribuída em várias linhas para facilitar a leitura).

    docker ps --format "tabela {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" ID DO CONTAINER ...  
    20d0c83a8b86 ... ad9bdc05b07c ... ... NOMES ... ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ... ... exciting_clarke ... ... STATUS DA IMAGEM CRIADA ... apiclient:latest 31 segundos atrás Up 30 segundos ... apiserver 2 horas atrás Up 2 horas
  4. Exibir o arquivo de log do Docker; para <ID_do_container>, substitua o valor do RECIPIENTE EU IA campo na saída da etapa anterior (aqui, 20d0c83a8b86). O arquivo de log mostra uma série de mensagens de sucesso, porque adicionamos o loop while True ao código do aplicativo. Pressione Ctrl+c para sair do comando.

    registros do docker -f <ID_do_container>200 Sucesso apiKey1
    200 Sucesso apiKey1
    200 Sucesso apiKey1
    200 Sucesso apiKey1
    200 Sucesso apiKey1
    200 Sucesso apiKey1
    200 Sucesso apiKey1
    ...
    ^c

Tente acessar o segredo

Sabemos que nenhuma variável de ambiente sensível está definida (mas você sempre pode verificar com o comando docker inspect, como na Etapa 2 de Examinar o contêiner no 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 executar/segredos/jot
cat executar/segredos/jot

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

Gire o Segredo

É claro que, com os privilégios certos, podemos criar um serviço e configurá-lo para ler o segredo no log ou defini-lo como uma variável de ambiente. Além disso, você deve ter notado que a comunicação entre nosso cliente de API e o servidor não é criptografada (texto simples).

Portanto, o vazamento de segredos ainda é possível com quase qualquer sistema de gerenciamento de segredos. Uma maneira de limitar a possibilidade de danos resultantes é rotacionar (substituir) os segredos regularmente.

Com o Docker Swarm, você só pode excluir e recriar segredos (o Kubernetes permite atualização dinâmica de segredos). Você também não pode excluir segredos anexados a serviços em execução.

  1. Listar os serviços em execução:

    serviço docker ls ID NOME MODO ... sl4mvv48vgjz secretstack_apiclient replicado ... ... PORTAS DE IMAGEM DE RÉPLICAS ... 1/1 apiclient:mais recente
  2. Exclua o serviço secretstack_apiclient .

    serviço docker rm secretstack_apiclient
  3. Exclua o segredo e recrie-o com um novo token:

    segredo do docker rm jot
    segredo do docker criar jot ./token2.jwt
  4. Recrie o serviço:

    pilha docker implantar --compose-file docker-compose.secretmgr.yml pilha secreta
  5. Procure o ID do contêiner para apiclient (para obter um exemplo de saída, consulte a Etapa 3 em Implantar o contêiner e verificar os logs ):

    docker ps --format "tabela {{.ID}}\t{{.Nomes}}\t{{.Imagem}}\t{{.RunningFor}}\t{{.Status}}"
  6. Exiba o arquivo de log do Docker, que mostra uma série de mensagens de sucesso. Para <ID_do_container>, substitua o valor do RECIPIENTE EU IA campo na saída da etapa anterior. Pressione Ctrl+c para sair do comando.

    registros do docker -f <ID_do_container>200 Sucesso apiKey2
    200 Sucesso apiKey2
    200 Sucesso apiKey2
    200 Sucesso apiKey2
    ...
    ^c

Viu a mudança de apiKey1 para apiKey2 ? Você girou o 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 declarações no JWT ou verificando as datas de expiração dos JWTs.

Observe também que se você estiver usando um sistema de segredos que permite que seu segredo seja atualizado, seu código precisa reler o segredo com frequência para obter novos valores secretos.

Limpar

Para limpar os objetos que você criou neste tutorial:

  1. Exclua o serviço secretstack_apiclient .

    serviço docker rm secretstack_apiclient
  2. Apague o segredo.

    docker segredo rm jot
  3. Deixe o enxame (supondo que você criou um enxame apenas para este tutorial).

    docker swarm sair --força
  4. Mate o contêiner apiserver em execução.

    docker ps -a | grep "apiserver" | awk {'imprimir $1'} |xargs docker kill
  5. Exclua contêineres indesejados listando-os e depois excluindo-os.

    docker ps -a --format "tabela {{.ID}}\t{{.Nomes}}\t{{.Imagem}}\t{{.RunningFor}}\t{{.Status}}"docker rm <ID_do_container>
  6. Exclua quaisquer imagens de contêiner indesejadas listando-as e excluindo-as.

    lista de imagens docker imagem docker rm <ID_da_imagem>

PRÓXIMOS PASSOS

Você pode usar este blog para implementar o tutorial em seu próprio ambiente ou experimentá-lo em nosso laboratório baseado em navegador ( registre-se aqui ). Para saber mais sobre o tópico de exposição de serviços do Kubernetes, acompanhe as outras atividades da Unidade 2: Segredos básicos de gerenciamento de microsserviços .

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


"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."