Skip to content

Buenas Prácticas de Creación de Imágenes de Contenedores

Introducción

La creación de imágenes de contenedores seguras es fundamental para garantizar la seguridad y estabilidad de las aplicaciones desplegadas en entornos de contenedores. Las imágenes contienen todo lo necesario para ejecutar una aplicación, incluyendo el código, las dependencias y las configuraciones. Sin embargo, una imagen mal construida puede introducir vulnerabilidades que comprometan la seguridad del entorno de producción. Por ello, seguir buenas prácticas en la construcción de imágenes de contenedores es crucial para minimizar riesgos y asegurar aplicaciones seguras y fiables desde el inicio.

Buenas Prácticas de Creación de Imágenes

Uso de Imágenes Oficiales y Verificadas

Utilizar imágenes oficiales y verificadas reduce el riesgo de incluir software malicioso o con vulnerabilidades. Las imágenes oficiales son mantenidas por los desarrolladores de las aplicaciones y se actualizan regularmente para corregir problemas de seguridad. Estas imágenes suelen pasar por procesos rigurosos de validación y control de calidad.

Guía Práctica de Laboratorio

  1. Buscar Imágenes Oficiales: Visita Docker Hub o un registro de contenedores confiable. Busca imágenes oficiales marcadas con una insignia de verificación.
  2. Ejemplo: Usar una Imagen Oficial de MySQL
       docker pull mysql:latest
    
  3. Verificar la Integridad de la Imagen: Habilita Docker Content Trust para asegurar la integridad de la imagen.
       export DOCKER_CONTENT_TRUST=1
       docker pull mysql:latest
    
  4. Verificar el resumen de las vulnerabilidades que se presentan en dicha imágen:
    docker scout quickview mysql:latest
    
  5. Visualizar el detalle de los CVES:
    docker scout cves mysql:latest
    
    Con lo anterior, se puede verificar el estado de la imágen a utilizar como base para nuestros proyectos.

Minimización de la Superficie de Ataque

Minimizar la superficie de ataque implica eliminar componentes innecesarios de la imagen. Cuantos menos componentes tenga una imagen, menor será la probabilidad de que alguno de ellos tenga una vulnerabilidad explotable. Usar imágenes base minimalistas y solo incluir lo esencial ayuda a reducir los posibles vectores de ataque.

Guía Práctica de Laboratorio

  1. Crear una Imagen Minimalista: Usa una imagen base ligera como alpine en lugar de una imagen completa como ubuntu, crea un archivo llamado Dockerfile con el siguiente contenido:
    # Dockerfile
    FROM alpine:latest
    RUN apk add --no-cache python3 py3-pip
    COPY . /app
    WORKDIR /app
    CMD ["python3", "app.py"]
    
  2. Construir y Ejecutar la Imagen:
    docker build -t myapp:latest .
    
    docker run -d --name myapp myapp:latest
    
  3. Limpia el ambiente
    docker stop myapp
    
    docker rm myapp
    
    rm -f Dockerfile
    

Eliminación de Datos Sensibles y Herramientas Innecesarias

Eliminar datos sensibles y herramientas innecesarias de las imágenes de contenedores es crucial para proteger información confidencial y reducir la cantidad de herramientas que podrían ser utilizadas por atacantes. Durante el proceso de construcción de la imagen, es común incluir herramientas y datos temporales que, si no se eliminan, pueden representar riesgos de seguridad.

Guía Práctica de Laboratorio

  1. Eliminar Herramientas y Archivos Innecesarios: Añade una etapa de limpieza al Dockerfile para eliminar herramientas utilizadas solo durante la construcción. Crea un archivo llamado Dockerfile con el siguiente contenido:
    # Dockerfile
    FROM python:3.9-slim as builder
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --user -r requirements.txt
    
    FROM python:3.9-slim
    WORKDIR /app
    COPY --from=builder /root/.local /root/.local
    ENV PATH=/root/.local/bin:$PATH
    COPY . /app
    CMD ["python", "app.py"]
    
  2. Crea un archivo de texto llamado requirements.txt
    echo "Flask==2.0.3" > requirements.txt
    
  3. Construir y Escanear la Imagen:
    docker build -t mysecureapp:latest .
    
    docker scout quickview mysecureapp:latest
    
  4. Limpia el ambiente
    docker rmi mysecureapp:latest
    
    rm -f Dockerfile requirements.txt
    

Escaneo Regular de Imágenes en Busca de Vulnerabilidades

El escaneo regular de imágenes en busca de vulnerabilidades permite identificar y corregir problemas de seguridad antes de que lleguen a producción. Existen varias herramientas que pueden integrarse en el flujo de trabajo de CI/CD para automatizar este proceso. Mas adelante se realizará una práctica de laboratorio relacionado a este tema

Uso de Multietapas en Dockerfile

El uso de multietapas en Dockerfile permite construir imágenes más pequeñas y seguras al separar el proceso de construcción del runtime. Esto reduce el tamaño de la imagen final y elimina herramientas y dependencias innecesarias utilizadas solo durante la construcción. 1. Crear una imagen Multicapa. Crea un archivo llamado Dockerfile con el siguiente contenido:

