Contenedores sin privilegios de root
Ejecuta Node.js como un usuario sin privilegios de root para que un escape de contenedor o RCE no otorgue privilegios a nivel de host.
Receta
Tarjeta de receta de referencia rápida: lista para copiar y pegar.
FROM node:24-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY dist ./dist
RUN chown -R node:node /app
USER node
ENV NODE_ENV=production
CMD ["node", "dist/main.js"]Cuándo usarlo: Cada imagen de Node en producción. Los benchmarks de CIS Docker y la mayoría de las políticas de K8s requieren UIDs sin privilegios de root.
Ejemplo funcional
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json src ./
RUN npm run build
FROM node:24-bookworm-slim AS prod
WORKDIR /app
# Instalar como root, luego transferir la propiedad
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
# Directorios escribibles que la aplicación necesita en tiempo de ejecución
RUN mkdir -p /app/tmp /app/logs \
&& chown -R node:node /app
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]// src/main.ts - enlazar puerto no privilegiado
import express from "express";
const app = express();
const port = Number(process.env.PORT ?? 3000);
app.get("/health", (_req, res) => res.json({ status: "ok" }));
app.listen(port, "0.0.0.0");Lo que esto demuestra:
- La imagen oficial de
nodeincluye un usuarionode(UID 1000) chownantes deUSERpara que la aplicación pueda leernode_modulesy escribir logs- El puerto 3000 no requiere root (solo los puertos por debajo de 1024 lo requieren)
Análisis profundo
Cómo funciona
- Los contenedores Linux comparten el kernel del host; root en el contenedor sigue siendo peligroso (capacidades, montajes de sockets)
USER nodecambia al UID 1000 para el procesoCMDy sus hijossecurityContext.runAsNonRoot: truede Kubernetes rechaza imágenes que comienzan como UID 0
Contexto de seguridad de Kubernetes
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: api
image: my-api:1.2.3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /app/tmp
volumes:
- name: tmp
emptyDir: {}readOnlyRootFilesystem requiere montajes escribibles para archivos temporales y sockets Unix.
UID personalizado para OpenShift
Algunas plataformas requieren UIDs arbitrarios. Construye con directorios escribibles por grupo:
RUN chgrp -R 0 /app && chmod -R g=u /app
USER 1001OpenShift se ejecuta como un UID aleatorio en el grupo root; g=u otorga acceso de escritura.
Lista de verificación de permisos de archivos
| Ruta | Necesidad de permiso |
|---|---|
node_modules/ | lectura + ejecución |
dist/ | lectura + ejecución |
/app/tmp | escritura (cargas, archivos pid) |
/app/logs | escritura si se registran archivos (preferir stdout) |
Errores comunes
USER nodeantes deCOPY- archivos propiedad de root, la aplicación no puede escribir. Solución:chowndespués de todas las copias.- Enlazar puerto 80 - falla sin root. Solución: escuchar en 3000; el ingreso mapea 443 a 3000.
npm installcomonodeen la compilación - errores de permiso de caché. Solución: instalar como root en la etapa de compilación,chown, luegoUSER node.- Los montajes de volumen anulan la propiedad - directorios del host propiedad de root. Solución:
fsGroupen K8s ochownen initContainer. - Prisma/sqlite escribiendo en
/app- el sistema de archivos root de solo lectura falla. Solución: montaremptyDiren/app/data. - Asumir que el usuario
nodeexiste en distroless - usar el usuariononrooten su lugar.
Alternativas
| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
USER node (UID 1000) | K8s y ECS estándar | Se requiere UID arbitrario de OpenShift |
| Usuario de aplicación personalizado | Refuerzo de imagen multi-inquilino | Innecesario para la mayoría de las APIs internas |
Distroless nonroot | Imagen mínima + sin root | Necesitas depuración de shell en el contenedor |
| Root + capacidades eliminadas | Imágenes heredadas en migración | Nuevas imágenes (usar sin root desde el primer día) |
Preguntas frecuentes
¿El modo sin root rompe `npm` en el contenedor en ejecución?
Los contenedores de producción no deben ejecutar npm install en tiempo de ejecución. Instala en la etapa de compilación como root, ejecuta la aplicación como node.
¿Qué UID debo documentar?
Documenta 1000 para las imágenes oficiales de Node. Si usas UIDs personalizados, documéntalos en el gráfico de Helm y en los comentarios del Dockerfile.
¿El modo clúster de NestJS puede ejecutarse sin root?
Sí. El primario y los workers se ejecutan como node. Usa readOnlyRootFilesystem con un volumen temporal para IPC si es necesario.
¿Cómo verifico localmente?
docker run --rm my-api id
# uid=1000(node) gid=1000(node)¿AWS Fargate requiere no-root?
No estrictamente, pero las mejores prácticas de seguridad de AWS y las auditorías de clientes lo esperan. Establece user en la definición de la tarea a 1000.
¿Qué pasa con los scripts de inicio que necesitan root?
Usa un initContainer en K8s para migraciones (prisma migrate) con permisos elevados; el contenedor de la aplicación permanece sin root.
Relacionado
- Conceptos básicos de Docker - primer Dockerfile
- Compilaciones de varias etapas -
chownen la etapa final - Compromisos de Distroless y Alpine - usuario
nonroot - Sondas de salud y preparación - sondas como no-root
- Mejores prácticas de Docker - lista de verificación de la sección
Versiones de pila: Esta página fue escrita para Node.js 24.18.0 (LTS activa), npm 10+, TypeScript 5.6+, Express 5, Fastify 5 y NestJS 11.