Nota perev. : Presentación de una traducción de un artículo de un ingeniero de seguridad de aplicaciones senior en la compañía británica ASOS.com. Con ella, comienza una serie de publicaciones sobre cómo mejorar la seguridad en Kubernetes mediante el uso de seccomp. Si a los lectores les gusta la introducción, seguiremos al autor y continuaremos con sus futuros materiales sobre este tema.
Este artículo es el primero de una serie de publicaciones sobre cómo crear perfiles seccomp en el espíritu de SecDevOps sin recurrir a la magia y la brujería. En la primera parte, hablaré sobre los conceptos básicos y detalles internos de la implementación de seccomp en Kubernetes.
El ecosistema de Kubernetes ofrece una amplia variedad de formas de garantizar la seguridad y el aislamiento de los contenedores. Este artículo trata sobre el Modo de computación segura, también conocido como
seccomp . Su esencia radica en el sistema de filtrado de llamadas disponibles para que los contenedores se ejecuten.
¿Por qué es esto importante? Un contenedor es solo un proceso que se ejecuta en una máquina específica. Y usa el kernel a la par con otras aplicaciones. Si los contenedores pudieran realizar llamadas al sistema, muy pronto el malware lo aprovecharía para evitar el aislamiento del contenedor y afectar otras aplicaciones: interceptar información, cambiar la configuración del sistema, etc.
Los perfiles seccomp determinan qué llamadas al sistema deben permitirse o denegarse. El tiempo de ejecución del contenedor los activa durante su lanzamiento, de modo que el núcleo puede controlar su ejecución. El uso de tales perfiles le permite limitar el vector de ataque y reducir el daño si algún programa dentro del contenedor (es decir, sus dependencias o sus dependencias) comienza a hacer lo que no está permitido hacer.
Comprender los conceptos básicos.
El perfil base de seccomp incluye tres elementos:
defaultAction
,
architectures
(o
archMap
) y
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" } ] }
( medio-básico-seccomp.json )defaultAction
determina el destino predeterminado de cualquier llamada al sistema no especificada en la sección
syscalls
. Para simplificar la tarea, nos centramos en dos valores principales que se utilizarán:
SCMP_ACT_ERRNO
: bloquea la ejecución de una llamada al sistema,SCMP_ACT_ALLOW
: permite.
La sección de
architectures
enumera las arquitecturas de destino. Esto es importante, ya que el filtro en sí, aplicado a nivel del núcleo, depende de los identificadores de las llamadas al sistema y no de sus nombres registrados en el perfil. Antes de su uso, el tiempo de ejecución del contenedor los asigna a identificadores. El punto es que las llamadas al sistema pueden tener ID completamente diferentes, dependiendo de la arquitectura del sistema. Por ejemplo, la llamada al sistema
recvfrom
(utilizada para obtener información de un socket) tiene ID = 64 en sistemas x64 e ID = 517 en x86.
Aquí puede encontrar una lista de todas las llamadas al sistema para arquitecturas x86-x64.
La sección
syscalls
enumera todas las llamadas al sistema e indica qué hacer con ellas. Por ejemplo, puede crear una lista blanca estableciendo
defaultAction
en
SCMP_ACT_ERRNO
, y asignar llamadas a la sección
SCMP_ACT_ALLOW
a
SCMP_ACT_ALLOW
. Por lo tanto, solo permite llamadas registradas en la sección
syscalls
y prohíbe todas las demás. Para la lista negra, debe cambiar los valores y acciones por
defaultAction
a lo opuesto.
Ahora se deben decir algunas palabras sobre los matices que no son tan obvios. Tenga en cuenta que las siguientes recomendaciones provienen del hecho de que está implementando una línea de aplicaciones comerciales en Kubernetes y es importante que trabajen con los menos privilegios.
1. AllowPrivilegeEscalation = false
Hay un parámetro
AllowPrivilegeEscalation
en el
securityContext
contenedor. Si se establece en
false
, los contenedores comenzarán con el bit
no_new_priv
establecido en (activado). El significado de este parámetro es obvio por el nombre: no permite que el contenedor inicie nuevos procesos con privilegios mayores que los que tiene.
Un efecto secundario de este parámetro establecido en
true
(el valor predeterminado) es que el tiempo de ejecución del contenedor aplica el perfil seccomp al comienzo del proceso de inicio. Por lo tanto, todas las llamadas al sistema necesarias para iniciar los procesos internos del tiempo de ejecución (por ejemplo, establecer identificadores de usuario / grupo, descartar algunas capacidades) deben permitirse en el perfil.
El contenedor que realiza el
echo hi
banal necesitará los siguientes permisos:
{ "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 )... en lugar de estos:
{ "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 )Pero, de nuevo, ¿por qué es esto un problema? Personalmente, evitaría incluir en la lista blanca las siguientes llamadas al sistema (si no son realmente necesarias):
capset
,
set_tid_address
,
setgid
,
setgroups
y
setuid
. Sin embargo, la verdadera dificultad es que al permitir procesos sobre los que no tiene absolutamente ningún control, vincula los perfiles a la implementación del tiempo de ejecución del contenedor. En otras palabras, un día puede encontrar el hecho de que después de actualizar el entorno de tiempo de ejecución del contenedor (por usted o, más probablemente, por el proveedor de servicios en la nube), los contenedores dejarán de iniciarse de repente.
Consejo # 1 : Ejecute contenedores con
AllowPrivilegeEscaltion=false
. Esto reducirá el tamaño de los perfiles seccomp y los hará menos sensibles a los cambios en el tiempo de ejecución del contenedor.
2. Establecer perfiles seccomp a nivel de contenedor
El perfil seccomp se puede configurar a nivel de pod:
annotations: seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"
... o en el nivel del contenedor:
annotations: container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"
Tenga en cuenta que la sintaxis anterior cambiará cuando Kubernetes seccomp se convierta en GA (este evento se espera en la próxima versión de Kubernetes - 1.18 - Transl. Aprox.).Pocas personas saben que Kubernetes siempre tuvo un
error que causaba que los perfiles seccomp se aplicaran al
contenedor de pausa . El tiempo de ejecución compensa parcialmente este inconveniente, pero este contenedor no desaparece de los pods, ya que se utiliza para configurar su infraestructura.
El problema es que este contenedor siempre comienza con
AllowPrivilegeEscalation=true
, lo que lleva a los problemas
AllowPrivilegeEscalation=true
en el párrafo 1, y esto no se puede cambiar.
Al aplicar los perfiles de seccomp a nivel de contenedor, evita esta trampa y puede crear un perfil que se "afinará" para un contenedor específico. Esto tendrá que hacerse hasta que los desarrolladores arreglen el error y la nueva versión (¿tal vez 1.18?) Esté disponible para todos.
Consejo # 2 : Establezca los perfiles de seccomp en el nivel del contenedor.
En un sentido práctico, esta regla generalmente sirve como una respuesta universal a la pregunta: "¿Por qué mi perfil seccomp funciona con
docker run
, pero no funciona después de la implementación en un clúster de Kubernetes?"
3. Use el tiempo de ejecución / predeterminado como último recurso
Kubernetes tiene dos opciones para perfiles integrados:
runtime/default
y
docker/default
. Ambos son implementados por el tiempo de ejecución del contenedor, no por Kubernetes. Por lo tanto, pueden diferir según el tiempo de ejecución utilizado y su versión.
En otras palabras, como resultado de cambiar el tiempo de ejecución, el contenedor puede acceder a otro conjunto de llamadas al sistema que puede usar o no usar. La mayoría de los tiempos de ejecución utilizan
una implementación de Docker . Si desea utilizar este perfil, asegúrese de que le quede bien.
El
docker/default
perfil
docker/default
está en desuso desde Kubernetes 1.11, por lo tanto, evite usarlo.
En mi opinión, el perfil de
runtime/default
es perfecto para el propósito para el que fue creado: para proteger a los usuarios de los riesgos asociados con la
docker run
en sus máquinas. Sin embargo, si hablamos de aplicaciones empresariales que se ejecutan en clústeres de Kubernetes, me atrevería a afirmar que dicho perfil es demasiado abierto y que los desarrolladores deberían concentrarse en crear perfiles para sus aplicaciones (o tipos de aplicaciones).
Consejo # 3 : Cree perfiles seccomp para aplicaciones específicas. Si esto no es posible, trate con perfiles para tipos de aplicaciones, por ejemplo, cree un perfil avanzado que incluya todas las API de aplicaciones web de Golang. Solo como último recurso, use runtime / default.
En futuras publicaciones, le diré cómo crear perfiles secccomp en el espíritu de SecDevOps, automatizarlos y probarlos en canalizaciones. En otras palabras, no tendrá excusas para no cambiar a perfiles para aplicaciones específicas.
4. No confinado NO es una opción
Desde la
primera auditoría de seguridad de Kubernetes, resultó que
seccomp estaba deshabilitado de forma predeterminada. Esto significa que si no especifica una
PodSecurityPolicy
que la habilitará en el clúster, todos los pods para los que el perfil seccomp no esté definido funcionarán en el modo
seccomp=unconfined
.
Trabajar en este modo significa que se pierde toda una capa de aislamiento, lo que proporciona protección de clúster. Este enfoque no es recomendado por los profesionales de seguridad.
Consejo # 4 : Ningún contenedor en un clúster debería funcionar en modo
seccomp=unconfined
, especialmente en entornos de producción.
5. "Modo de auditoría"
Este punto no es exclusivo de Kubernetes, pero aún cae en la categoría de "lo que debe saber antes de comenzar".
Dio la casualidad de que crear perfiles seccomp siempre fue un negocio complicado y se basó en gran medida en prueba y error. El hecho es que los usuarios simplemente no tienen la oportunidad de probarlos en entornos de producción sin correr el riesgo de "descartar" la aplicación.
Después del advenimiento del núcleo Linux 4.14, se hizo posible ejecutar partes del perfil en modo auditoría, registrando información sobre todas las llamadas del sistema en syslog, pero no bloqueándolas. Puede activar este modo utilizando el parámetro
SCMT_ACT_LOG
:
SCMP_ACT_LOG : seccomp no afectará la operación de un subproceso que realiza una llamada al sistema si no se encuentra bajo ninguna regla del filtro, pero se registrará información sobre la llamada al sistema.Aquí hay una estrategia de muestra para usar esta función:
- Permitir llamadas del sistema que sean necesarias.
- Bloquee los sistemas de llamadas que se sabe que no son útiles.
- Registre información sobre todas las demás llamadas en el registro.
Un ejemplo simplificado es el siguiente:
{ "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" } ] }
( medio-mixto-seccomp.json )Pero recuerde que debe bloquear todas las llamadas que se sabe que no se utilizan y que podrían dañar el clúster. Una buena base para el listado es la
documentación oficial de
Docker . Explica en detalle qué llamadas al sistema están bloqueadas en el perfil predeterminado y por qué.
Sin embargo, hay una trampa. Aunque
SCMT_ACT_LOG
compatible con el kernel de Linux desde finales de 2017, solo ha ingresado recientemente al ecosistema de Kubernetes. Por lo tanto, para utilizar este método, necesitará el kernel de Linux 4.14 y la versión runC no inferior a
v1.0.0-rc9 .
Consejo # 5 : Puede crear un perfil de modo de auditoría para probar en producción combinando listas en blanco y negro, y registrar todas las excepciones.
6. Use listas blancas
Crear listas blancas requiere esfuerzos adicionales, ya que debe identificar cada llamada que la aplicación pueda necesitar, pero este enfoque mejora significativamente la seguridad:
Se recomienda encarecidamente que utilice el enfoque de la lista blanca, ya que es más simple y más confiable. La lista negra deberá actualizarse cada vez que se agregue una llamada al sistema potencialmente peligrosa (o una marca / opción peligrosa si están en la lista negra). Además, a menudo puede cambiar la presentación de un parámetro sin cambiar su esencia y así evitar las limitaciones de la lista negra.
Para las aplicaciones Go, desarrollé una herramienta especial que acompaña a la aplicación y recoge todas las llamadas realizadas en tiempo de ejecución. Por ejemplo, para la siguiente aplicación:
package main import "fmt" func main() { fmt.Println("test") }
... ejecuta
gosystract
así:
go install https://github.com/pjbgf/gosystract gosystract --template='{{- range . }}{{printf "\"%s\",\n" .Name}}{{- end}}' application-path
... y obtén el siguiente resultado:
"sched_yield", "futex", "write", "mmap", "exit_group", "madvise", "rt_sigprocmask", "getpid", "gettid", "tgkill", "rt_sigaction", "read", "getpgrp", "arch_prctl",
Hasta ahora, esto es solo un ejemplo: los detalles sobre las herramientas serán más detallados.
Consejo # 6 : Solo permite llamadas que realmente necesites y bloquea a todos los demás.
7. Sentar las bases (o prepararse para un comportamiento inesperado)
El kernel supervisará el cumplimiento del perfil sin importar lo que haya registrado en él. Incluso si esto no es exactamente lo que quería. Por ejemplo, si bloquea el acceso a llamadas como
exit
o
exit_group
, el contenedor no podrá completar el trabajo correctamente e incluso un comando simple como
echo hi
suspenderá por un período indefinido. Como resultado, obtendrá una alta utilización de la CPU en el clúster:

En tales casos, la utilidad
strace
puede venir al rescate; mostrará cuál puede ser el problema:

sudo strace -c -p 9331
Asegúrese de que los perfiles contengan todas las llamadas al sistema que necesita la aplicación mientras se está ejecutando.
Consejo # 7 : Presta atención a las pequeñas cosas y asegúrate de que todas las llamadas al sistema necesarias estén incluidas en la lista blanca.
Con esto, la primera parte de una serie de artículos sobre el uso de seccomp en Kubernetes en el espíritu de SecDevOps llega a su fin. En las siguientes partes hablaremos sobre por qué esto es importante y cómo automatizar el proceso.
PD del traductor
Lea también en nuestro blog: