Skip to main content

securityContext de Kubernetes: capacidades de Linux en Kubernetes

Escrito por
Kubernetes securityContext

26 de enero de 2021

0 minutos de lectura

Hace mucho tiempo, en los anales de la historia, los sistemas operativos Unix tenían un modelo de permisos relativamente sencillo. O eras un usuario normal o eras root, el superusuario con permisos para hacer cualquier cosa.

Aunque se podían otorgar permisos elevados a usuarios normales en archivos o directorios, casi todas las funciones a nivel del kernel estaban restringidas al usuario root. Si tu aplicación necesitaba una sola llamada al kernel para funcionar, había que otorgarle privilegios SUID, lo que en la práctica daba privilegios de root al proceso si lo iniciaba un usuario normal.

Este modelo funcionaba bastante bien en la época en que las máquinas físicas eran utilizadas por grupos relativamente pequeños de usuarios, pero esa granularidad limitada ya no era adecuada para la era moderna. Para ofrecer más flexibilidad y seguridad, los desarrolladores del kernel de Linux idearon una solución mucho más granular: agrupar las llamadas al kernel en capacidades y permitir que estas se asignaran a procesos individuales. Así, si una aplicación necesitaba una sola llamada al kernel, bastaba con asignarle esa capacidad específica, lo que limitaba la exposición de seguridad del sistema.

Administración de privilegios en contenedores de Kubernetes

Entonces, ¿cómo funciona esto en los contenedores?

Un contenedor no es más que un proceso que se ejecuta en el sistema y está aislado mediante cgroups y espacios de nombres del kernel. Esto significa que se le pueden asignar capacidades igual que a cualquier otro proceso. El entorno de ejecución del contenedor se encarga de hacerlo cuando lo crea.

Por lo general, el entorno de ejecución del contenedor le asigna un conjunto predeterminado de capacidades (puedes consultar aquí el conjunto predeterminado que proporciona Docker) y también ofrece un mecanismo para agregar o quitar capacidades. Si ejecutas un contenedor con la marca --privileged, le otorgas todas las capacidades. Si lo ejecutas como root, esto puede generar un problema de seguridad grave, un tema que analicé en una publicación anterior.

Cómo configurar las capacidades de un contenedor en securityContext de Kubernetes

Al usar el entorno de ejecución de contenedores en Kubernetes, el plano de control de Kubernetes utiliza estos controles para definir con qué capacidades debe iniciarse nuestro contenedor. La configuración de las capacidades se presenta al usuario mediante varios ajustes en la sección securityContext del YAML de un contenedor. La configuración es similar a la siguiente:

securityContext:
      capabilities:
        drop:
          - ALL
        add: [“NET_ADMIN”]

En este caso, quitaríamos todas las capacidades y luego agregaríamos la capacidad CAP_NET_ADMIN. Si la sección de capacidades en securityContext está vacía, obtendremos el conjunto predeterminado que define el entorno de ejecución del contenedor. Por lo general, este conjunto es bastante amplio y puede incluir muchas más capacidades de las que necesita nuestra aplicación. Iniciemos un pod en Kubernetes para ver qué capacidades obtenemos.

Primero, crearemos un archivo YAML para implementar un Pod. En esta configuración, usamos una imagen de Docker Hub, basada en Alpine, que incluye la herramienta capsh. Esta nos permitirá ver qué capacidades tiene nuestro contenedor.

Ten en cuenta que no definimos ninguna sección securityContext para este contenedor, así que se aplicarán los valores predeterminados del sistema. Recuerda que, si no especificamos un usuario en Kubernetes, el contenedor se ejecutará como el usuario predeterminado indicado en el Dockerfile con el que se creó. En muchos contenedores, ese usuario es root.

% cat caps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: caps
  labels:
    app: caps
spec:
  containers:
  - name: caps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
  restartPolicy: Always

% kubectl apply -f caps.yaml 
pod/caps created

Cuando el pod esté en ejecución, podemos abrir una shell dentro de él y ejecutar capsh para revisar las capacidades:

% kubectl exec --stdin --tty caps -- ash 
/ # capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Cómo quitar capacidades en securityContext de Kubernetes

Como podemos ver, de forma predeterminada se ejecuta como root y cuenta con bastantes capacidades. Este es el conjunto definido de forma predeterminada por el entorno de ejecución del contenedor. Ahora intentemos quitar una de esas capacidades en la configuración de securityContext. Quitaremos la capacidad de crear nodos del sistema de archivos, es decir, CAP_MKNOD:

matt@Mattbook capabilities_testing % cat dropcaps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: dropcaps
  labels:
    app: dropcaps
spec:
  containers:
  - name: dropcaps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        drop: ["MKNOD"]
  restartPolicy: Always

% kubectl apply -f dropcaps.yaml 
pod/dropcaps created

% kubectl exec --stdin --tty dropcaps -- ash
/ # capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_audit_write,cap_setfcap
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Si lo comparamos con el primer pod, podemos ver que este ya no tiene la capacidad cap_mknod. Además de quitar capacidades individuales, también podemos quitarlas todas mediante securityContext:

matt@Mattbook capabilities_testing % cat nocaps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: nocaps
  labels:
    app: nocaps
spec:
  containers:
  - name: nocaps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        drop:
          - ALL
  restartPolicy: Always

matt@Mattbook capabilities_testing % kubectl apply -f nocaps.yaml 
pod/nocaps created

matt@Mattbook capabilities_testing % kubectl exec --stdin --tty nocaps -- ash
/ # capsh --print
Current: =
Bounding set =
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Si no tenemos ninguna capacidad, algunas funciones del sistema fallarán al intentar ejecutarlas. Por ejemplo, intentemos instalar bash con apk:

/ # apk add bash
(1/5) Installing ncurses-terminfo-base (6.1_p20180818-r1)
(2/5) Installing ncurses-terminfo (6.1_p20180818-r1)
(3/5) Installing ncurses-libs (6.1_p20180818-r1)
(4/5) Installing readline (7.0.003-r0)
(5/5) Installing bash (4.4.19-r1)
Executing bash-4.4.19-r1.post-install
ERROR: bash-4.4.19-r1.post-install: script exited with error 127
Executing busybox-1.28.4-r1.trigger
ERROR: busybox-1.28.4-r1.trigger: script exited with error 127
1 error; 13 MiB in 19 packages

Aunque nuestro contenedor se ejecuta como root, la instalación del paquete bash falla porque necesita configurar los permisos del sistema de archivos y quitamos esa capacidad del contenedor. Si quisiéramos impedir que alguien instalara software en nuestro contenedor, administrar esos permisos mediante capacidades sería una opción.

Aunque capsh nos muestra las capacidades del contenedor en un formato fácil de leer, no es la única manera de averiguar cuáles están disponibles. También podemos consultar esta información directamente desde el sistema de archivos proc, sin instalar software adicional:

/ # cd /proc/1/
/proc/1 # cat status 
----truncated
CapPrm:0000000000000000
CapEff:0000000000000000
CapBnd:0000000000000000
CapAmb:0000000000000000
----truncated

En el archivo /proc/1/status, las capacidades se muestran como un mapa de bits. Como este contenedor no tiene ninguna capacidad habilitada, todos los valores son cero. Si revisamos el mismo archivo en el contenedor original, donde todas las capacidades están habilitadas:

/ # cd /proc/1/
/proc/1 # cat status 
----truncated
CapInh:00000000a80425fb
CapPrm:00000000a80425fb
CapEff:00000000a80425fb
CapBnd:00000000a80425fb
----truncated

No es tan fácil de leer como el resultado de capsh, pero cada bit que aparece aquí representa una capacidad específica, tal como se define en el archivo de encabezado correspondiente del kernel.

Obtén más información sobre cómo mejorar la seguridad de Kubernetes al quitar las capacidades predeterminadas de un contenedor.

Cómo agregar capacidades en securityContext de Kubernetes

Además de quitar capacidades, también podemos volver a agregarlas. Tomemos el ejemplo anterior y agreguemos una sola capacidad a la configuración de securityContext, nuevamente CAP_MKNOD:

% cat addcaps.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: addcaps
  labels:
    app: addcaps
spec:
  containers:
  - name: addcaps
    image: ollijanatuinen/capsh
    command: ["/bin/sleep", "3650d"]
    imagePullPolicy: IfNotPresent
    securityContext:
      capabilities:
        drop:
          - ALL
        add: ["MKNOD"]
  restartPolicy: Always

