Saltar al contenido

Capítulo 7 de 13

Configuración y permisos con intención

Reducir permisos, distribuir secretos por servicio y actualizar imágenes sin confundir aislamiento con invulnerabilidad.

#Empezar por lo que cada servicio necesita

Seguridad no consiste en pegar diez opciones de Docker que nadie comprende. Consiste en decidir qué puede leer, escribir, ejecutar y contactar cada servicio, y comprobar que esos permisos bastan para su función. Una API que consulta libros no necesita administrar el motor de Docker, cambiar su propio código ni ser superusuario de PostgreSQL.

El aislamiento contiene parte del impacto de un fallo. No arregla consultas SQL inseguras, permisos de aplicación mal definidos ni una biblioteca vulnerable. Por eso se combinan controles del programa, contenedor, máquina y acceso administrativo.

Servicio Necesita No necesita normalmente
Proxy HTTP Leer configuración, recibir 80/443, escribir certificados Contraseña de base de datos o socket Docker
API Leer código y su secreto, hablar con la base Usuario root, escribir el código, administrar roles
Base de datos Escribir su volumen, aceptar conexiones de la API Publicar su puerto a Internet
Tarea de migración Cambiar esquema de una base concreta Estar arrancada permanentemente

#Usuario no privilegiado y sistema de archivos

USER 10001:10001 en una imagen de aplicación hace que su proceso arranque con ese UID/GID. No basta con crear el usuario en un RUN si el Dockerfile nunca cambia a él. Los archivos de código pueden ser propiedad de root y legibles por el usuario de aplicación: así el proceso no tiene permiso ordinario para modificarlos.

services:
  api:
    image: catalogo:1
    read_only: true
    tmpfs:
      - /tmp:size=64m,mode=1777
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    init: true

Es un fragmento para una API ya definida. read_only hace de solo lectura su sistema de archivos raíz, pero no convierte automáticamente en solo lectura los volúmenes que montes. tmpfs ofrece una zona temporal de 64 MB para lo que sí debe escribir, como archivos auxiliares del servidor. mode=1777 concede escritura temporal con el bit sticky: distintos usuarios no pueden borrar arbitrariamente los archivos de otros en esa carpeta.

Las capabilities son permisos especiales de Linux separados de la idea de "root lo puede todo". cap_drop: [ALL] quita las que no necesita una API que escucha en 8000. no-new-privileges impide obtener nuevos privilegios mediante mecanismos como binarios setuid. Prueba estas medidas con tu imagen: una base de datos o proxy con inicialización particular puede necesitar otra configuración. No copies el mismo endurecimiento a todos sin entender su arranque.

#El socket de Docker es una capacidad administrativa

Montar /var/run/docker.sock permite hablar con el motor. En un motor tradicional, un proceso con ese acceso puede crear contenedores con montajes del host y obtener un poder comparable al administrador. Montarlo :ro no convierte necesariamente la API del motor en solo lectura: sigue siendo un canal para enviar órdenes.

No lo montes en una aplicación por comodidad. Lo mismo ocurre con pertenecer al grupo docker en el host: no es una autorización menor. Para automatización, dedica credenciales y entorno de ejecución, y limita quién puede modificar los trabajos que las usan. El modo rootless reduce privilegios del motor cuando encaja con tus requisitos, pero no elimina la necesidad de administrar quién controla ese motor.

#Configuración ordinaria y secretos son cosas diferentes

Un puerto, nombre de base o nivel de logs no necesita secreto. Una contraseña o token sí. Separarlos permite revisar la configuración sin distribuir credenciales junto al código.

services:
  api:
    environment:
      DB_HOST: db
      DB_NAME: catalogo
      DB_USER: catalogo
      DB_PASSWORD_FILE: /run/secrets/app_password
    secrets:
      - app_password
secrets:
  app_password:
    file: ./secrets/app_password

La declaración superior localiza el archivo. La lista del servicio concede acceso solo a api. La aplicación lee el archivo; Docker no interpreta automáticamente DB_PASSWORD_FILE para cualquier software. En Python:

import os
from pathlib import Path

password = Path(os.environ["DB_PASSWORD_FILE"]).read_text().strip()
if not password:
    raise RuntimeError("El archivo de contraseña está vacío")

Path es una herramienta de Python para trabajar con rutas. os.environ[...] falla si falta la variable, lo que aquí es preferible a arrancar con una contraseña vacía. .strip() retira el salto de línea final; no imprimas password al diagnosticar.

Compose local monta archivos del host para estos secretos. No es una bóveda cifrada ni garantiza rotación automática. Protege los originales, sus backups y las cuentas que pueden leerlos. Con secretos originados en archivos, no supongas que declarar uid, gid o mode en YAML remapea sus permisos: comprueba el comportamiento de la implementación y el UID real de la imagen.

