Seccomp no Kubernetes: 7 coisas que você precisa saber desde o início

Observação. trad.: Apresentamos a sua atenção a tradução de um artigo de um engenheiro sênior de segurança de aplicações da empresa britânica ASOS.com. Com ele, ele inicia uma série de publicações dedicadas a melhorar a segurança no Kubernetes por meio do uso do seccomp. Se os leitores gostarem da introdução, seguiremos o autor e continuaremos com seus futuros materiais sobre este tema.

Seccomp no Kubernetes: 7 coisas que você precisa saber desde o início

Este artigo é o primeiro de uma série de postagens sobre como criar perfis seccomp no espírito do SecDevOps, sem recorrer a magia e bruxaria. Na Parte XNUMX, abordarei os fundamentos e detalhes internos da implementação do seccomp no Kubernetes.

O ecossistema Kubernetes oferece uma ampla variedade de maneiras de proteger e isolar contêineres. O artigo é sobre o Modo de Computação Segura, também conhecido como segundo. Sua essência é filtrar as chamadas de sistema disponíveis para execução por containers.

Por que isso é importante? Um contêiner é apenas um processo executado em uma máquina específica. E usa o kernel como outros aplicativos. Se os contêineres pudessem realizar qualquer chamada de sistema, muito em breve o malware aproveitaria isso para contornar o isolamento do contêiner e afetar outros aplicativos: interceptar informações, alterar configurações do sistema, etc.

Os perfis seccomp definem quais chamadas de sistema devem ser permitidas ou desabilitadas. O tempo de execução do contêiner os ativa quando é iniciado para que o kernel possa monitorar sua execução. O uso de tais perfis permite limitar o vetor de ataque e reduzir os danos se algum programa dentro do contêiner (ou seja, suas dependências ou as dependências deles) começar a fazer algo que não é permitido.

Compreendendo o básico

O perfil seccomp básico inclui três elementos: defaultAction, architectures (ou archMap) E syscalls:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(médio-básico-seccomp.json)

defaultAction determina o destino padrão de qualquer chamada de sistema não especificada na seção syscalls. Para facilitar, vamos nos concentrar nos dois principais valores que serão utilizados:

  • SCMP_ACT_ERRNO — bloqueia a execução de uma chamada de sistema,
  • SCMP_ACT_ALLOW - permite.

Na seção architectures as arquiteturas de destino são listadas. Isso é importante porque o filtro em si, aplicado no nível do kernel, depende dos identificadores de chamada do sistema, e não dos nomes especificados no perfil. O tempo de execução do contêiner irá combiná-los com os identificadores antes do uso. A ideia é que as chamadas do sistema possam ter IDs completamente diferentes dependendo da arquitetura do sistema. Por exemplo, chamada de sistema recvfrom (usado para receber informações do soquete) possui ID = 64 em sistemas x64 e ID = 517 em x86. é você pode encontrar uma lista de todas as chamadas de sistema para arquiteturas x86-x64.

Na seção syscalls lista todas as chamadas do sistema e especifica o que fazer com elas. Por exemplo, você pode criar uma lista de permissões definindo defaultAction em SCMP_ACT_ERRNOe chamadas na seção syscalls atribuir SCMP_ACT_ALLOW. Assim, você só permite chamadas especificadas na seção syscallse proibir todos os outros. Para a lista negra você deve alterar os valores defaultAction e ações para o oposto.

Agora devemos dizer algumas palavras sobre nuances que não são tão óbvias. Observe que as recomendações abaixo pressupõem que você está implantando uma linha de aplicativos de negócios no Kubernetes e deseja que eles sejam executados com o mínimo de privilégios possível.

1. AllowPrivilegeEscalation=falso

В securityContext contêiner tem um parâmetro AllowPrivilegeEscalation. Se estiver instalado em false, os contêineres começarão com (on) pedaço no_new_priv. O significado deste parâmetro é óbvio pelo nome: ele evita que o contêiner inicie novos processos com mais privilégios do que ele próprio possui.

Um efeito colateral desta opção ser definida como true (padrão) é que o tempo de execução do contêiner aplique o perfil seccomp logo no início do processo de inicialização. Assim, todas as chamadas do sistema necessárias para executar processos internos de tempo de execução (por exemplo, definir IDs de usuário/grupo, descartar determinados recursos) devem ser habilitadas no perfil.

Para um contêiner que faz coisas triviais echo hi, as seguintes permissões serão necessárias:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "capget",
                "capset",
                "chdir",
                "close",
                "execve",
                "exit_group",
                "fstat",
                "fstatfs",
                "futex",
                "getdents64",
                "getppid",
                "lstat",
                "mprotect",
                "nanosleep",
                "newfstatat",
                "openat",
                "prctl",
                "read",
                "rt_sigaction",
                "statfs",
                "setgid",
                "setgroups",
                "setuid",
                "stat",
                "uname",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-pod-seccomp.json)

