Docker Content Trust: o que é e como protege as imagens de contêiner

Sua imagem de contêiner é confiável? Saiba como o Docker Content Trust (DCT) emprega assinaturas digitais para verificação de imagem de contêiner e gerencia coleções confiáveis de conteúdo.

By Brandon Niemczyk (Pesquisador de Segurança)

“Elas são confiáveis?” Um dos principais problemas de segurança que se deve resolver em um sistema baseado em contêiner é validar se suas imagens estão corretas e vieram da fonte certa (ou se foram manipuladas de forma maliciosa). Uma de nossas previsões de segurança para 2020 discutiu como imagens de contêiner maliciosas — se confiáveis — poderiam ter um efeito prejudicial no pipeline da empresa. Relatamos ataques que envolveram a exploração de imagens de contêineres para realizar atividades maliciosas, como varredura de servidores vulneráveis e mineração de criptomoedas.

Para ajudar a resolver isso, o Docker fornece um recurso chamado “Content Trust”. Ele permite que os usuários implantem imagens em um cluster ou swarm com segurança e verifiquem se são as imagens que você espera que sejam. O que o Docker Content Trust (DCT) não faz é monitorar suas imagens em todo o swarm quanto a mudanças ou qualquer coisa dessa natureza. É estritamente uma verificação única feita pelo cliente Docker, não pelo servidor.

Isso tem implicações para a utilidade do DCT como um sistema de monitoramento de integridade total. Em uma postagem anterior de meu colega Magno sobre sistemas nativos da nuvem, ele mencionou o uso de ferramentas de assinatura de imagem, como o “Notary”, para resolver a questão: eles são confiáveis? O DCT é uma tentativa de fornecer ferramentas integradas para clientes Docker fazerem exatamente isso

Este artigo cobrirá quatro áreas

  1. Como funciona o DCT
  2. Como habilitar o DCT
  3. Quais etapas podem ser executadas para automatizar a validação de confiança no pipeline de integração contínua e deploy contínuo (CI/CD)
  4. Quais são as limitações do sistema

Um objetivo adicional deste artigo é fornecer um tutorial in-loco singular sobre como começar a experimentar no DCT, especialmente porque os documentos existentes parecem estar espalhados e não centralizados.

Como funciona o Docker Content Trust (DCT)?

Basicamente, o Docker Content Trust é muito simples. É a lógica dentro do cliente Docker que pode verificar as imagens que você puxa ou implanta de um servidor de registro, assinado em um servidor Docker Notary de sua escolha.

A ferramenta Docker Notary permite que os editores assinem digitalmente suas coleções enquanto os usuários verificam a integridade do conteúdo que extraem. Por meio do The Update Framework (TUF), os usuários do Notary podem fornecer confiança sobre coleções arbitrárias de dados e gerenciar as operações necessárias para garantir a atualização do conteúdo. Se você nunca usou um servidor Notary antes, consulte o guia introdutório do Docker.

O gráfico na Figura 1 mostra como implantar um Docker swarm ou Docker build  permitindo que o cliente converse com o servidor de registro para obter as imagens necessárias e o servidor Notary para ver como elas foram assinadas. Se você tiver a configuração correta das variáveis de ambiente, haverá falha na implantação de imagens não assinadas. A assinatura pode ser feita em uma máquina diferente para que as chaves privadas não precisem ser armazenadas no nó de gerenciamento do Docker usado na implantação.

Figura 1. O cliente Docker pode se comunicar com o servidor de registro e o servidor Notary

Habilitando DCT

Por padrão, o DCT está desabilitado. Precisamos seguir alguns passos para configurá-lo para que possamos assinar as imagens que queremos implantar:

  1. Configurar nosso registro
  2. Configurar um servidor Notary
  3. Enviar uma imagem para o nosso servidor de registro
  4. Assinar a imagem enviada
  5. Habilitar o DCT – defina as variáveis de ambiente corretas em nosso host de gerenciamento para que as assinaturas de imagem sejam verificadas por comandos do Docker

Etapa 1: Configurando nosso servidor de registro

A maneira mais fácil de configurar o servidor de registro é executar a imagem de registro base do Docker Hub. Podemos fazer isso com um único comando (veja abaixo). Certifique-se de expor a porta 5000 porque é nela que o servidor de registro se liga.

