Nota do tradutor.: Apresentamos a tradução do artigo de um engenheiro sênior de segurança de aplicativos da empresa britânica ASOS.com. Com isso, ele inicia uma série de publicações dedicadas a aumentar a segurança no Kubernetes por meio do uso do seccomp. Se a introdução agradar aos leitores, seguiremos o autor e continuaremos com seus futuros materiais sobre este tema.

Este artigo é o primeiro de uma série de publicações sobre como criar perfis seccomp no espírito do SecDevOps, sem recorrer a mágica e feitiçaria. Na primeira parte, vou falar sobre os fundamentos e detalhes internos da implementação do seccomp no Kubernetes.
O ecossistema Kubernetes oferece uma variedade suficiente de maneiras de garantir a segurança e a isolação dos contêineres. O artigo é dedicado ao Modo de Computação Segura, também conhecido como seccomp. Sua essência consiste na filtragem de chamadas de sistema disponíveis para execução pelos contêineres.
Por que isso é importante? Um contêiner é apenas um processo em execução em uma determinada máquina. E ele utiliza o kernel em conjunto com outros aplicativos. Se os contêineres pudessem realizar quaisquer chamadas de sistema, em breve programas maliciosos aproveitariam isso para contornar a isolação 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 proibidas. O ambiente de execução do contêiner os ativa durante seu início, para que o kernel possa controlar sua execução. A aplicação de perfis desse tipo permite limitar o vetor de ataque e reduzir danos caso algum programa dentro do contêiner (ou seja, suas dependências, ou as dependências delas) comece a fazer o que não é permitido.
Explorando os fundamentos
Um perfil básico de seccomp inclui três elementos: defaultAction, architectures (ou archMap) e além desses dois, utilizei apenas eles.:
{
"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"
}
]
}()
defaultAction define o destino padrão de qualquer chamada de sistema não especificada na seção além desses dois, utilizei apenas eles.. Para simplificar a tarefa, vamos nos concentrar em dois significados principais que serão utilizados:
-
SCMP_ACT_ERRNO— bloqueia a execução da chamada de sistema, -
SCMP_ACT_ALLOW— permite.
Na seção architectures são listadas as arquiteturas-alvo. Isso é importante, pois o próprio filtro aplicado ao nível do núcleo depende dos identificadores das chamadas de sistema, e não dos nomes escritos no perfil. Antes de aplicar, o ambiente de execução do contêiner irá correlacioná-los com os identificadores. A questão é que as chamadas de sistema podem ter IDs completamente diferentes dependendo da arquitetura do sistema. Por exemplo, a chamada de sistema recvfrom (usada para receber informações de um soquete) tem ID = 64 em sistemas x64 e ID = 517 em x86. você pode encontrar a lista de todas as chamadas de sistema para as arquiteturas x86-x64.
Na seção além desses dois, utilizei apenas eles. são listadas todas as chamadas de sistema e indicado o que fazer com elas. Por exemplo, você pode criar uma lista branca, definindo defaultAction para SCMP_ACT_ERRNO, e atribuindo aos chamados na seção além desses dois, utilizei apenas eles. o valor SCMP_ACT_ALLOW. Assim, você está permitindo apenas chamadas escritas na seção além desses dois, utilizei apenas eles., e bloqueando todas as outras. Para a lista negra, você deve inverter os valores defaultAction e as ações.
Agora, é necessário dizer algumas palavras sobre sutilezas que não são tão óbvias. Observe que as recomendações abaixo assumem que você está desenvolvendo uma linha de aplicativos de negócios no Kubernetes e que é importante que eles funcionem com o menor privilégio possível.
1. AllowPrivilegeEscalation=false
Em securityContext o contêiner possui o parâmetro AllowPrivilegeEscalation. Se definido como falso, os contêineres serão executados com o bit definido (ativado) . O sentido deste parâmetro é evidente pelo nome: ele não permite que o contêiner inicie novos processos com privilégios superiores aos que já possui.
O efeito colateral deste parâmetro definido como verdadeiro (valor padrão) é que o ambiente de execução do contêiner aplica o perfil seccomp no início do processo de inicialização. Assim, todas as chamadas de sistema necessárias para iniciar os processos internos do ambiente de execução (por exemplo, definir identificadores de usuário/grupo, descartar algumas capacidades) devem ser permitidas no perfil.
Um contêiner que executa um simples echo hi, precisará das seguintes permissões:
{
"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"
}
]
}()
… 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"
}
]
}()
Mas, novamente, por que isso é um problema? Pessoalmente, eu evitaria incluir nas listas brancas os seguintes chamadas de sistema (a menos que haja uma necessidade real): capset, set_tid_address, setgid, setgroups e setuid. No entanto, a verdadeira complexidade é que, ao permitir processos que você absolutamente não controla, você está ligando os perfis à implementação do ambiente de execução do contêiner. Em outras palavras, um dia você pode se deparar com o fato de que, após uma atualização do ambiente de execução do contêiner (por você ou, mais provavelmente, pelo provedor de serviços em nuvem), os contêineres podem de repente parar de funcionar.
Dica nº 1: Execute contêineres com AllowPrivilegeEscaltion=false. Isso reduzirá o tamanho dos perfis seccomp e os tornará menos sensíveis a alterações do ambiente de execução do contêiner.
2. Definindo 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/: "localhost/profile.json"Observe que a sintaxe acima mudará quando o seccomp do Kubernetes (este evento é esperado na próxima versão do Kubernetes — 1.18 — nota do tradutor).
Poucos sabem que sempre existiu no Kubernetes , que fez com que os perfis seccomp fossem aplicados ao . O ambiente de execução compensa parcialmente essa desvantagem, no entanto, esse contêiner não desaparece dos pods, pois é usado para configurar sua infraestrutura.
O problema é que este contêiner sempre é iniciado com AllowPrivilegeEscalation=true, levando aos problemas mencionados no ponto 1, e isso não pode ser alterado.
Ao aplicar perfis seccomp no nível do contêiner, você evita essa armadilha e pode criar um perfil que será "adaptado" para um contêiner específico. Isso deve ser feito até que os desenvolvedores resolvam o bug e uma nova versão (talvez 1.18?) se torne disponível para todos.
Dica nº 2: Defina perfis seccomp no nível do contêiner.
Na prática, esta regra geralmente serve como uma resposta universal à pergunta: "Por que meu perfil seccomp funciona com docker run, mas não funciona após a implantação no cluster Kubernetes?".
3. Use runtime/default apenas como último recurso
No Kubernetes, existem duas opções de perfis integrados: runtime/default e docker/default. Ambos são implementados pelo ambiente de execução do contêiner, e não pelo Kubernetes. Portanto, podem diferir dependendo do ambiente de execução utilizado e da sua versão.
Em outras palavras, por causa da troca do runtime, o contêiner pode ter acesso a um conjunto diferente de chamadas de sistema, que ele pode ou não usar. A maioria dos ambientes de execução usa . Se você deseja usar esse perfil, certifique-se de que ele seja adequado para você.
O perfil docker/default é considerado obsoleto desde o Kubernetes 1.11, portanto, evite seu uso.
Na minha opinião, o perfil runtime/default é excelente para os fins para os quais foi criado: proteger os usuários de riscos associados à execução do comando docker run em suas máquinas. No entanto, quando se trata de aplicativos empresariais que operam em clusters Kubernetes, eu ousaria afirmar que esse perfil é muito permissivo e os desenvolvedores devem se concentrar na criação de perfis específicos para seus aplicativos (ou tipos de aplicativos).
Dica nº 3: Crie perfis seccomp para aplicativos específicos. Se isso não for possível, dedique-se a perfis para tipos de aplicativos, por exemplo, crie um perfil expandido que inclua todas as API da web da aplicação em Golang. Use runtime/default apenas como um último recurso.
Em publicações futuras, vou mostrar como criar perfis seccomp no espírito do SecDevOps, automatizar e testá-los em pipelines. Em outras palavras, você não terá mais desculpas para não optar por perfis específicos para aplicações.
4. Unconfined — NÃO é uma opção
De ficou claro que por padrão . Isso significa que se você não definir Política de Segurança de Pod, que o ativará no cluster, todos os pods para os quais não for definido um perfil seccomp funcionarão no modo seccomp=unconfined.
Trabalhar nesse modo significa que se perde uma camada inteira de isolamento que protege o cluster. Essa abordagem não é recomendada por especialistas em segurança.
Dica nº 4: Nenhum contêiner no cluster deve operar no modo seccomp=unconfined, especialmente em ambientes de produção.
5. 'Modo de Auditoria'
Esse ponto não é exclusivo do Kubernetes, mas ainda assim se enquadra na categoria 'o que você deve saber antes de começar'.
É uma tradição que criar perfis seccomp sempre foi uma tarefa difícil e, em grande parte, baseada no método de tentativa e erro. O problema é que os usuários simplesmente não têm a possibilidade de testá-los em ambientes de produção, sem correr o risco de 'quebrar' a aplicação.
Com o lançamento do kernel Linux 4.14, surgiu a opção de executar partes do perfil no modo de auditoria, registrando no syslog informações sobre todas as chamadas de sistema, mas sem bloqueá-las. Ativar esse modo pode ser feito usando o parâmetro SCMT_ACT_LOG:
SCMP_ACT_LOG: o seccomp não afetará o funcionamento do thread que faz a chamada de sistema, a menos que esta não se enquadre em alguma regra do filtro, no entanto, as informações sobre a chamada de sistema serão registradas.
Aqui está uma estratégia típica para utilizar essa possibilidade:
- Permitir chamadas de sistema que são necessárias.
- Bloquear chamadas de sistema das quais se saiba que não serão úteis.
- Registrar informações sobre todas as outras chamadas no log.
Um exemplo simplificado é o seguinte:
{
"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"
}
]
}()
Mas lembre-se de bloquear todas as chamadas que se sabe que não serão utilizadas e que podem potencialmente prejudicar o cluster. Uma boa base para a elaboração da lista é a documentação . Ela explica detalhadamente quais chamadas de sistema são bloqueadas no perfil padrão e por quê.
No entanto, há uma armadilha. Embora SCMT_ACT_LOG seja suportado pelo kernel do Linux desde o final de 2017, ele só foi incorporado ao ecossistema Kubernetes relativamente recentemente. Portanto, para usar esse método, serão necessários o kernel do Linux 4.14 e a runC na versão .
Dica nº 5: Um perfil de modo de auditoria para teste em produção pode ser criado combinando listas negras e listas brancas, e todas as exceções registradas em log.
6. Use listas brancas
A formação de listas brancas requer esforços adicionais, pois é preciso identificar cada chamada que pode ser necessária para o aplicativo, mas essa abordagem aumenta consideravelmente a segurança:
Recomenda-se fortemente o uso de uma abordagem baseada em listas brancas, pois é mais simples e confiável. A lista negra precisará ser atualizada toda vez que uma chamada de sistema potencialmente perigosa (ou uma flag/opção perigosa, se estiver na lista negra) for adicionada. Além disso, frequentemente é possível alterar a representação de um parâmetro sem mudar sua essência, contornando assim as restrições da lista negra.
Para aplicativos em Go, eu desenvolvi uma ferramenta especial que acompanha o aplicativo e coleta todas as chamadas feitas durante a execução. Por exemplo, para o seguinte aplicativo:
package main
import "fmt"
func main() {
fmt.Println("test")
} … vamos executar gosystract assim:
go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf "%s", .Name}}{{- end}}' application-path… e teremos 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 — detalhes sobre as ferramentas virão a seguir.
Dica nº 6: Permita apenas as chamadas que você realmente precisa e bloqueie todas as outras.
7. Estabeleça as bases corretas (ou prepare-se para comportamentos inesperados)
O kernel monitorará o cumprimento do perfil, independentemente do que você definiu nele. Mesmo que não seja exatamente o que você queria. Por exemplo, se você bloquear o acesso a chamadas como sigreturn ou exit_group, o contêiner não conseguirá encerrar corretamente, e até mesmo um comando simples como echo hi indefinidamente. Como resultado, você terá uma alta carga de CPU no cluster:

Nesses casos, a utilidade strace pode ajudar — ela mostrará qual pode ser o problema:

sudo strace -c -p 9331
Certifique-se de que os perfis contenham todas as chamadas do sistema necessárias para o aplicativo durante a execução.
Dica nº 7: Preste atenção aos detalhes e verifique se todas as chamadas do sistema necessárias estão na lista branca.
Com isso, a primeira parte do ciclo de artigos sobre o uso do seccomp no Kubernetes no espírito do SecDevOps chega ao fim. Nas próximas partes, falaremos sobre por que isso é importante e como automatizar o processo.
P.S. do tradutor
Confira também em nosso blog:
- «»;
- «»;
- «»;
- «».
Fonte: habr.com
