Seccomp (Secure Computing Mode)
Es un recurso del kernel Linux que permite restringir llamadas de sistema (syscalls) que los procesos pueden hacer. Sería como aplicar un modo de seguridad en el proceso para que solamente pueda hacer llamadas de syscalls exit(), sigreturn(), read() y para descriptores de archivo write() que ya estén abiertos. En caso de intentar cualquier otra llamada, el kernel solo registrará el evento o finalizará el proceso con SIGKILL o SIGSYS.
prctl()?
El prctl (Process Control) es una llamada de sistema de Linux que permite que un proceso configure varios aspectos de su propio comportamiento y de su entorno. Esta llamada se usa para ajustar propiedades relacionadas con el proceso, como seguridad, gestión de señales, y control de recursos.
Usando la llamada prctl() podemos activar el Modo Seccomp (PR_SET_SECCOMP) que restringe las llamadas de syscalls y lo que el proceso puede hacer. Además de activar el modo seccomp es posible utilizar prctl() para configurar gestión de señales, protección de memoria, etc.
Modos Seccomp pueden ser:
- SECCOMP_MODE_DISABLED: Seccomp desactivado.
- SECCOMP_MODE_STRICT: Solo un conjunto muy limitado de syscalls es permitido.
- SECCOMP_MODE_FILTER: Permite la configuración de filtros de syscalls usando la API de filtros seccomp.
Si fuera en 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("Error al activar seccomp con prctl")
if __name__ == "__main__":
try:
getpid() # Función teórica para mostrar el pid del proceso funcionaría
set_seccomp_mode()
print("Seccomp activado con éxito!")
getpid() # No funcionaría más después de activado y el proceso sufriría una terminación forzada
except OSError as e:
print(f"Error: {e}")
Si fuera en golang.
package main
import (
"fmt"
"log"
"golang.org/x/sys/unix"
)
func main() {
getpid() // Función teórica para mostrar el pid del proceso funcionaría
// Activa el seccomp en modo estricto
err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_STRICT)
if err != nil {
log.Fatalf("Error al activar seccomp con prctl: %v", err)
}
fmt.Println("Seccomp activado con éxito!")
getpid() // No funcionaría más y el proceso terminaría con el sigkill
}
Si fuera en rust...
extern crate nix;
use nix::sys::prctl::{self, SeccompMode};
use nix::Error;
fn main() {
getpid() // Función teórica para mostrar el pid del proceso funcionaría
// Activa el seccomp en modo estricto
match prctl::prctl(prctl::PrctlCmd::SetSeccompMode, SeccompMode::Strict as usize) {
Ok(_) => println!("Seccomp activado con éxito!"),
Err(e) => eprintln!("Error al activar seccomp: {}", e),
}
getpid() // No funcionaría
}
Observa que fue añadida una llamada de sistema antes y después de la activación del seccomp para mostrar resultados. Es aconsejable activar el seccomp después de que los servicios de la aplicación estén listos con todos los archivos abiertos.
Claro que existen bibliotecas de seccomp para varios lenguajes y maneras más fáciles de hacer la activación, siendo esta demostración solamente con propósito de mostrar el uso del prctl() activando el seccomp directo en el código. La libseccomp se utiliza para configurar el seccomp añadiendo y eliminando permisos.
Raramente el desarrollador irá "perder tiempo" o tendrá conocimiento sobre esto, además, el código quedaría restringido al entorno Linux.
De esta forma, podemos sortear aplicando el seccomp en un proceso durante su inicialización disminuyendo la preocupación durante el desarrollo.
Seccomp-BPF (Secure Computing Mode with Berkeley Packet Filter)
El seccomp evolucionó y fue combinado con filtros BPF permitiendo el filtrado avanzado de syscalls. Ofrece un control granular sobre qué syscalls un proceso puede ejecutar, permitiendo crear políticas de seguridad detalladas y restrictivas para procesos, especialmente para aquellos ejecutados en contenedores, como los usados en Docker.
-
En seccomp sin BPF en modo SECCOMP_MODE_STRICT el proceso solo puede hacer syscalls muy básicas, como read, write, exit y sigreturn. Cualquier intento de usar otras syscalls resultará en una finalización del proceso.
-
Con Seccomp-BPF, puedes especificar qué syscalls un proceso puede llamar, cuáles deben ser bloqueadas, o cuáles deben resultar en una señal o error específico. La biblioteca mencionada libseccomp permite añadir permisos para más llamadas de syscall.
En golang, una idea de cómo usar.
package main
import (
"fmt"
"log"
"os"
seccomp "github.com/seccomp/libseccomp-golang"
)
func main() {
// La lib ya usa el prctl() para activar el seccomp quedando el desarrollador enfocado en añadir y remover permisos.
// Crea un filtro seccomp con la acción predeterminada de kill
filter, err := seccomp.NewFilter(seccomp.ActKill)
if err != nil {
log.Fatalf("Error al crear filtro seccomp: %v", err)
}
// Añade reglas para permitir las 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("Error al añadir regla de syscall: %v", err)
}
}
// Aplica el filtro seccomp al proceso actual
err = filter.Load()
if err != nil {
log.Fatalf("Error al cargar el filtro seccomp: %v", err)
}
}
Softwares Usando Seccomp
Mirando un poco la seccomp wikipedia podemos ver que varios softwares utilizan seccomp o tienen soporte para él.
Solo algunos:
- Android
- Varios sandboxes
- Docker, LXD para contenedores
- LXD
- Chrome, Firefox
- Snap y Flatpak
- OpenSSH
- etc
Seccomp Contenedores y Kubernetes
Primero debemos tener en mente que el container runtime no consigue alterar el seccomp después de estar en uso. Las reglas predefinidas serán válidas hasta que el contenedor finalice o muera.
Es necesario crear un perfil con todas las syscalls que serán permitidas y estas serán aplicadas al proceso. Este perfil se define en Json.
Vamos a entender la estructura de un perfil 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": "Bloquea la llamada de sistema open()"
}
]
}
Principales secciones:
-
defaultAction: Define la acción predeterminada a ser tomada para cualquier syscall que no esté explícitamente listada en el perfil. SCMP_ACT_ERRNO significa que el proceso recibirá un error (errno) cuando intente ejecutarlas. -
archMap: Arquitectura del sistema en que el perfil debe ser aplicado. -
syscalls: Es la lista de syscalls permitidas. Cada entrada contiene:names: Una lista de syscalls que la regla se aplica.action: Acción a ser tomada para las syscalls listadas.SCMP_ACT_ALLOW: Permite la ejecución de la syscall.SCMP_ACT_ERRNO: Retorna un error errno cuando la syscall es llamada.SCMP_ACT_KILL: Termina el proceso que intentó ejecutar la syscall.SCMP_ACT_TRAP: Envía una señal SIGSYS al proceso.
args: Lista de argumentos para definir reglas condicionales basadas en los argumentos de la syscall. Está vacía en este ejemplo, pero puede ser usada para permitir o bloquear syscalls basadas en sus parámetros.comment(opcional): Un campo de comentario para describir la regla.
Tenemos el siguiente perfil más complejo retirado de la documentación de docker, inclusive utilizado por containerd.
El podman utiliza otro perfil, pero muy parecido retirado del github de podman.
Ahora vamos a crear un contenedor utilizando el seccomp de docker.
# Creando el archivo de perfil con el perfil de docker.
root@cks-worker:~# vim default.json
# Ejecutando el contenedor de nginx para ver si 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
Hasta ahora todo correcto. Elimina el write en la lista de syscalls y haz la prueba nuevamente.
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:
Pasando para Kubernetes...
Es posible configurar el kubelet para utilizar un seccomp predeterminado en todos los pods automáticamente pasando el parámetro --seccomp-default en el kubelet. Para esto es necesario tener certeza de que todos tus workloads funcionan correctamente utilizando el perfil.
Cuando se pasa un perfil específico en un pod él buscará en la carpeta /var/lib/kubelet/seccomp/. Entonces vamos a crear y colocar nuestro perfil allí dentro.
Es necesario que todos los nodos tengan este perfil disponible, o al menos aquellos que van a ejecutar el pod específico.
Podemos definir el seccomp a nivel de pod o por contenedor usando el security context.
# En un nodo worker
root@cks-worker:~/var/lib/kubelet~# mkdir -p /var/lib/kubelet/seccomp
# Colocando el write de vuelta en su 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
# Ahora vamos a ejecutar un contenedor apuntando este perfil.
# En el 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
# ruta para el perfil a partir de la carpeta 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)