Nota de traducción.: Presentamos la traducción de un artículo del ingeniero senior de seguridad de aplicaciones de la empresa británica ASOS.com. Con esto, comienza una serie de publicaciones dedicadas a mejorar la seguridad en Kubernetes mediante el uso de seccomp. Si la introducción agrada a los lectores, seguiremos al autor y continuaremos con su futuro material sobre este tema.

Este artículo es el primero de una serie de publicaciones sobre cómo crear perfiles de seccomp al estilo SecDevOps, sin recurrir a magia ni hechicería. En la primera parte, hablaré sobre los fundamentos y los detalles internos de la implementación de seccomp en Kubernetes.
El ecosistema de Kubernetes ofrece una variedad suficiente de maneras para garantizar la seguridad y la separación de los contenedores. El artículo trata sobre el Modo de Computación Segura, también conocido como seccomp. Su esencia radica en filtrar las llamadas al sistema que pueden ser ejecutadas por los contenedores.
¿Por qué es importante? Un contenedor es simplemente un proceso que se ejecuta en una máquina determinada. Y utiliza el núcleo en conjunto con otras aplicaciones. Si los contenedores pudieran ejecutar cualquier llamada al sistema, pronto los programas maliciosos lo aprovecharían para eludir el aislamiento del contenedor y afectar a otras aplicaciones: interceptar información, modificar configuraciones del sistema, etc.
Los perfiles de seccomp definen qué llamadas al sistema deben ser permitidas o prohibidas. El entorno de ejecución del contenedor las activa al momento de su inicio, para que el núcleo pueda controlar su ejecución. La aplicación de tales perfiles permite limitar el vector de ataque y reducir el daño en caso de que algún programa dentro del contenedor (es decir, sus dependencias o las dependencias de estas) comience a hacer lo que no se le permite.
Entendiendo los fundamentos
Un 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"
}
]
}()
defaultAction determina el destino por defecto de cualquier llamada al sistema que no esté especificada en la sección syscalls. Para simplificar la tarea, centrémonos en dos significados principales que se utilizarán:
-
SCMP_ACT_ERRNO— bloquea la ejecución de la llamada al sistema, -
SCMP_ACT_ALLOW— permite.
En la sección architectures se enumeran las arquitecturas objetivo. Esto es importante porque el propio filtro, aplicado a nivel de núcleo, depende de los identificadores de las llamadas al sistema, no de sus nombres escritos en el perfil. Antes de aplicarlo, el entorno de ejecución del contenedor los comparará con los identificadores. El sentido es que las llamadas al sistema pueden tener ID completamente diferentes según la arquitectura del sistema. Por ejemplo, la llamada al sistema recvfrom (que se utiliza para recibir información de un socket) tiene ID = 64 en sistemas x64 e ID = 517 en x86. puede encontrar una lista de todas las llamadas al sistema para las arquitecturas x86-x64.
En la sección syscalls se enumeran todas las llamadas al sistema y se indica qué hacer con ellas. Por ejemplo, se puede crear una lista blanca estableciendo defaultAction en SCMP_ACT_ERRNO, y asignar a las llamadas en la sección syscalls el valor de SCMP_ACT_ALLOW. De este modo, solo se permiten las llamadas especificadas en la sección syscalls, y se prohíben todas las demás. Para la lista negra, los valores deben invertirse defaultAction y las acciones ser opuestas.
Ahora deberíamos mencionar un par de detalles que no son tan evidentes. Tenga en cuenta que las recomendaciones a continuación suponen que está implementando una línea de aplicaciones empresariales en Kubernetes y es importante que funcionen con los mínimos privilegios.
1. AllowPrivilegeEscalation=false
En securityContext el contenedor tiene el parámetro AllowPrivilegeEscalation. Si está configurado en false, los contenedores se ejecutarán con el bit establecido (on) . El sentido de este parámetro es evidente por su nombre: no permite que el contenedor inicie nuevos procesos con privilegios mayores que los que ya posee.
El efecto secundario de este parámetro, cuando se establece en true (valor por defecto), es que el runtime del contenedor aplica el perfil seccomp al inicio del proceso de arranque. Así, todas las llamadas al sistema necesarias para iniciar los procesos internos del entorno de ejecución (por ejemplo, establecer identificadores de usuario/grupo, descartando algunas capacidades) deben estar permitidas en el perfil.
A un contenedor que ejecuta el simple echo hi, se le requerirán 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"
}
]
}()
… 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"
}
]
}()
Pero, ¿por qué es un problema? Personalmente, evitaría agregar a la lista blanca los siguientes llamados del sistema (a menos que sea absolutamente necesario): capset, set_tid_address, setgid, setgroups y setuid. Sin embargo, la verdadera complejidad radica en que, al permitir que procesos que no controla en absoluto sean ejecutados, vincula los perfiles a la implementación del entorno de ejecución de contenedores. En otras palabras, podría darse el caso de que, después de una actualización del entorno de ejecución del contenedor (ya sea por usted o, más probablemente, por un proveedor de servicios en la nube), los contenedores dejen de funcionar repentinamente.
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 entorno de ejecución del contenedor.
2. Fijar perfiles seccomp a nivel de contenedor
El perfil seccomp se puede asignar a nivel de pod:
annotations:
seccomp.security.alpha.kubernetes.io/pod: "localhost/profile.json"… o a nivel de contenedor:
annotations:
container.security.alpha.kubernetes.io/<container-name>: "localhost/profile.json"Tenga en cuenta que la sintaxis anterior cambiará cuando seccomp de Kubernetes (este evento se espera en la próxima versión de Kubernetes — 1.18 — nota del traductor).
Pocas personas saben que siempre ha habido un , que hacía que los perfiles seccomp se aplicaran al . El entorno de ejecución compensa parcialmente esta deficiencia, sin embargo, este contenedor no desaparece de los pods, ya que se utiliza para configurar su infraestructura.
El problema es que este contenedor siempre se inicia con AllowPrivilegeEscalation=true, lo que lleva a los problemas mencionados en el punto 1, y no se puede modificar.
Al aplicar perfiles seccomp a nivel de contenedor, evitas esta trampa y puedes crear un perfil que esté "ajustado" a un contenedor específico. Tendrás que hacerlo hasta que los desarrolladores solucionen el error y una nueva versión (quizás la 1.18) esté disponible para todos los interesados.
Consejo Nº2: Asigna perfiles seccomp a nivel de contenedor.
En un sentido práctico, esta regla suele servir como respuesta universal a la pregunta: "¿Por qué mi perfil seccomp funciona con docker run, pero no funciona después de implementarlo en el clúster de Kubernetes?".
3. Utiliza runtime/default solo en caso extremo
En Kubernetes hay dos opciones de perfiles incorporados: runtime/default y docker/default. Ambos son implementados por el entorno de ejecución de contenedores, no por Kubernetes. Por lo tanto, pueden diferir dependiendo del entorno de ejecución y su versión que estés utilizando.
En otras palabras, como resultado del cambio de runtime, el contenedor puede acceder a un conjunto diferente de llamadas al sistema que puede usar o no usar. La mayoría de los entornos de ejecución usan . Si deseas utilizar este perfil, asegúrate de que se adapte a tus necesidades.
El perfil docker/default se considera obsoleto desde Kubernetes 1.11, por lo que se debe evitar su uso.
En mi opinión, el perfil runtime/default es bastante adecuado para los propósitos para los que fue creado: proteger a los usuarios de los riesgos asociados con la ejecución del comando docker run en sus máquinas. Sin embargo, hablando de aplicaciones empresariales en clústeres de Kubernetes, me atrevería a afirmar que dicho perfil es demasiado abierto y los desarrolladores deberían concentrarse en crear perfiles para sus aplicaciones (o tipos de aplicaciones).
Consejo Nº3: Crea perfiles seccomp para aplicaciones específicas. Si eso no es posible, trabaja en perfiles para tipos de aplicaciones, por ejemplo, crea un perfil ampliado que incluya todas las API web de la aplicación en Golang. Utiliza runtime/default solo como último recurso.
En futuras publicaciones, explicaré cómo crear perfiles de seccomp al estilo SecDevOps, automatizarlos y probarlos en los pipelines. En otras palabras, no tendrás excusas para no cambiar a perfiles específicos para aplicaciones.
4. Unconfined NO es una opción
De resultó que de forma predeterminada . Esto significa que si no estableces PodSecurityPolicy, que lo habilite en el clúster, todos los pods para los que no se especifica un perfil de seccomp funcionarán en modo seccomp=unconfined.
Operar en tal modo significa que se pierde una capa completa de aislamiento que proporciona protección al clúster. Este enfoque no es recomendado por los especialistas en seguridad.
Consejo Nº 4: Ningún contenedor en el clúster debe operar 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 así cae en la categoría de "lo que se debe saber antes de comenzar".
Ha sido tradicional que la creación de perfiles de seccomp siempre ha sido una tarea complicada y se basa en gran medida en el método de prueba y error. El problema es que los usuarios simplemente no tienen la oportunidad de probarlos en entornos de producción sin arriesgarse a "caer" la aplicación.
Con la llegada del núcleo de Linux 4.14, se introdujo la posibilidad de ejecutar partes del perfil en modo de auditoría, registrando en syslog información sobre todas las llamadas al sistema, pero sin bloqueándolas. Este modo se puede activar con el parámetro SCMT_ACT_LOG:
SCMP_ACT_LOG: seccomp no afectará la operación del hilo que hace la llamada al sistema, si no entra dentro de ninguna regla del filtro, sin embargo, la información sobre la llamada al sistema se registrará.
Aquí hay una estrategia típica para utilizar esta capacidad:
- Permitir las llamadas al sistema que sean necesarias.
- Bloquear las llamadas al sistema que se sabe que no serán útiles.
- Registrar información sobre todas las demás llamadas en el registro.
Un ejemplo simplificado se ve así:
{
"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"
}
]
}()
Pero recuerde que es necesario bloquear todas las llamadas que se sabe que no se utilizarán y que pueden dañar el clúster. Una buena base para la elaboración de la lista es la documentación oficial . En ella se explica detalladamente qué llamadas al sistema están bloqueadas en el perfil predeterminado y por qué.
Sin embargo, hay una trampa. Aunque SCMT_ACT_LOG es compatible con el núcleo de Linux desde finales de 2017, entró en el ecosistema de Kubernetes relativamente recientemente. Por lo tanto, para utilizar este método se necesita un núcleo de Linux 4.14 y runC de versión no inferior a .
Consejo n.º 5: Se puede crear un perfil de modo de auditoría para pruebas en producción combinando listas negras y listas blancas, registrando todas las excepciones.
6. Utilice listas blancas
La creación de listas blancas requiere esfuerzos adicionales, ya que es necesario identificar cada llamada que puede ser necesaria para la aplicación, sin embargo, este enfoque mejora considerablemente la seguridad:
Se recomienda encarecidamente utilizar un enfoque basado en listas blancas, ya que es más sencillo y seguro. La lista negra deberá actualizarse cada vez que se agregue una llamada al sistema potencialmente peligrosa (o un flag/opción peligrosa, si se encuentran en la lista negra). Además, a menudo se puede modificar la representación de un parámetro sin cambiar su esencia y así eludir las restricciones de la lista negra.
Para aplicaciones en Go, he desarrollado una herramienta especial que acompaña a la aplicación y recopila todas las llamadas realizadas durante la ejecución. Por ejemplo, para la siguiente aplicación:
package main
import "fmt"
func main() {
fmt.Println("test")
} ... lanzaremos gosystract así que:
go install https://github.com/pjbgf/gosystract
gosystract --template='{{- range . }}{{printf "%s", .Name}}{{- end}}' application-path... y obtendremos el siguiente resultado:
"sched_yield",
"futex",
"write",
"mmap",
"exit_group",
"madvise",
"rt_sigprocmask",
"getpid",
"gettid",
"tgkill",
"rt_sigaction",
"read",
"getpgrp",
"arch_prctl",Por ahora, esto es solo un ejemplo; los detalles sobre las herramientas vendrán más adelante.
Consejo nº 6: Permita solo aquellas llamadas que realmente necesite y bloquee todas las demás.
7. Establezca bases sólidas (o prepárese para comportamientos impredecibles)
El núcleo supervisará la conformidad con el perfil, independientemente de lo que haya especificado en él. Incluso si no es exactamente lo que desea. Por ejemplo, si bloquea el acceso a llamadas como exit o exit_group, el contenedor no podrá cerrarse correctamente y hasta un simple comando como echo hi durante un período indefinido. Como resultado, obtendrá una alta carga de CPU en el clúster:

En tales casos, la utilidad strace puede ayudar: mostrará cuál puede ser el problema:

sudo strace -c -p 9331
Asegúrese de que los perfiles incluyan todas las llamadas del sistema necesarias para que la aplicación funcione.
Consejo nº 7: Preste atención a los detalles y verifique que todas las llamadas del sistema necesarias estén en la lista blanca.
Con esto, la primera parte del ciclo 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é es importante y cómo automatizar el proceso.
P.D. del traductor
También puedes leer en nuestro blog:
- «»;
- «»;
- «»;
- «».
Fuente: habr.com
