# 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 login` y `docker 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í:

```dockerfile
# 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:

```console
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:

```console
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`.

```dockerfile
# 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:

```console
docker build -t backstage:dhi-dev .
```

> [!NOTE]
>
> La variante `-dev` incluye una shell y un gestor de paquetes, por lo que `apk add` funciona. Backstage requiere `python3` y herramientas de compilación nativas en la imagen de tiempo de ejecución porque `yarn workspaces focus --all --production` recompila 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:

```console
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:

```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:

```console
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.

> [!TIP]
>
> La variante `-sfw-dev` es más grande (1.9 GB frente a 1.72 GB) porque Socket Firewall añade herramientas de monitoreo. El beneficio de seguridad durante `yarn install` supera 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.

> [!IMPORTANT]
>
> Al 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 (como `python-3.14`). No añadas herramientas de compilación (como `g++`, `make` o `python3` de 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.14` del catálogo de DHI. A diferencia de `python3` instalado a través de `apk`, 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.

**Interfaz de usuario de Docker Hub**



Después de reflejar (mirror) el repositorio DHI de Node.js en el espacio de nombres de tu organización:

1. Abre el repositorio de Node.js reflejado en Docker Hub.
2. Selecciona **Customize** y elige la etiqueta `node:24-alpine3.23`.
3. En **Packages**, añade `sqlite-libs` y `python-3.14`.
4. Crea la personalización.

Para obtener más información, consulta [Personalizar una imagen](/dhi/how-to/customize/).

**CLI de dhictl**



`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](/dhi/how-to/cli/).

En lugar de escribir el archivo YAML de personalización a mano, utiliza `dhictl` para crear un punto de partida:

```console
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:

```yaml
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: 1000
```

Luego crea la personalización:

```console
dhictl customization create --org YOUR_ORG node-backstage.yaml
```

Monitorea el progreso de la compilación:

```console
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`.

> [!NOTE]
>
> Si tu configuración de Backstage no requiere Python en tiempo de ejecución, puedes omitir `python-3.14` de la lista de paquetes. El paquete `sqlite-libs` por sí solo es suficiente para ejecutar Backstage con `better-sqlite3`.



### Actualizar el Dockerfile

Actualiza únicamente la etapa final de tu Dockerfile para utilizar la imagen personalizada:

```dockerfile
# 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:

```console
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:

```console
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:

```console
docker exec -it <container-id> sh
OCI runtime exec failed: exec failed: unable to start container process: ...
```

Utiliza [Docker Debug](/dhi/troubleshoot/#general-debugging) si necesitas solucionar problemas en un contenedor distroless en ejecución.

> [!NOTE]
>
> Si 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:

```console
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) |

> [!NOTE]
>
> La variante `-sfw-dev` es 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 durante `yarn install` supera el aumento de tamaño.

Para una evaluación más exhaustiva, realiza escaneos con múltiples herramientas:

```console
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](/dhi/how-to/customize/): referencia completa sobre la interfaz de usuario de personalización Enterprise.
- [Crear y compilar una DHI](/dhi/how-to/build/): aprende cómo escribir un archivo de definición DHI y compilar imágenes localmente.
- [Usar la CLI de DHI](/dhi/how-to/cli/): gestiona imágenes DHI, espejos (mirrors) y personalizaciones desde la línea de comandos.
- [Migrar a DHI](/dhi/migration/): para aplicaciones que funcionan con imágenes DHI estándar sin paquetes adicionales.
- [Comparar imágenes](/dhi/how-to/explore/#compare-and-evaluate-images): evalúa las mejoras de seguridad entre tu imagen original y la protegida.
- [Docker Debug](/dhi/troubleshoot/#general-debugging): soluciona problemas en contenedores distroless que no tienen shell.