% kubectl create -f addcaps.yaml           
pod/addcaps created
% kubectl exec --stdin --tty addcaps -- ash
/ # capsh --print
Current: = cap_mknod+eip
Bounding set =cap_mknod
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

En este ejemplo, podemos ver que ahora solo tenemos habilitada esa capacidad.

Principio de privilegio mínimo en securityContext de Kubernetes

En esta publicación vimos cómo funcionan las capacidades en los contenedores, cómo se configuran en securityContext de Kubernetes y cómo las controla el entorno de ejecución del contenedor.

Si seguimos el principio de privilegio mínimo, la práctica recomendada desde el punto de vista de la seguridad es proporcionar solo las capacidades que realmente necesita nuestro contenedor. Analicé este tema en una publicación anterior. Puede sorprender que la mayoría de los procesos no necesiten ninguna capacidad del kernel, ya que incluso los permisos elevados suelen poder controlarse mediante permisos a nivel de archivo.

Empieza por quitar todas las capacidades en securityContext y luego ve agregando solo las que necesites. Puedes depurar los errores consultando los resultados de herramientas como SELinux para identificar qué capacidades podrían estar causando el problema. También debemos tener en cuenta que los contenedores de Kubernetes pueden ejecutarse como root, a menos que especifiquemos otro usuario.

Cómo aplicar la configuración de capacidades de securityContext en Kubernetes

Si queremos asegurarnos de que se configuren opciones de securityContext, como las capacidades y la ejecución como usuario no root, podemos usar controladores de admisión en nuestro clúster de Kubernetes para evitar que se inicien contenedores sin la configuración de seguridad adecuada. Kubernetes incluye el controlador PodSecurityPolicy, que permite aplicar la configuración de securityContext.

Sin embargo, ten en cuenta que se dejará de usar a partir de la versión 1.21, en favor de proyectos mantenidos externamente, como Open Policy Agent.

También necesitamos integrar la visibilidad y la corrección de este tipo de configuraciones de seguridad directamente en el proceso de desarrollo. Snyk puede analizar tus archivos YAML de Kubernetes, detectar configuraciones inseguras de capacidades y otros ajustes de securityContext, y ofrecer recomendaciones para corregirlas directamente en los flujos de trabajo de los desarrolladores. Esta funcionalidad está disponible mediante Snyk CLI y también puede integrarse directamente con sistemas de administración de código fuente y de integración continua:

% snyk iac test caps.yaml

Testing caps.yaml...

Infrastructure as code issues:
  ✗ Container is running with default set of capabilities [Medium Severity] [SNYK-CC-K8S-6] in Deployment
    introduced by input > spec > containers[caps] > securityContext > capabilities > drop

  ✗ Container is running without root user control [Medium Severity] [SNYK-CC-K8S-10] in Deployment
    introduced by input > spec > containers[caps] > securityContext > runAsNonRoot

  ✗ Container is running without memory limit [Low Severity] [SNYK-CC-K8S-4] in Deployment
    introduced by input > spec > containers[caps] > resources > limits > memory

  ✗ Container is running without cpu limit [Low Severity] [SNYK-CC-K8S-5] in Deployment
    introduced by input > spec > containers[caps] > resources > limits > cpu

  ✗ Container is running with writable root filesystem [Low Severity] [SNYK-CC-K8S-8] in Deployment
    introduced by input > spec > containers[caps] > securityContext > readOnlyRootFilesystem

  ✗ Container is running without AppArmor profile [Low Severity] [SNYK-CC-K8S-32] in Deployment
    introduced by metadata > annotations['container.apparmor.security.beta.kubernetes.io/caps']

  ✗ Container is running without liveness probe [Low Severity] [SNYK-CC-K8S-41] in Deployment
    introduced by spec > containers[caps] > livenessProbe

  ✗ Container could be running with outdated image [Low Severity] [SNYK-CC-K8S-42] in Deployment
    introduced by spec > containers[caps] > imagePullPolicy

Organization:      matt-jarvis-snyk
Type:              Kubernetes
Target file:       caps.yaml
Project name:      capabilities_testing
Open source:       no
Project path:      caps.yaml

Tested caps.yaml for known issues, found 8 issues
Ejemplo de código de contexto de seguridad de Kubernetes que muestra un contenedor configurado con privileged: true y las capacidades predeterminadas

¿Te interesa probar esta función? ¡Crea hoy una cuenta gratis!

Empieza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.