# Dockerfile
FROM golang:1.16 as builder
WORKDIR /app
COPY . .
RUN go build -o myapp

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
2. Crea un archivo llamado main.go con el siguiente contenido:
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
3. Crea un archivo llamado go.mod con el siguiente contenido:
module myapp

go 1.23
4. Construir y Ejecutar la Imagen:
docker build -t mymultistageapp:latest .
docker run -d --name mymultistageapp mymultistageapp:latest
5. Verifica la salida de los logs del contenedor anterior y deverías ver el mensaje "Hello, World!"
docker logs -f mymultistageapp
4. Limpia el ambiente
docker stop mymultistageapp ; docker rm mymultistageapp
docker rmi mymultistageapp:latest
rm -f Dockerfile go.mod main.go

Implementación de Políticas de Seguridad a Nivel de Contenedor

Implementar políticas de seguridad a nivel de contenedor, como restricciones de usuarios y capacidades, ayuda a reducir el impacto de posibles ataques y mejora la seguridad general del contenedor.

  1. Restringir el Usuario en Dockerfile: Ejecutar el contenedor con un usuario no privilegiado. Crea un archivo llamado Dockerfile con el siguiente contenido:
    # Dockerfile
    FROM node:14
    WORKDIR /app
    COPY . .
    RUN npm install
    RUN useradd -m appuser
    USER appuser
    CMD ["node", "app.js"]
    
  2. Crea un archivo llamado package.json con el siguiente contenido:
    {
      "name": "myapp",
      "version": "1.0.0",
      "description": "A simple Node.js app",
      "main": "app.js",
      "scripts": {
        "start": "node app.js"
      },
      "author": "",
      "license": "ISC"
    }
    
  3. Crea un archivo llamado app.js con el siguiente contenido:
    const os = require('os');
    const { execSync } = require('child_process');
    
    const username = os.userInfo().username;
    const userId = execSync('id -u').toString().trim();
    const groupId = execSync('id -g').toString().trim();
    
    console.log('Hello, World!');
    console.log(`Running as user: ${username}`);
    console.log(`User ID: ${userId}`);
    console.log(`Group ID: ${groupId}`);
    
  4. Construir y Ejecutar la Imagen:
    docker build -t userapp:latest .
    
    docker run -d --name userapp userapp:latest
    
  5. Verifica la salida de los logs del contenedor anterior y deverías ver el mensaje "Hello, World!" y la información del ID del usuario.
    docker logs -f userapp
    
  6. Limpia el ambiente
    docker stop userapp ; docker rm userapp
    
    docker rmi userapp:latest
    
    rm -f Dockerfile app.js package.json
    

Implementar Capabilities y Seccomp: Limitar las capacidades del contenedor y aplicar un perfil seccomp.

  1. Descargar el perfil seccomp por defecto
    curl -o default.json https://raw.githubusercontent.com/docker/docker/master/profiles/seccomp/default.json
    
  2. Ejecutar el contenedor con el perfil seccomp
    docker run -it --name secureapp --cap-drop=ALL --security-opt seccomp=$(pwd)/default.json alpine sh
    
  3. Una vez dentro del contenedor, instala el paquete libcap que contiene getpcaps.
    apk update
    
    apk add libcap
    
  4. Usa getpcaps para verificar las capacidades del proceso.
    getpcaps 1
    
    El resultado 1: = indica que no hay capacidades adicionales, lo cual es correcto.
  5. Ejecuta una syscall restringida para verificar que el perfil seccomp está funcionando. Por ejemplo, intenta usar unshare:
    unshare --mount
    
    Esto debería fallar y devolver un error como Operation not permitted.
  6. Ejecuta algunos comandos básicos para asegurarte de que el contenedor está funcionando correctamente bajo las restricciones de seguridad. Por ejemplo:
    echo "Hello from Alpine!"
    
    exit
    
  7. Limpia el ambiente

    docker stop secureapp ; docker rm secureapp
    
    rm -f default.json
    

  8. --cap-drop=ALL: Esta opción elimina todas las capacidades de Linux del contenedor. Las capacidades son privilegios adicionales que los procesos en un contenedor pueden tener. Al eliminar todas las capacidades, se reduce la superficie de ataque y se limita lo que el contenedor puede hacer en el sistema anfitrión. Esto es una medida de seguridad importante.

  9. --security-opt seccomp=default.json: Esta opción aplica un perfil de seccomp (secure computing mode) al contenedor. Seccomp es una herramienta de seguridad que restringe las llamadas al sistema (syscalls) que el contenedor puede realizar. El perfil default.json es un conjunto de reglas que especifica qué llamadas al sistema están permitidas o denegadas, lo que ayuda a limitar el comportamiento del contenedor y proteger el sistema anfitrión de posibles exploits.

Resumen

Implementar estas buenas prácticas en la creación de imágenes de contenedores ayuda a asegurar que las aplicaciones sean seguras desde la base. Usar imágenes oficiales y verificadas, minimizar la superficie de ataque, eliminar datos sensibles y herramientas innecesarias, realizar escaneos regulares de vulnerabilidades, usar multietapas en Dockerfile y aplicar políticas de seguridad a nivel de contenedor son pasos esenciales para construir imágenes de contenedores robustas y seguras. Siguiendo estas prácticas, podemos reducir significativamente los riesgos de seguridad y asegurar la integridad de nuestras aplicaciones en entornos de contenedores.