...em vez destes:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "brk",
                "close",
                "execve",
                "exit_group",
                "futex",
                "mprotect",
                "nanosleep",
                "stat",
                "write"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

(hi-container-seccomp.json)

Mas, novamente, por que isso é um problema? Pessoalmente, eu evitaria colocar na lista de permissões as seguintes chamadas de sistema (a menos que haja uma necessidade real delas): capset, set_tid_address, setgid, setgroups и setuid. No entanto, o verdadeiro desafio é que, ao permitir processos sobre os quais você não tem absolutamente nenhum controle, você vincula perfis à implementação do tempo de execução do contêiner. Em outras palavras, um dia você poderá descobrir que, depois de atualizar o ambiente de tempo de execução do contêiner (por você ou, mais provavelmente, pelo provedor de serviços de nuvem), os contêineres pararão de funcionar repentinamente.

Dica # 1: Execute contêineres com AllowPrivilegeEscaltion=false. Isto reduzirá o tamanho dos perfis seccomp e os tornará menos sensíveis a alterações no ambiente de tempo de execução do contêiner.

2. Configurando perfis seccomp no nível do contêiner

O perfil seccomp pode ser definido no nível do pod:

annotations:
  seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"

...ou no nível do contêiner:

annotations:
  container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"

Observe que a sintaxe acima mudará quando o Kubernetes seccomp se tornará GA (este evento é esperado na próxima versão do Kubernetes - 1.18 - tradução aproximada).

Poucas pessoas sabem que o Kubernetes sempre teve bugque fez com que os perfis seccomp fossem aplicados a pausar contêiner. O ambiente de execução compensa parcialmente essa deficiência, mas esse contêiner não desaparece dos pods, pois é utilizado para configurar sua infraestrutura.

O problema é que esse contêiner sempre começa com AllowPrivilegeEscalation=true, conduzindo aos problemas expressos no n.º 1, e esta situação não pode ser alterada.

Ao usar perfis seccomp no nível do contêiner, você evita essa armadilha e pode criar um perfil personalizado para um contêiner específico. Isso terá que ser feito até que os desenvolvedores corrijam o bug e a nova versão (talvez 1.18?) esteja disponível para todos.

Dica # 2: defina perfis seccomp no nível do contêiner.

No sentido prático, esta regra geralmente serve como uma resposta universal à pergunta: “Por que meu perfil seccomp funciona com docker runmas não funciona após a implantação em um cluster Kubernetes?

3. Use runtime/default apenas como último recurso

O Kubernetes tem duas opções de perfis integrados: runtime/default и docker/default. Ambos são implementados pelo tempo de execução do contêiner, não pelo Kubernetes. Portanto, eles podem diferir dependendo do ambiente de execução utilizado e de sua versão.

Em outras palavras, como resultado da alteração do tempo de execução, o contêiner pode ter acesso a um conjunto diferente de chamadas de sistema, que ele pode ou não utilizar. A maioria dos tempos de execução usa Implementação do Docker. Se desejar usar este perfil, certifique-se de que ele é adequado para você.

Perfil docker/default está obsoleto desde o Kubernetes 1.11, portanto evite usá-lo.

Na minha opinião, perfil runtime/default perfeitamente adequado ao propósito para o qual foi criado: proteger os usuários dos riscos associados à execução de um comando docker run em seus carros. No entanto, quando se trata de aplicativos de negócios executados em clusters Kubernetes, ouso argumentar que tal perfil é muito aberto e os desenvolvedores deveriam se concentrar na criação de perfis para seus aplicativos (ou tipos de aplicativos).

Dica # 3: crie perfis seccomp para aplicativos específicos. Se isso não for possível, crie perfis para tipos de aplicação, por exemplo, crie um perfil avançado que inclua todas as APIs web da aplicação Golang. Use runtime/default apenas como último recurso.

Em postagens futuras, abordarei como criar perfis seccomp inspirados em SecDevOps, automatizá-los e testá-los em pipelines. Em outras palavras, você não terá desculpa para não atualizar para perfis específicos de aplicativos.

4. Não confinado NÃO é uma opção.

De primeira auditoria de segurança do Kubernetes descobriu-se que por padrão seccomp desabilitado. Isso significa que se você não definir PodSecurityPolicy, que o habilitará no cluster, todos os pods para os quais o perfil seccomp não está definido funcionarão em seccomp=unconfined.

Operar neste modo significa que é perdida toda uma camada de isolamento que protege o cluster. Esta abordagem não é recomendada por especialistas em segurança.

Dica # 4: Nenhum contêiner no cluster deve estar em execução seccomp=unconfined, especialmente em ambientes de produção.

5. "Modo de auditoria"

Este ponto não é exclusivo do Kubernetes, mas ainda se enquadra na categoria “coisas para saber antes de começar”.

Acontece que a criação de perfis seccomp sempre foi um desafio e depende muito de tentativa e erro. O fato é que os usuários simplesmente não têm a oportunidade de testá-los em ambientes de produção sem correr o risco de “descartar” a aplicação.

Após o lançamento do kernel Linux 4.14, tornou-se possível executar partes de um perfil em modo de auditoria, registrando informações sobre todas as chamadas do sistema no syslog, mas sem bloqueá-las. Você pode ativar este modo usando o parâmetro SCMT_ACT_LOG:

SCMP_ACT_LOG: seccomp não afetará o thread que faz a chamada do sistema se não corresponder a nenhuma regra no filtro, mas as informações sobre a chamada do sistema serão registradas.

Aqui está uma estratégia típica para usar esse recurso:

  1. Permitir chamadas de sistema necessárias.
  2. Bloqueie chamadas do sistema que você sabe que não serão úteis.
  3. Registre informações sobre todas as outras chamadas no log.

Um exemplo simplificado é assim:

{
    "defaultAction": "SCMP_ACT_LOG",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "arch_prctl",
                "sched_yield",
                "futex",
                "write",
                "mmap",
                "exit_group",
                "madvise",
                "rt_sigprocmask",
                "getpid",
                "gettid",
                "tgkill",
                "rt_sigaction",
                "read",
                "getpgrp"
            ],
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "names": [
                "add_key",
                "keyctl",
                "ptrace"
            ],
            "action": "SCMP_ACT_ERRNO"
        }
    ]
}

(médio-misto-seccomp.json)

Mas lembre-se de que você precisa bloquear todas as chamadas que você sabe que não serão usadas e que podem prejudicar o cluster. Uma boa base para compilar uma lista é o documento oficial Documentação do Docker. Explica detalhadamente quais chamadas do sistema estão bloqueadas no perfil padrão e por quê.

No entanto, há um problema. Embora SCMT_ACT_LOG suportado pelo kernel Linux desde o final de 2017, ele entrou no ecossistema Kubernetes apenas há relativamente pouco tempo. Portanto, para usar este método você precisará de um kernel Linux 4.14 e versão runC não inferior v1.0.0-rc9.

Dica # 5: um perfil de modo de auditoria para testes em produção pode ser criado combinando listas negras e brancas, e todas as exceções podem ser registradas.

6. Use listas de permissões

A lista de permissões requer esforço adicional porque você precisa identificar cada chamada que o aplicativo possa precisar, mas essa abordagem melhora muito a segurança:

É altamente recomendável usar a abordagem de lista branca, pois é mais simples e confiável. A lista negra precisará ser atualizada sempre que uma chamada de sistema potencialmente perigosa (ou um sinalizador/opção perigosa se estiver na lista negra) for adicionada. Além disso, muitas vezes é possível alterar a representação de um parâmetro sem alterar a sua essência e, assim, contornar as restrições da lista negra.

Para aplicações Go, desenvolvi uma ferramenta especial que acompanha a aplicação e coleta todas as chamadas realizadas durante a execução. Por exemplo, para a seguinte aplicação:

package main

import "fmt"

func main() {
	fmt.Println("test")
}

... vamos lançar gosystract como se segue:

go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf ""%s",n" .Name}}{{- end}}' application-path

... e obtemos o seguinte resultado:

"sched_yield",
"futex",
"write",
"mmap",
"exit_group",
"madvise",
"rt_sigprocmask",
"getpid",
"gettid",
"tgkill",
"rt_sigaction",
"read",
"getpgrp",
"arch_prctl",

Por enquanto, este é apenas um exemplo – mais detalhes sobre as ferramentas virão a seguir.

Dica # 6: permita apenas as chamadas que você realmente precisa e bloqueie todas as outras.

7. Estabeleça as bases certas (ou prepare-se para um comportamento inesperado)

O kernel irá impor o perfil independentemente do que você escrever nele. Mesmo que não seja exatamente o que você queria. Por exemplo, se você bloquear o acesso a chamadas como exit ou exit_group, o contêiner não será capaz de desligar corretamente e até mesmo um comando simples como echo hi pendure-oo por tempo indeterminado. Como resultado, você obterá alto uso de CPU no cluster:

Seccomp no Kubernetes: 7 coisas que você precisa saber desde o início

Nesses casos, uma concessionária pode vir em socorro strace - mostrará qual pode ser o problema:

Seccomp no Kubernetes: 7 coisas que você precisa saber desde o início
sudo strace -c -p 9331

Certifique-se de que os perfis contenham todas as chamadas de sistema que o aplicativo precisa em tempo de execução.

Dica # 7: preste atenção aos detalhes e certifique-se de que todas as chamadas de sistema necessárias estejam na lista de permissões.

Isso conclui a primeira parte de uma série de artigos sobre o uso do seccomp no Kubernetes no espírito do SecDevOps. Nas partes seguintes falaremos sobre por que isso é importante e como automatizar o processo.

PS do tradutor

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário