
En resumen
- Un pipeline que recibe archivos de usuarios y guarda una llave de API es un objetivo: trátalo así.
- Imagen mínima, base fijada por digest y proceso sin root.
- Docker Scout en CI debe poder detener el despliegue, no solo generar un reporte.
- Los secretos se montan al ejecutar; nunca viajan en una capa de la imagen.
- El entorno de desarrollo usa las mismas restricciones que producción.
La seguridad en pipelines Docker suele revisarse al final, cuando la imagen ya está en producción. Para entonces, las decisiones que más pesan ya se tomaron: qué base se usó, con qué usuario corre el proceso y dónde quedó la llave de API.
Llego a este tema desde un caso concreto: procesé 80,000 folios con OCR + LLM en producción. Un pipeline así reúne justo lo que vuelve delicado a un contenedor: archivos que suben desconocidos, binarios nativos para leer imágenes y una credencial de pago.
Una precisión: ese pipeline corrió con cron, directo sobre un VPS con AlmaLinux. Este artículo es cómo empaqueto y aseguro en Docker una carga con ese mismo perfil. El código es ilustrativo, no el del cliente.
El perfil de riesgo de un pipeline de OCR + LLM
Antes de escanear nada, conviene escribir qué puede salir mal. En un verificador de comprobantes hay cuatro superficies de ataque:
- La entrada no es confiable. Cada comprobante es una imagen que subió alguien desde internet. El código que la decodifica procesa datos hostiles por diseño.
- Hay dependencias nativas. Tesseract y las bibliotecas de imágenes están escritas en C. Sus fallas son de memoria, y esas son las que terminan en ejecución de código.
- Hay una credencial con costo. La llave de API del LLM vale dinero. Si se filtra, alguien más gasta tu presupuesto.
- El pipeline de CI tiene permisos. Quien controla la construcción controla lo que llega a producción.
La estrategia no es evitar todas las vulnerabilidades. Es asumir que alguna va a existir y limitar lo que un atacante puede hacer con ella.
Análisis de seguridad en pipelines Docker: empieza por la imagen
Casi todo lo que un escáner reporta se decide en el Dockerfile. Una imagen con menos paquetes tiene menos vulnerabilidades que parchar y menos herramientas disponibles para un intruso.
Estas son las cuatro decisiones que más reducen el riesgo, en un solo archivo:
# La etiqueta puede cambiar de contenido; el digest no. Fija la base.
FROM python:3.12-slim@sha256:<digest> AS build
WORKDIR /app
COPY requirements.txt .
# Con hashes, pip rechaza un paquete alterado aunque el nombre coincida.
RUN pip install --no-cache-dir --require-hashes --prefix=/install -r requirements.txt
FROM python:3.12-slim@sha256:<digest>
# Solo lo que el OCR necesita. Sin compiladores ni paquetes recomendados.
RUN apt-get update \
&& apt-get install -y --no-install-recommends tesseract-ocr \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /install /usr/local
COPY --chown=10001:10001 src/ /app/
# Si alguien explota el decodificador de imágenes, no obtiene root.
USER 10001
ENTRYPOINT ["python", "/app/verificador.py"]
Vale la pena explicar cada una.
Construcción en etapas. Las herramientas para instalar dependencias se quedan en la primera etapa. La imagen final solo lleva lo necesario para ejecutar.
Base fijada por digest. Una etiqueta como 3.12-slim apunta a contenido distinto cada semana. Con el digest, la construcción de hoy y la de mañana usan exactamente la misma base, y las actualizaciones entran por un pull request revisado.
Dependencias con hash. La cadena de suministro de paquetes es un vector real. Verificar el hash de cada paquete cierra la puerta a una versión sustituida.
Usuario sin privilegios. Es una línea y es la que más cambia el resultado de un incidente.
Los secretos merecen su propia regla. Todo lo que entra a una capa se queda en la imagen, aunque lo borres después. Si la construcción necesita una credencial, usa un secreto de BuildKit con --mount=type=secret: está disponible durante ese paso y no queda en ninguna capa.
Integrando Docker Scout para una mejor protección
Docker Scout analiza la imagen, arma el inventario de sus paquetes y lo cruza con las vulnerabilidades conocidas. Lo importante no es tener el reporte. Es decidir qué pasa cuando el reporte trae algo grave.
Un escaneo que solo informa termina ignorado. El que sirve es el que detiene el pipeline:
jobs:
imagen:
runs-on: ubuntu-latest
permissions:
contents: read # el token del job no necesita escribir en el repo
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PAT }}
- uses: docker/build-push-action@v6
with:
context: .
load: true # se analiza antes de publicar, no después
tags: verificador:${{ github.sha }}
- uses: docker/scout-action@v1
with:
command: cves
image: local://verificador:${{ github.sha }}
only-severities: critical,high
exit-code: true # con hallazgos graves, el job falla
El orden es la clave: construir, analizar y solo entonces publicar. Una imagen vulnerable nunca llega al registro.
Tres ajustes que hacen la diferencia en la práctica:
- Filtra por severidad. Si el pipeline falla por cada hallazgo menor, el equipo aprende a saltárselo. Bloquea lo crítico y lo alto; registra lo demás.
- Vuelve a analizar lo ya desplegado. Las vulnerabilidades nuevas se publican después de tu construcción. Programa un análisis periódico de las imágenes en producción.
- Fija también las acciones. Las acciones de terceros corren con tus secretos. Fíjalas por SHA del commit y da al token solo los permisos que el job necesita.
También conviene acordar de antemano qué se hace con un hallazgo. Sin ese acuerdo, cada alerta se vuelve una discusión. El orden que funciona es este:
- Actualiza la base. La mayoría de los hallazgos vive en paquetes del sistema y se resuelve subiendo el digest de la imagen base.
- Quita el paquete. Si la biblioteca vulnerable no se usa, la mejor corrección es que no esté en la imagen.
- Documenta la excepción con fecha de caducidad. Si no hay parche y el riesgo es aceptable, se registra quién lo aceptó, por qué y hasta cuándo. Una excepción sin fecha es un hallazgo ignorado.
Y guarda el inventario de paquetes de cada imagen publicada. Cuando salga la próxima vulnerabilidad sonada, la pregunta «¿nos afecta?» se responde con una búsqueda, no con una tarde de arqueología.
Una nota de mantenimiento: las entradas de estas acciones cambian entre versiones. Confirma los nombres contra la documentación de la versión que fijes.
El contenedor en ejecución: mínimos privilegios
La imagen define qué hay dentro. La forma de ejecutarla define qué puede hacer. Aquí es donde se contiene el daño de una vulnerabilidad que el escáner todavía no conoce.
# ⚠️ Producción: respalda la base de datos y prueba primero en staging.
# - Raíz en solo lectura: un intruso no puede dejar archivos en el contenedor.
# - /tmp sin ejecución: lo que se descargue ahí no se puede ejecutar.
# - Sin capabilities ni escalada: el proceso no puede ganar privilegios.
# - Límites de memoria y procesos: una imagen maliciosa no tumba al servidor.
# - La llave entra como archivo montado, no como variable visible en inspect.
docker run --rm \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m \
--cap-drop ALL --security-opt no-new-privileges \
--memory 1g --pids-limit 200 \
--mount type=bind,src=/etc/verificador/llm_key,dst=/run/secrets/llm_key,readonly \
verificador:1.4.2
Cada opción responde a una de las superficies de ataque del inicio. La entrada hostil queda encerrada en un proceso sin privilegios. La llave no aparece en el historial ni en las variables de entorno. Y un archivo diseñado para agotar memoria choca con un límite.
Falta una pieza: la red. Un verificador solo necesita hablar con la base de datos y con la API del LLM. Todo lo demás debe estar cerrado, para que un intruso no pueda sacar datos hacia donde quiera.
Configuración óptima de seguridad en entornos de desarrollo
El entorno de desarrollo es donde nacen los malos hábitos: contenedores como root «para que funcione», llaves reales en un archivo .env y volúmenes que montan todo el disco.
La regla es que desarrollo use las mismas restricciones que producción. Si el contenedor no arranca en solo lectura en tu laptop, es mejor enterarte ahí.
services:
verificador:
build: .
read_only: true # igual que producción: sin sorpresas al desplegar
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
user: "10001:10001"
secrets: [llm_key]
volumes:
- ./muestras:/datos:ro # comprobantes de prueba, nunca datos reales
secrets:
llm_key:
file: ./.secretos/llm_key # fuera de Git; llave de desarrollo, no la de producción
Tres prácticas completan el entorno:
- Llaves separadas por entorno. La de desarrollo tiene su propio límite de gasto. Si se filtra, el daño está acotado y se revoca sin tocar producción.
- Datos de prueba, no copias de producción. Los comprobantes reales contienen datos personales. En desarrollo se usan muestras fabricadas.
- Detección de secretos antes del commit. Un escáner de secretos en el hook de pre-commit y en CI atrapa la llave antes de que llegue al historial.
Lo que NO debes hacer
- Usar la etiqueta
latesten producción. No sabes qué estás ejecutando ni puedes volver a la versión anterior con certeza. Usa versiones explícitas y digest. - Poner secretos en
ENVoARG. Quedan en el historial de la imagen y cualquiera con acceso al registro los puede leer. Borrarlos en una capa posterior no los elimina. - Escanear sin consecuencias. Un reporte de vulnerabilidades que no puede detener el despliegue es documentación, no un control.
- Montar el socket de Docker dentro del contenedor. Es entregar el control del servidor completo. Si un paso de CI lo necesita, aíslalo en un ejecutor desechable.
El siguiente paso
La seguridad en pipelines Docker no depende de una herramienta. Depende de una cadena de decisiones pequeñas: una imagen mínima, un análisis que sí bloquea, secretos que nunca tocan una capa y un contenedor que corre con lo justo.
Si tu equipo necesita auditar sus pipelines, llevar a contenedores una carga que hoy corre directo en el servidor o endurecer su infraestructura antes de una revisión de seguridad, en Conqr Digital Agency diseñamos arquitecturas que escalan. Revisemos tu caso.
Si quieres ver el sistema que dio origen a este artículo, está en el caso de Panini Fan Zone. Para la parte de despliegue, revisa el servicio de Cloud y DevOps.
Preguntas frecuentes
- ¿Qué es Docker Scout y para qué sirve en un pipeline?
- Es la herramienta de Docker para analizar imágenes: genera el inventario de paquetes de la imagen y lo cruza con vulnerabilidades conocidas. En un pipeline sirve como control: si aparece una vulnerabilidad crítica o alta, la imagen no se publica.
- ¿Por qué no debo ejecutar contenedores como root?
- Porque si un atacante logra ejecutar código dentro del contenedor, hereda los privilegios del proceso. Sin root, sin capabilities y con el sistema de archivos en solo lectura, una falla en una biblioteca tiene mucho menos alcance.
- ¿Dónde guardo la llave de API de un LLM en Docker?
- Fuera de la imagen. Durante la construcción, usa secretos de BuildKit. Al ejecutar, monta la llave como archivo de solo lectura o usa el gestor de secretos de tu plataforma. Nunca la pongas en una instrucción ENV ni en el repositorio.



