Pular para o conteúdo principal

Seccomp (Secure Computing Mode)

É um recurso do kernel Linux que permite restringir chamadas de sistema (syscalls) que os processos podem fazer. Seria como aplicar um modo de segurança no processo para que ele somente possa fazer chamadas de syscalls exit(), sigreturn(), read() e para descritores de arquivo write() que já estejam abertos. Caso tente qualquer outra chamada, o kernel apenas registrará o evento ou encerrará o processo com SIGKILL ou SIGSYS.

prctl()?

O prctl (Process Control) é uma chamada de sistema do Linux que permite que um processo configure vários aspectos de seu próprio comportamento e de seu ambiente. Essa chamada é usada para ajustar propriedades relacionadas ao processo, como segurança, gerenciamento de sinais, e controle de recursos.

Usando a chamada prctl() podemos ativar o Modo Seccomp (PR_SET_SECCOMP) que restringe as chamadas de syscalls e o que o processo pode fazer. Além de ativar o modo seccomp é possível utilizar o prctl() para configurar gerenciamento de sinais, proteção de memória, etc.

Modos Seccomp podem ser:

  • SECCOMP_MODE_DISABLED: Seccomp desativado.
  • SECCOMP_MODE_STRICT: Apenas um conjunto muito limitado de syscalls é permitido.
  • SECCOMP_MODE_FILTER: Permite a configuração de filtros de syscalls usando a API de filtros seccomp.

Se fosse em python.

import ctypes
import os

def set_seccomp_mode():
libc = ctypes.CDLL('libc.so.6')
result = libc.prctl(22, 1) # PR_SET_SECCOMP = 22, SECCOMP_MODE_STRICT = 1
if result != 0:
raise OSError("Erro ao ativar seccomp com prctl")

if __name__ == "__main__":
try:
getpid() # Funcao teórica para exibir o pid do processo funcionaria
set_seccomp_mode()
print("Seccomp ativado com sucesso!")
getpid() # Não funcionaria mais depois de ativado e o processo sofreria um termino forçado
except OSError as e:
print(f"Erro: {e}")

Se fosse em golang.

package main

import (
"fmt"
"log"
"golang.org/x/sys/unix"
)

func main() {
getpid() // Função teórica para exibir o pid do processo funcionaria
// Ativa o seccomp em modo estrito
err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_STRICT)
if err != nil {
log.Fatalf("Erro ao ativar seccomp com prctl: %v", err)
}
fmt.Println("Seccomp ativado com sucesso!")
getpid() // Não funcionaria mais e o processo terminaria com o sigkill
}

Se fosse em rust...

extern crate nix;

use nix::sys::prctl::{self, SeccompMode};
use nix::Error;

fn main() {
getpid() // Função teórica para exibir o pid do processo funcionaria
// Ativa o seccomp em modo estrito
match prctl::prctl(prctl::PrctlCmd::SetSeccompMode, SeccompMode::Strict as usize) {
Ok(_) => println!("Seccomp ativado com sucesso!"),
Err(e) => eprintln!("Erro ao ativar seccomp: {}", e),
}
getpid() // Não funcionaria
}

Observe que foi adicionado uma chamada de sistema antes de depois da ativação do seccomp para mostrar resultados. É aconselhavel ativar o seccomp depois que serviços da aplicação estejam prontos com todos os arquivos abertos.

Claro que existem bibliotecas de seccomp para várias linguagem e maneiras mais fáceis de fazer a ativação, sendo essa demonstração somente com propósito mostrar o uso do prctl() ativando o seccomp direto no código. A libseccomp é utilizada para configurar o seccomp adicionando e removendo permissões.

Raramente o desenvolvedor irá "perder tempo" ou ter conhecimento sobre isso, além do mais, o código ficaria restrito ao ambiente Linux.

Desta forma, podemos contornar aplicando o seccomp em um processo durante a sua inicialização diminuindo a preocupação durante o desenvolvimento.

Seccomp-BPF (Secure Computing Mode with Berkeley Packet Filter)

O seccomp evoluiu e foi combinado com filtros BPF permitindo a filtragem avançada syscalls. Ele oferece um controle granular sobre quais syscalls um processo pode executar, permitindo criar políticas de segurança detalhadas e restritivas para processos, especialmente para aqueles executados em containers, como os usados no Docker.

  • No seccomp sem BPF em modo SECCOMP_MODE_STRICT o processo só pode fazer syscalls muito básicas, como read, write, exit e sigreturn. Qualquer tentativa de usar outras syscalls resultará em um encerramento do processo.

  • Com o Seccomp-BPF, você pode especificar quais syscalls um processo pode chamar, quais devem ser bloqueadas, ou quais devem resultar em um sinal ou erro específico. A biblioteca mensionada libseccomp permite adicionar permissões para mais chamadas de syscall.

Em golang, uma idéia de como usar.

package main

import (
"fmt"
"log"
"os"

seccomp "github.com/seccomp/libseccomp-golang"
)

func main() {
// A lib já usa o prctl() para ativar o seccomp ficando o desenvolvedor focado em adicionar e remove permissões.
// Cria um filtro seccomp com a ação padrão de kill
filter, err := seccomp.NewFilter(seccomp.ActKill)
if err != nil {
log.Fatalf("Erro ao criar filtro seccomp: %v", err)
}

// Adiciona regras para permitir as syscalls
syscallsToAllow := []seccomp.ScmpSys{
seccomp.ScmpSys(syscall.SYS_GETPID), // Extra
seccomp.ScmpSys(syscall.SYS_READ),
seccomp.ScmpSys(syscall.SYS_WRITE),
seccomp.ScmpSys(syscall.SYS_EXIT),
seccomp.ScmpSys(syscall.SYS_SIGRETURN),
}

for _, syscall := range syscallsToAllow {
err = filter.AddRule(syscall, seccomp.ActAllow)
if err != nil {
log.Fatalf("Erro ao adicionar regra de syscall: %v", err)
}
}

// Aplica o filtro seccomp ao processo atual
err = filter.Load()
if err != nil {
log.Fatalf("Erro ao carregar o filtro seccomp: %v", err)
}
}

Softwares Usando Seccomp

Olhando um pouco o seccomp wikipedia podemos ver que vários softwares utilizam o seccomp ou tem suporte a ele.

Apenas alguns:

  • Android
  • Vários sandboxes
  • Docker, LXD para container
  • LXD
  • Chrome, Firefox
  • Snap e Flatpak
  • OpenSSH
  • etc

Seccomp Containers e Kubernetes

Primeiramente devemos ter em mente que o container runtime não consegue alterar o seccomp depois que estiver em uso. As regras pré definidas serão válidas até que o container finalize ou morra.

É necessário criar um profile com todas as syscalls que serão permitidas e estas serão aplicadas ao processo. Esse profile é definido em Json.

Vamos entender a estrutura de um profile seccomp.

{
"defaultAction": "SCMP_ACT_ERRNO",
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": [
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
]
},
{
"architecture": "SCMP_ARCH_AARCH64",
"subArchitectures": [
"SCMP_ARCH_ARM"
]
}
],
"syscalls": [
{
"names": ["getpid", "read", "write", "exit", "rt_sigreturn"],
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"names": ["open"],
"action": "SCMP_ACT_ERRNO",
"args": [],
"comment": "Bloqueia a chamada de sistema open()"
}
]
}

Principais sessões:

  • defaultAction: Define a ação padrão a ser tomada para qualquer syscall que não esteja explicitamente listada no profile. SCMP_ACT_ERRNO significa que o processo irá receber um erro (errno) quando tentar executá-las.

  • archMap: Arquitetura do sistema em que o profile deve ser aplicado.

  • syscalls: É a lista de syscalls permitidas. Cada entrada contém:

    • names: Uma lista de syscalls que a regra se aplica.
    • action: Ação a ser tomada para as syscalls listadas.
      • SCMP_ACT_ALLOW: Permite a execução da syscall.
      • SCMP_ACT_ERRNO: Retorna um erro errno quando a syscall é chamada.
      • SCMP_ACT_KILL: Termina o processo que tentou executar a syscall.
      • SCMP_ACT_TRAP: Envia um sinal SIGSYS para o processo.
    • args: Lista de argumentos para definir regras condicionais baseadas nos argumentos da syscall. Está vazia neste exemplo, mas pode ser usada para permitir ou bloquear syscalls baseadas em seus parâmetros.
    • comment (opcional): Um campo de comentário para descrever a regra.

Temos o seguinte profile mais complexo retirado da documentação do docker, inclusive utilizado pelo containerd.

O podman utiliza outro profile, mas bem parecido retirado github do podman.

Agora vamos criar um container utilizando o seccomp do docker.

# Criando o arquivo de profile com o profile do docker.
root@cks-worker:~# vim default.json
# Rodando o container do nginx para ver se funciona
root@cks-worker:~# docker run --security-opt seccomp=default.json nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2024/09/13 13:07:10 [notice] 1#1: using the "epoll" event method
2024/09/13 13:07:10 [notice] 1#1: nginx/1.27.1
2024/09/13 13:07:10 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
2024/09/13 13:07:10 [notice] 1#1: OS: Linux 5.15.0-1067-gcp
2024/09/13 13:07:10 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2024/09/13 13:07:10 [notice] 1#1: start worker processes
2024/09/13 13:07:10 [notice] 1#1: start worker process 29
2024/09/13 13:07:10 [notice] 1#1: start worker process 30
^C2024/09/13 13:07:28 [notice] 1#1: signal 2 (SIGINT) received, exiting
2024/09/13 13:07:28 [notice] 29#29: exiting
2024/09/13 13:07:28 [notice] 29#29: exit
2024/09/13 13:07:28 [notice] 30#30: exiting
2024/09/13 13:07:28 [notice] 30#30: exit
2024/09/13 13:07:28 [notice] 1#1: signal 17 (SIGCHLD) received from 29
2024/09/13 13:07:28 [notice] 1#1: worker process 29 exited with code 0
2024/09/13 13:07:28 [notice] 1#1: worker process 30 exited with code 0

Até então tudo certo. Remova o write na lista de syscalls e faça o teste novamente.

root@cks-worker:~# cat default.json | grep "write"
"pwrite64",
"pwritev",
"pwritev2",
"write", # <<<<<
"writev"
"s390_pci_mmio_write",
"process_vm_writev",

root@cks-worker:~# vim default.json
root@cks-worker:~# cat default.json | grep "write"
"pwrite64",
"pwritev",
"pwritev2",
"writev"
"s390_pci_mmio_write",
"process_vm_writev",

root@cks-worker:~# docker run --security-opt seccomp=default.json nginx
docker: Error response from daemon: OCI runtime start failed: cannot start an already running container: unknown.
ERRO[0000] error waiting for container:

Passando para o Kubernetes...

É possível configurar o kubelet para utilizar um seccomp default em todos os pods automaticamente passando o parâmetro --seccomp-default no kubelet. Para isso é necessário que tenha certeza de que todos os seus workloads funcionam corretamente utilizando o profile.

Quando passado um profile específico em um pod ele irá buscar na pasta /var/lib/kubelet/seccomp/. Então vamos criar e colocar o nosso profile ali dentro.

É necessário que todos os nodes tenham esse profile disponível, ou pelo menos aqueles que irão rodar o pod específico.

Podemos definir o seccomp em nível de pod ou por container usando o security context.

# Em um node worker
root@cks-worker:~/var/lib/kubelet~# mkdir -p /var/lib/kubelet/seccomp
# Colocando o write de volta no lugar.
root@cks-worker:~# vim default.json
root@cks-worker:~# cat default.json | grep write
"pwrite64",
"pwritev",
"pwritev2",
"write",
"writev"
"s390_pci_mmio_write",
"process_vm_writev",
root@cks-worker:~# mv default.json /var/lib/kubelet/seccomp/
root@cks-worker:~# ls /var/lib/kubelet/seccomp/
default.json

# Agora vamos rodar um container apontando esse profile.
# No master...
root@cks-master:~# k run nginx --image=nginx -oyaml --dry-run=client > nginx.yaml

root@cks-master:~# vim nginx.yaml
root@cks-master:~# cat nginx.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
resources: {}
securityContext:
seccompProfile:
type: Localhost
# path para o profile a partir da pasta seccomp
localhostProfile: default.json
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}

root@cks-master:~# k apply -f nginx.yaml
pod/nginx created

root@cks-master:~# k get pods
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 4s

root@cks-master:~# k describe pod nginx
Name: nginx
Namespace: default
Priority: 0
Service Account: default
Node: cks-worker/10.128.0.7
Start Time: Fri, 13 Sep 2024 13:54:22 +0000
Labels: run=nginx
Annotations: cni.projectcalico.org/containerID: e29bb575577eb4f5d7a1686520fcc1375b3efa95c922d07da33a3f68321e4ae0
cni.projectcalico.org/podIP: 192.168.1.19/32
cni.projectcalico.org/podIPs: 192.168.1.19/32
Status: Running
IP: 192.168.1.19
IPs:
IP: 192.168.1.19
Containers:
nginx:
Container ID: containerd://bb31ae8ab0db99ee90bb85dc0ea34a6709879cfb4f356a63dff0050a47c6d0ab
Image: nginx
Image ID: docker.io/library/nginx@sha256:04ba374043ccd2fc5c593885c0eacddebabd5ca375f9323666f28dfd5a9710e3
Port: <none>
Host Port: <none>
SeccompProfile: Localhost # <<<<<
LocalhostProfile: default.json #<<<<<
State: Running
Started: Fri, 13 Sep 2024 13:54:23 +0000
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-mmzj8 (ro)
Conditions:
Type Status
PodReadyToStartContainers True
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-mmzj8:
Type: Projected (a volume that contains injected data from multiple sources)