ubuntu@ip-{BLOCKED}-20-187:~$ docker run -d -p 5000:5000 --restart always --name registry registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry Digest: sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d Status: Downloaded newer image for registry:2 5df581b6eb4186edeebb40da766e7907427005d387facdb81365df35647d952d

Para validar que está em execução:

ubuntu@ip-{BLOCKED}-20-187:~$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
5df581b6eb41        registry:2          "/entrypoint.sh /etc…"   4 seconds ago       Up 3 seconds        0.0.0.0:5000->5000/tcp   registry

Etapa 2: Configurando um servidor Notary

Além de um servidor de registro para armazenar nossas imagens, precisamos de um servidor Notary para armazenar nossas assinaturas de imagens. Só executar uma imagem de registro do Docker Hub já requer muita configuração, então vamos para a maneira mais simples de fazer isso: clonando o repositório do framework de atualização. Podemos então usar um simples docker-compose up para implantar com seu Dockerfile.

ubuntu@ip-{BLOCKED}-20-187:~$ git clone https://github.com/theupdateframework/notary[.]git
Cloning into 'notary'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 26412 (delta 0), reused 1 (delta 0), pack-reused 26409
Receiving objects: 100% (26412/26412), 35.08 MiB | 5.32 MiB/s, done.
Resolving deltas: 100% (16038/16038), done.
ubuntu@ip-{BLOCKED}-20-187:~$ cd notary/
ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker-compose up -d WARNING: The Docker Engine you're using is running in swarm mode. Compose does not use swarm mode to deploy services to multiple nodes in a swarm. All containers will be scheduled on the current node. To deploy your application across the swarm, use `docker stack deploy`. <….. Too much output … cut out ….> Creating notary_mysql_1 ... Creating notary_mysql_1 ... done Creating notary_signer_1 ... Creating notary_signer_1 ... done Creating notary_server_1 ... Creating notary_server_1 ... done


Deixei a mensagem de aviso sobre não executar no modo swarm de propósito. O arquivo docker-compose.yml fornecido pelo framework de atualização não é compatível com o swarm por alguns motivos:

  • Ele usa a versão: "2" – Isso geralmente pode ser corrigido apenas atualizando para a versão “3”. Outras alterações podem ser necessárias.
  • Ele usa um comando build: – O modo swarm não suporta a operação de construção. Você precisará criar esses serviços separadamente e adicioná-los a um servidor de registro. Uma vez que você não terá assinaturas de confiança de conteúdo neste ponto, você precisará garantir que não está impondo confiança de conteúdo em seu cliente Docker ao implantar este serviço.

Como foi dito antes, a maneira mais fácil de configurar o servidor de registro é executar a imagem base do registro fora do Docker Hub com um único comando. Certifique-se de que a porta 5000 esteja aberta porque é nela que o servidor de registro se liga.

ubuntu@ip-{BLOCKED}-20-187:~$ docker run -d -p 5000:5000 --restart always --name registry registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
Digest: sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d
Status: Downloaded newer image for registry:2
5df581b6eb4186edeebb40da766e7907427005d387facdb81365df35647d952d

Vamos validar que nosso serviço do Notary já está funcionando. Ele também deve ter implantado um serviço MySQL que usa:

ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                             NAMES
6c72520afc2e        notary_server       "/usr/bin/env sh -c …"   12 minutes ago      Up 12 minutes       0.0.0.0:4443->4443/tcp, 0.0.0.0:32769->8080/tcp   notary_server_1
862fea9019c9        notary_signer       "/usr/bin/env sh -c …"   12 minutes ago      Up 12 minutes                                                         notary_signer_1
8c8a05af5224        mariadb:10.4        "docker-entrypoint.s…"   12 minutes ago      Up 12 minutes       3306/tcp                                          notary_mysql_1
5df581b6eb41        registry:2          "/entrypoint.sh /etc…"   21 minutes ago      Up 21 minutes       0.0.0.0:5000->5000/tcp                            registry

Agora, precisamos enviar uma imagem para nosso repositório. Fazemos isso marcando uma imagem com o URL do repositório e, em seguida, chamando o Docker push nessa tag:

ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker pull ubuntu:latest
latest: Pulling from library/ubuntu
54ee1f796a1e: Already exists 
f7bfea53ad12: Already exists 
46d371e02073: Already exists 
b66c17bbf772: Already exists 
Digest: sha256:31dfb10d52ce76c5ca0aa19d10b3e6424b830729e32a89a7c6eee2cda2be67a5
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker tag ubuntu:latest localhost:5000/ubuntu:mine
ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker push localhost:5000/ubuntu:mine
The push refers to repository [localhost:5000/ubuntu]
a4399aeb9a0e: Pushed 
35a91a75d24b: Pushed 
ad44aa179b33: Pushed 
2ce3c188c38d: Pushed 
mine: digest: sha256:6f2fb2f9fb5582f8b587837afd6ea8f37d8d1d9e41168c90f410a6ef15fa8ce5 size: 1152

Para que o cliente Docker saiba como usar este servidor, você precisará definir uma variável de ambiente indicando para ele:

ubuntu@ip-{BLOCKED}-20-187:~/notary$ export DOCKER_CONTENT_TRUST_SERVER=https://localhost:4443

Agora, vamos assinar nossa imagem. Existem três etapas. Primeiro, devemos adicionar uma chave ao Docker que podemos usar para assinar. Em seguida, devemos adicionar essa chave como um signatário para o repositório do Notary para esta imagem, então precisamos assiná-la.

Adicionando a chave:

ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker trust key generate sample_signer
Generating key for sample_signer...
Enter passphrase for new sample_signer key with ID f39f731: 
Repeat passphrase for new sample_signer key with ID f39f731: 
Successfully generated and loaded private key. Corresponding public key available: /home/ubuntu/notary/sample_signer.pub

Adicionando a chave como signatária:

ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker trust signer add --key sample_signer.pub sample_signer localhost:5000/ubuntu:mine
Adding signer "sample_signer" to localhost:5000/ubuntu:mine...
Initializing signed repository for localhost:5000/ubuntu:mine...
You are about to create a new root signing key passphrase. This passphrase
will be used to protect the most sensitive key in your signing system. Please
choose a long, complex passphrase and be careful to keep the password and the
key file itself secure and backed up. It is highly recommended that you use a
password manager to generate the passphrase and keep it safe. There will be no
way to recover this key. You can find the key in your config directory.
Enter passphrase for new root key with ID 65c87b3: 
Repeat passphrase for new root key with ID 65c87b3: 
Enter passphrase for new repository key with ID 10e5763: 
Repeat passphrase for new repository key with ID 10e5763: 
Successfully initialized "localhost:5000/ubuntu:mine"
Successfully added signer: sample_signer to localhost:5000/ubuntu:mine

Agora você pode fazer um docker inspect e ver o signatário adicionado, mas observe que nenhuma tag foi assinada ainda:

ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker trust inspect localhost:5000/ubuntu:mine
[
    {
        "Name": "localhost:5000/ubuntu:mine",
        "SignedTags": [],
        "Signers": [
            {
                "Name": "sample_signer",
                "Keys": [
                    {
                        "ID": "f39f731f1c288b66c10d70905de6d98dfa40104741c878cb2766cddc6ed52f28"
                    }
                ]
            }
        ],
        "AdministrativeKeys": [
            {
                "Name": "Root",
                "Keys": [
                    {
                        "ID": "58617dd8ce70d089e7a2669bc782472e677466278d4519c2de5ec0148b681129"
                    }
                ]
            },
            {
                "Name": "Repository",
                "Keys": [
                    {
                        "ID": "10e5763ffaaa7e1e606575005f460369f9f3ef49e553914b50589f1b822f695b"
                    }
                ]
            }
        ]
    }
]

Por fim, vamos assinar nossa tag :mine.

ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker trust sign localhost:5000/ubuntu:mine
Signing and pushing trust data for local image localhost:5000/ubuntu:mine, may overwrite remote trust data
The push refers to repository [localhost:5000/ubuntu]
a4399aeb9a0e: Layer already exists 
35a91a75d24b: Layer already exists 
ad44aa179b33: Layer already exists 
2ce3c188c38d: Layer already exists 
mine: digest: sha256:6f2fb2f9fb5582f8b587837afd6ea8f37d8d1d9e41168c90f410a6ef15fa8ce5 size: 1152
Signing and pushing trust metadata
Enter passphrase for sample_signer key with ID f39f731: 
Successfully signed localhost:5000/ubuntu:mine

Outra inspeção mostra que a tag :mine foi assinada por sample_signer.

