Proteger una aplicación Backstage con Docker Hardened Images
Esta guía muestra cómo proteger una aplicación Backstage utilizando Docker Hardened Images (DHI). Backstage es un portal de desarrolladores de código abierto de la CNCF utilizado por miles de organizaciones para gestionar sus catálogos de software, plantillas y herramientas de desarrollo.
Al final de esta guía, tendrás una imagen de contenedor de Backstage que es distroless, se ejecuta como un usuario no root por defecto y tiene drásticamente menos CVEs que la imagen base estándar node:24-trixie-slim, mientras sigue siendo compatible con la compilación de módulos nativos que requiere Backstage.
Requisitos previos
- Docker Desktop o Docker Engine con BuildKit habilitado
- Una cuenta de Docker Hub autenticada con
docker loginydocker login dhi.io - Un proyecto Backstage creado con
@backstage/create-app
Por qué Backstage necesita personalización
Los ejemplos de migración de DHI cubren aplicaciones donde puedes cambiar la imagen base y todo funciona. Backstage es diferente. Utiliza better-sqlite3 y otros paquetes que compilan módulos nativos de Node.js al momento de la instalación, lo que significa que la etapa de compilación (build stage) necesita g++, make, python3 y sqlite-dev; ninguno de los cuales está en la imagen base dhi.io/node. La imagen de tiempo de ejecución (runtime) solo necesita la biblioteca compartida (sqlite-libs) con la que se enlaza el módulo nativo compilado.
Este es un patrón común. Cualquier aplicación Node.js que dependa de complementos (addons) nativos (como bcrypt, sharp, sqlite3 o node-canvas) se enfrenta al mismo desafío. El enfoque de esta guía se aplica a todos ellos.
Paso 1: Examinar el Dockerfile original
La documentación oficial de Backstage recomienda un Dockerfile multi-etapa utilizando node:24-trixie-slim (Debian). Una configuración típica se ve así:
# Etapa 1 - Crear la capa de esqueleto para la instalación de yarn
FROM node:24-trixie-slim AS packages
WORKDIR /app
COPY backstage.json package.json yarn.lock ./
COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY packages packages
COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 \
-exec rm -rf {} \+
# Etapa 2 - Instalar dependencias y compilar paquetes
FROM node:24-trixie-slim AS build
ENV PYTHON=/usr/bin/python3
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends python3 g++ build-essential && \
rm -rf /var/lib/apt/lists/*
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends libsqlite3-dev && \
rm -rf /var/lib/apt/lists/*
USER node
WORKDIR /app
COPY --from=packages --chown=node:node /app .
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn install --immutable
COPY --chown=node:node . .
RUN yarn tsc
RUN yarn --cwd packages/backend build
RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
&& tar xzf packages/backend/dist/skeleton.tar.gz \
-C packages/backend/dist/skeleton \
&& tar xzf packages/backend/dist/bundle.tar.gz \
-C packages/backend/dist/bundle
# Etapa 3 - Compilar la imagen de backend real
FROM node:24-trixie-slim
ENV PYTHON=/usr/bin/python3
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends python3 g++ build-essential && \
rm -rf /var/lib/apt/lists/*
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends libsqlite3-dev && \
rm -rf /var/lib/apt/lists/*
USER node
WORKDIR /app
COPY --from=build --chown=node:node /app/.yarn ./.yarn
COPY --from=build --chown=node:node /app/.yarnrc.yml ./
COPY --from=build --chown=node:node /app/backstage.json ./
COPY --from=build --chown=node:node /app/yarn.lock \
/app/package.json \
/app/packages/backend/dist/skeleton/ ./
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn workspaces focus --all --production
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./
CMD ["node", "packages/backend", "--config", "app-config.yaml"]Ejecuta esta imagen e inspecciona qué hay disponible dentro del contenedor:
docker build -t backstage:init .
docker run -d \
-e APP_CONFIG_backend_database_client='better-sqlite3' \
-e APP_CONFIG_backend_database_connection=':memory:' \
-e APP_CONFIG_auth_providers_guest_dangerouslyAllowOutsideDevelopment='true' \
-p 7007:7007 \
-u 1000 \
--cap-drop=ALL \
--read-only \
--tmpfs /tmp \
backstage:init
Esto funciona, pero el contenedor en tiempo de ejecución tiene una shell, un gestor de paquetes y yarn. Ninguno de ellos es necesario para ejecutar Backstage. Ejecuta docker exec para ver qué es accesible en su interior:
docker exec -it <container-id> sh
$ cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/usr/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/usr/bin/dash
$ yarn --version
4.12.0
$ dpkg --version
dpkg version 1.22.11 (arm64).
$ whoami
node
$ id
uid=1000(node) gid=1000(node) groups=1000(node)
La imagen node:24-trixie-slim incluye tres shells (dash, bash y rbash), un gestor de paquetes (dpkg) y yarn. Cada una de estas herramientas aumenta la superficie de ataque. Un atacante que obtenga acceso a este contenedor podría utilizarlas para realizar movimientos laterales en tu infraestructura.
Paso 2: Cambiar las etapas de compilación a DHI
Reemplaza las tres etapas con equivalentes de DHI. Las imágenes DHI de Node.js están disponibles en variantes tanto de Alpine como de Debian. Esta guía utiliza la variante de Alpine (dhi.io/node:24-alpine3.23) porque produce una imagen más pequeña. Si necesitas mantenerte en Debian por razones de compatibilidad, utiliza dhi.io/node:24-bookworm y mantén apt-get en lugar de apk.
# Etapa 1: preparar paquetes
FROM --platform=$BUILDPLATFORM dhi.io/node:24-alpine3.23-dev AS packages
WORKDIR /app
COPY backstage.json package.json yarn.lock ./
COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY packages packages
COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 \
-exec rm -rf {} \+
# Etapa 2: compilar la aplicación
FROM --platform=$BUILDPLATFORM dhi.io/node:24-alpine3.23-dev AS build
ENV PYTHON=/usr/bin/python3
RUN apk add --no-cache g++ make python3 sqlite-dev && \
rm -rf /var/lib/apk/lists/*
WORKDIR /app
COPY --from=packages --chown=node:node /app .
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn install --immutable
COPY --chown=node:node . .
RUN yarn tsc
RUN yarn --cwd packages/backend build
RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
&& tar xzf packages/backend/dist/skeleton.tar.gz \
-C packages/backend/dist/skeleton \
&& tar xzf packages/backend/dist/bundle.tar.gz \
-C packages/backend/dist/bundle
# Etapa final: crear la imagen de tiempo de ejecución
FROM dhi.io/node:24-alpine3.23-dev
ENV PYTHON=/usr/bin/python3
RUN apk add --no-cache g++ make python3 sqlite-dev && \
rm -rf /var/lib/apk/lists/*
WORKDIR /app
COPY --from=build --chown=node:node /app/.yarn ./.yarn
COPY --from=build --chown=node:node /app/.yarnrc.yml ./
COPY --from=build --chown=node:node /app/backstage.json ./
COPY --from=build --chown=node:node /app/yarn.lock \
/app/package.json \
/app/packages/backend/dist/skeleton/ ./
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn workspaces focus --all --production \
&& rm -rf "$(yarn cache clean)"
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./
CMD ["node", "packages/backend", "--config", "app-config.yaml"]Compila y etiqueta esta versión:
docker build -t backstage:dhi-dev .
NoteLa variante
-devincluye una shell y un gestor de paquetes, por lo queapk addfunciona. Backstage requierepython3y herramientas de compilación nativas en la imagen de tiempo de ejecución porqueyarn workspaces focus --all --productionrecompila módulos nativos durante la instalación de producción. Esto es específico del proceso de compilación de Backstage; la mayoría de las aplicaciones Node.js pueden utilizar la variante de tiempo de ejecución DHI estándar (no dev) sin paquetes adicionales.
Las imágenes de DHI vienen con atestaciones que las imágenes originales de node:24-trixie-slim no tienen. Comprueba qué está adjunto:
docker scout attest list dhi.io/node:24-alpine3.23
Las imágenes DHI se entregan con 15 atestaciones, incluyendo CycloneDX SBOM, procedencia SLSA, OpenVEX, informes de salud de Scout, escaneos de secretos, informes de virus/malware y un resumen de verificación de SLSA.
Paso 3: Añadir protección con Socket Firewall
DHI proporciona variantes -sfw (Socket Firewall) para las imágenes de Node.js. Socket Firewall intercepta los comandos npm y yarn durante la compilación para detectar y bloquear paquetes maliciosos antes de que ejecuten scripts de instalación.
Para habilitar Socket Firewall, cambia las etiquetas -dev a -sfw-dev en las tres etapas. La versión SFW del Dockerfile:
# Etapa 1: preparar paquetes
FROM --platform=$BUILDPLATFORM dhi.io/node:24-alpine3.23-sfw-dev AS packages
WORKDIR /app
COPY backstage.json package.json yarn.lock ./
COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY packages packages
COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 \
-exec rm -rf {} \+
# Etapa 2: compilar los paquetes
FROM --platform=$BUILDPLATFORM dhi.io/node:24-alpine3.23-sfw-dev AS build-packages
ENV PYTHON=/usr/bin/python3
RUN apk add --no-cache g++ make python3 sqlite-dev && \
rm -rf /var/lib/apk/lists/*
WORKDIR /app
COPY --from=packages --chown=node:node /app .
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn install --immutable
COPY --chown=node:node . .
RUN yarn tsc
RUN yarn --cwd packages/backend build
RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
&& tar xzf packages/backend/dist/skeleton.tar.gz \
-C packages/backend/dist/skeleton \
&& tar xzf packages/backend/dist/bundle.tar.gz \
-C packages/backend/dist/bundle
# Etapa final: crear la imagen de tiempo de ejecución
FROM dhi.io/node:24-alpine3.23-sfw-dev
ENV PYTHON=/usr/bin/python3
RUN apk add --no-cache g++ make python3 sqlite-dev && \
rm -rf /var/lib/apk/lists/*
WORKDIR /app
COPY --from=build-packages --chown=node:node /app/.yarn ./.yarn
COPY --from=build-packages --chown=node:node /app/.yarnrc.yml ./
COPY --from=build-packages --chown=node:node /app/backstage.json ./
COPY --from=build-packages --chown=node:node /app/yarn.lock \
/app/package.json \
/app/packages/backend/dist/skeleton/ ./
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn workspaces focus --all --production \
&& rm -rf "$(yarn cache clean)"
COPY --from=build-packages --chown=node:node /app/packages/backend/dist/bundle/ ./
CMD ["node", "packages/backend", "--config", "app-config.yaml"]Compila esta versión:
docker build -t backstage:dhi-sfw-dev .
Cuando realices la compilación, verás mensajes de Socket Firewall en la salida de compilación: Protected by Socket Firewall para cualquier comando yarn y npm ejecutado en el Dockerfile o en los contenedores en ejecución.
TipLa variante
-sfw-deves más grande (1.9 GB frente a 1.72 GB) porque Socket Firewall añade herramientas de monitoreo. El beneficio de seguridad duranteyarn installsupera el aumento de tamaño.
Paso 4: Eliminar la shell y el gestor de paquetes con personalizaciones de DHI
Los pasos anteriores siguen utilizando la variante -dev o -sfw-dev como la imagen de tiempo de ejecución, la cual incluye una shell y un gestor de paquetes. Las personalizaciones de DHI te permiten comenzar desde la imagen base (no dev) —que no tiene shell ni gestor de paquetes— y añadir únicamente las bibliotecas de tiempo de ejecución y los entornos de ejecución de lenguajes que necesita tu aplicación.
ImportantAl crear una personalización, añade solo lo que tu aplicación necesita en tiempo de ejecución:
- Paquetes del sistema: añade bibliotecas compartidas (como
sqlite-libs) y entornos de ejecución de lenguajes del catálogo de DHI (comopython-3.14). No añadas herramientas de compilación (comog++,makeopython3de Alpine).- Herramientas de compilación: mantenlas únicamente en la etapa de compilación
-dev. Nunca las añadas a la personalización de tiempo de ejecución.Los entornos de ejecución de lenguajes instalados desde el feed de paquetes protegidos de DHI se parchean y rastrean en el SBOM de la imagen, razón por la cual son aceptables como paquetes del sistema. Las herramientas de compilación de los feeds de paquetes de Alpine o Debian no están protegidas y nunca deben aparecer en la imagen de tiempo de ejecución.
Para Backstage, la imagen de tiempo de ejecución necesita:
- sqlite-libs: la biblioteca compartida contra la que se enlaza el módulo nativo compilado de
better-sqlite3(añadida como un paquete del sistema). - Python: si tus plugins o configuración de Backstage requieren Python en tiempo de ejecución. Se añade como el paquete del sistema
python-3.14del catálogo de DHI. A diferencia depython3instalado a través deapk, este paquete está parcheado por Docker y rastreado en el SBOM de la imagen.
Docker compilará continuamente con cumplimiento de nivel 3 de SLSA y parcheará estas imágenes personalizadas dentro del SLA garantizado para el parcheo de CVEs.
Para crear la personalización, utiliza uno de los siguientes métodos.
Después de reflejar (mirror) el repositorio DHI de Node.js en el espacio de nombres de tu organización:
- Abre el repositorio de Node.js reflejado en Docker Hub.
- Selecciona Customize y elige la etiqueta
node:24-alpine3.23. - En Packages, añade
sqlite-libsypython-3.14. - Crea la personalización.
Para obtener más información, consulta Personalizar una imagen.
dhictl es la herramienta de línea de comandos de Docker para gestionar Docker Hardened Images. Te deja explorar el catálogo de DHI, reflejar imágenes y crear personalizaciones directamente desde tu terminal. Puedes integrar dhictl en pipelines de CI/CD y flujos de trabajo de infraestructura como código (IaC). Puedes instalar dhictl como un binario independiente o como un plugin de la CLI de Docker (docker dhi); para instrucciones de instalación, consulta
Usar la CLI de DHI.
En lugar de escribir el archivo YAML de personalización a mano, utiliza dhictl para crear un punto de partida:
dhictl customization prepare --org YOUR_ORG node 24-alpine3.23 \
--destination YOUR_ORG/dhi-node \
--name "backstage" \
--tag-suffix "_backstage" \
--output node-backstage.yaml
Edita el archivo generado para añadir las bibliotecas de tiempo de ejecución:
name: backstage
source: dhi/node
tag_definition_id: node/alpine-3.23/24
destination: YOUR_ORG/dhi-node
tag_suffix: _backstage
platforms:
- linux/amd64
- linux/arm64
contents:
packages:
- sqlite-libs
- python-3.14
accounts:
root: true
runs-as: node
users:
- name: node
uid: 1000
groups:
- name: node
gid: 1000Luego crea la personalización:
dhictl customization create --org YOUR_ORG node-backstage.yaml
Monitorea el progreso de la compilación:
dhictl customization build list --org YOUR_ORG YOUR_ORG/dhi-node "backstage"
Docker compila la imagen personalizada en su infraestructura segura y la publica como YOUR_ORG/dhi-node:24-alpine3.23_backstage.
NoteSi tu configuración de Backstage no requiere Python en tiempo de ejecución, puedes omitir
python-3.14de la lista de paquetes. El paquetesqlite-libspor sí solo es suficiente para ejecutar Backstage conbetter-sqlite3.
Actualizar el Dockerfile
Actualiza únicamente la etapa final de tu Dockerfile para utilizar la imagen personalizada:
# Etapa final: crear la imagen de tiempo de ejecución
FROM YOUR_ORG/dhi-node:24-alpine3.23_backstage
WORKDIR /app
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./
CMD ["node", "packages/backend", "--config", "app-config.yaml"]Compila esta versión:
docker build -t backstage:dhi .
Dado que la personalización incluye únicamente bibliotecas de tiempo de ejecución y artefactos OCI —sin herramientas de compilación, sin gestor de paquetes y sin shell— la imagen resultante es distroless:
docker run --rm YOUR_ORG/dhi-node:24-alpine3.23_backstage sh -c "echo hello"
docker: Error response from daemon: ... exec: "sh": executable file not found in $PATH
Con la personalización Enterprise:
- La imagen de tiempo de ejecución es distroless: sin shell ni gestor de paquetes.
- Docker recompila automáticamente tu imagen personalizada cuando la imagen base de Node.js o cualquiera de sus paquetes reciben un parche de seguridad.
- Se mantiene toda la cadena de confianza, incluyendo la procedencia SLSA Build Nivel 3.
- Tanto el entorno de ejecución de Node.js como el de Python se rastrean en el SBOM de la imagen.
Confirma que el contenedor ya no tiene acceso a la shell:
docker exec -it <container-id> sh
OCI runtime exec failed: exec failed: unable to start container process: ...
Utiliza Docker Debug si necesitas solucionar problemas en un contenedor distroless en ejecución.
NoteSi tu organización requiere imágenes que cumplan con FIPS/STIG , esa también es una opción en DHI Enterprise.
Paso 5: Verificar los resultados
Compara la imagen basada en DHI con la original utilizando Docker Scout:
docker scout compare backstage:dhi \
--to backstage:init \
--platform linux/amd64 \
--ignore-unchanged
Una comparación típica entre los distintos enfoques muestra resultados similares a los siguientes:
| Métrica | Original | DHI -dev | DHI -sfw-dev | Enterprise |
|---|---|---|---|---|
| Uso de disco | 1.61 GB | 1.72 GB | 1.9 GB | 1.49 GB |
| Tamaño de contenido | 268 MB | 288 MB | 328 MB | 247 MB |
| Shell en runtime | Sí | Sí | Sí | No |
| Gestor de paquetes | Sí | Sí | Sí | No |
| No root por defecto | No | No | No | Sí |
| Socket Firewall | No | No | Sí (compilación) | Sí (compilación) / No (runtime) |
| Procedencia SLSA | No | Solo base | Solo base | Completa (Nivel 3) |
NoteLa variante
-sfw-deves más grande porque Socket Firewall añade herramientas de monitoreo a la imagen. El tamaño adicional está en las etapas de compilación, y el beneficio de seguridad duranteyarn installsupera el aumento de tamaño.
Para una evaluación más exhaustiva, realiza escaneos con múltiples herramientas:
trivy image backstage:dhi
grype backstage:dhi
docker scout quickview backstage:dhi
Diferentes escáneres detectan diferentes problemas. Ejecutar los tres te da la perspectiva más completa de tu postura de seguridad.
Siguientes pasos
- Personalizar una imagen: referencia completa sobre la interfaz de usuario de personalización Enterprise.
- Crear y compilar una DHI: aprende cómo escribir un archivo de definición DHI y compilar imágenes localmente.
- Usar la CLI de DHI: gestiona imágenes DHI, espejos (mirrors) y personalizaciones desde la línea de comandos.
- Migrar a DHI: para aplicaciones que funcionan con imágenes DHI estándar sin paquetes adicionales.
- Comparar imágenes: evalúa las mejoras de seguridad entre tu imagen original y la protegida.
- Docker Debug: soluciona problemas en contenedores distroless que no tienen shell.