#Un detalle que evita horas de errores de permisos

Si el archivo del host tiene modo 600 y pertenece a tu UID 1000, una API de UID 10001 no podrá leerlo al montarse sin traducción adecuada. Una solución en un servidor Linux es asignar grupo y modo coherentes con el proceso. En el proyecto local usamos un directorio secrets de modo 700 y archivos individuales legibles que solo se montan en servicios autorizados; el directorio privado restringe el acceso por ruta en el host. No copies ese patrón a una carpeta pública ni compartas el directorio completo con todos los contenedores.

Otra opción es un gestor de secretos que materialice archivos con los permisos adecuados. Elige según el entorno; el requisito es el mismo: el proceso correcto puede leer el secreto y los demás no deberían tener acceso innecesario.

#Rotar no es editar un archivo y olvidarse

Una contraseña de PostgreSQL configurada mediante POSTGRES_PASSWORD_FILE inicializa el rol cuando se crea el directorio de datos. Cambiar el archivo con un volumen ya existente no cambia por sí mismo la contraseña dentro de PostgreSQL.

La rotación coordina ambos extremos: servidor que valida y cliente que presenta la credencial. Para una aplicación pequeña puedes programar una ventana, cambiar el rol mediante una operación de administración, actualizar el secreto y recrear la API para que lo lea. Para reducir interrupción, crea temporalmente una segunda credencial/rol con los mismos permisos limitados, migra clientes, verifica y revoca la anterior.

Nunca valides la rotación solo porque el archivo contiene otro valor. Prueba una conexión nueva con la credencial nueva y confirma que la antigua deja de servir. Las conexiones ya abiertas pueden ocultar que un cambio no se ha aplicado como esperabas.

#La aplicación no debe ser administradora de su base

En PostgreSQL, un rol representa una identidad con permisos. El administrador puede crear roles y cambiar estructura; la aplicación suele necesitar SELECT, INSERT, UPDATE o DELETE sobre unas tablas. Las migraciones son cambios ordenados del esquema, ejecutados por un proceso controlado con permisos específicos.

Separar roles limita el daño de una consulta mal formada o de una aplicación comprometida. También hace visible un error de diseño: si la API necesita crear tablas cada vez que arranca, quizá estás mezclando inicialización con servicio de peticiones.

El proyecto final crea una tabla y concede al rol de aplicación solamente lectura, inserción y uso de la secuencia que genera identificadores. No puede borrar todo el catálogo ni crear roles. Si añades una función de borrado, cambia deliberadamente el permiso y verifica quién puede invocarla.

#Imágenes, actualizaciones y confianza

Una imagen contiene tu código y software de terceros: sistema base, bibliotecas y servidor. Escoger una imagen oficial ayuda a conocer su procedencia, pero no significa que nunca tenga vulnerabilidades. Conserva un inventario de dependencias, reconstruye con actualizaciones comprobadas y mantén un mecanismo de rollback.

Un escáner compara componentes con avisos conocidos. Es una fuente de evidencia, no una prueba de seguridad total. Prioriza según si el componente vulnerable está presente, es alcanzable y tiene corrección; documenta excepciones con fecha de revisión. Un SBOM es un inventario estructurado del software incluido que facilita ese análisis. Una firma o atestación puede relacionar el artefacto con un proceso de construcción conocido; verifica también quién controla ese proceso.

Evita descargar una imagen desconocida y darle --privileged para "que funcione". Esa opción elimina gran parte de las restricciones del contenedor y concede acceso amplio a dispositivos. Si falta un permiso concreto, identifica cuál, si es realmente necesario y si puede concederse de forma limitada.

#Una comprobación práctica antes de publicar

Para tu servicio de API:

docker compose exec api id
docker compose exec api python -c \
  'from pathlib import Path; print(Path("/run/secrets/app_password").is_file())'
docker compose exec api python -c \
  'from pathlib import Path; Path("/app/no-deberia-escribirse").write_text("prueba")'

Esperas un usuario distinto de cero, True en el archivo necesario y fallo al intentar escribir en /app. La comprobación del secreto muestra su existencia, no su valor. Verifica además desde una máquina externa que solo los puertos previstos responden y desde la aplicación que sus operaciones legítimas siguen funcionando.

La configuración segura debe ser operable: si no puedes diagnosticar, restaurar o actualizar con ella, acabarás desactivándola en la primera urgencia. Practica esas operaciones con las mismas restricciones que usarás en despliegue.

#Referencias para profundizar

    ↑ ↓ para recorrer · Intro para abrir · Ctrl K para buscar