ubuntu@ip-{BLOCKED}-20-187:~/notary$ docker trust inspect localhost:5000/ubuntu:mine
[
    {
        "Name": "localhost:5000/ubuntu:mine",
        "SignedTags": [
            {
                "SignedTag": "mine",
                "Digest": "6f2fb2f9fb5582f8b587837afd6ea8f37d8d1d9e41168c90f410a6ef15fa8ce5",
                "Signers": [
                    "sample_signer"
                ]
            }
        ],
        "Signers": [
            {
                "Name": "sample_signer",
                "Keys": [
                    {
                        "ID": "f39f731f1c288b66c10d70905de6d98dfa40104741c878cb2766cddc6ed52f28"
                    }
                ]
            }
        ],
        "AdministrativeKeys": [
            {
                "Name": "Root",
                "Keys": [
                    {
                        "ID": "58617dd8ce70d089e7a2669bc782472e677466278d4519c2de5ec0148b681129"
                    }
                ]
            },
            {
                "Name": "Repository",
                "Keys": [
                    {
                        "ID": "10e5763ffaaa7e1e606575005f460369f9f3ef49e553914b50589f1b822f695b"
                    }
                ]
            }
        ]
    }
]

Podemos fazer com que o cliente Docker valide se todas as imagens de seu repositório estão assinadas antes da implantação, definindo a variável de ambiente DOCKER_CONTENT_TRUST=1. Lembre-se, se você usar sudo para executar o Docker, precisará usar a flag -E para garantir que as variáveis de ambiente sejam preservadas.

Automatize a validação de confiança no pipeline de CI/CD

Verificar se uma imagem está assinada ou não (e não verificar uma assinatura específica) provavelmente não resolve as necessidades de segurança interna. Ser capaz de inspecionar assinaturas para qualquer imagem em seu repositório, no entanto, torna possível integrar verificações em seu pipeline de CI/CD. Sua equipe pode escrever um código para garantir que imagens específicas sejam assinadas por seus proprietários e que apenas esses proprietários tenham acesso às chaves privadas.

Essa é uma boa maneira de validar se a parte responsável correta aprovou qualquer imagem que está sendo implantada para produção. Isso também torna mais difícil para um invasor tentar implantar uma imagem mal-intencionada dentro do seu swarm, seja por meio de engenharia social ou algum mecanismo técnico.

Uma breve discussão das limitações

Uma das aplicações naturais para um recurso como esse é fornecer monitoramento contínuo da integridade de uma imagem. Seria incrível ser capaz de monitorar uma imagem em busca de alterações “não aprovadas” e te alertar ou agir assim que elas acontecerem. Infelizmente, isso exigiria monitoramento do daemon, kernel e nível do sistema de arquivos, e simplesmente não está no escopo do que o DCT faz como uma implementação “somente-cliente”.

A solução Trend Micro™ Deep Security™ protege hosts e fornece Monitoramento de Integridade para fornecer integridade dos arquivos de configuração Docker e Kubernetes em execução no mesmo host. O Trend Micro Cloud One™ – Container Security tem um recurso que usa seu próprio controlador de admissão para interromper a implantação de contêineres com base nas descobertas do Deep Security Smart Check ou outras configurações de contêiner (como um contêiner privilegiado ou um rodando como root).

A solução Trend Micro™ Hybrid Cloud Security oferece segurança poderosa, simplificada e automatizada dentro do pipeline de DevOps da organização e oferece várias técnicas de defesa contra ameaças XGen™ para proteger workloads físicos, virtuais, serverless e em nuvem em tempo de execução. A Trend Micro Cloud One é uma plataforma de serviços de segurança que fornece às organizações uma visão única simplificada de seus ambientes de nuvem híbrida e segurança em tempo real por meio dos serviços Network Security, Workload Security, Container Security, Application Security, File Storage SecurityConformity.

Para organizações que buscam segurança para workloads em tempo de execução, para imagens de contêiner e para armazenamento de arquivos e objetos em forma de software, o Deep Security Smart Check verifica workloads e imagens de contêiner em busca de malwares e vulnerabilidades em qualquer intervalo no pipeline de desenvolvimento para evitar ameaças antes de serem implantadas.

HIDE

Like it? Add this infographic to your site:
1. Click on the box below.   2. Press Ctrl+A to select all.   3. Press Ctrl+C to copy.   4. Paste the code into your page (Ctrl+V).

Image will appear the same size as you see above.