Skip to main content

GitHub Actions para publicar paquetes npm de forma segura

Escrito por

10 de noviembre de 2020

0 minutos de lectura

GitHub Actions ha ganado popularidad desde que GitHub anunció su disponibilidad general para todos los desarrolladores y repositorios de la plataforma. Las limitaciones de uso que vemos en el ecosistema —como los nuevos límites de facturación y uso para proyectos de código abierto de Travis CI— impulsarán aún más a los desarrolladores a migrar sus automatizaciones de software a GitHub Actions.

En este artículo, quiero mostrarte cómo uso GitHub Actions para publicar paquetes npm que mantengo en mis proyectos de código abierto. Si sigues el flujo de GitHub, que incluye flujos de trabajo de pull requests de GitHub, esto unificará aún más la experiencia de tus equipos y colaboradores de la comunidad en torno a los flujos de trabajo de GitHub en tu proyecto.

¿Qué es GitHub Actions?

GitHub Actions es una tecnología desarrollada por GitHub que permite a los desarrolladores automatizar sus flujos de trabajo de integración continua: los ayuda a compilar, implementar, programar tareas recurrentes y mucho más. GitHub Actions está disponible de forma nativa e integrado en los repositorios de GitHub, y ofrece muchos flujos de trabajo reutilizables aportados por la comunidad, como publicar paquetes npm, publicar imágenes de Docker, ejecutar pruebas de seguridad y mucho más.

¿Cómo funcionan las acciones en GitHub?

La infraestructura en la nube de GitHub ejecuta GitHub Actions para los usuarios. Para ello, se crea un archivo de flujo de trabajo en un directorio del repositorio de GitHub llamado .github/workflows, donde se describen el activador, la programación del trabajo y las acciones que este debe realizar mediante YAML.

¿Qué es un flujo de trabajo de GitHub?

Un flujo de trabajo de GitHub es un conjunto de trabajos que se ejecutan según un activador o una programación basada en cron. Un trabajo consta de uno o más pasos que conforman un flujo de trabajo automatizado.

Configurar un proyecto Node.js para GitHub Actions

Agreguemos una automatización inicial de GitHub Actions a un proyecto Node.js. El trabajo de GitHub Actions instalará todos los paquetes npm necesarios, ejecutará las pruebas y, finalmente, publicará nuestro proyecto como un paquete npm que los usuarios podrán usar.

Nuestro paquete npm será una interfaz de línea de comandos (CLI) para explorar la increíble lista de charlas de SnykCon 2020, el primer evento global de seguridad de Snyk, que se realizó en 2020.

El proyecto completo está alojado en GitHub. Aquí tienes un adelanto del aspecto del paquete npm:

Gráfico con estilo de terminal que enumera las charlas de SnykCon 2020, incluidas sesiones sobre DevSecOps y seguridad de la superficie de ataque del front-end.

Un flujo de trabajo completo de CI de GitHub comienza con la creación del siguiente archivo de GitHub Actions en la raíz de la ruta del repositorio: .github/workflows/main.yml

name: main

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x, 14.x]
    steps:
      - uses: actions/checkout@v2
      - name: Build on Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci --ignore-scripts
      - run: npm run build --if-present
        name: Build
      - run: npm test
        env:
          CI: true

Lo anterior es una plantilla estándar para compilar y probar proyectos Node.js. Realiza lo siguiente:

  1. El activador se ejecuta con cada push, en todas las ramas.

  2. Ejecutamos esta acción en las versiones 12.x y 14.x de Node.js para garantizar la compatibilidad con ambas versiones LTS de Node.js.

  3. Luego, el trabajo ejecuta varios pasos: extrae el código de git, realiza una instalación de npm segura y determinista, continúa con un paso de compilación si lo hay y, por último, ejecuta las pruebas.

Al hacer push de nuevas confirmaciones al repositorio, ya sea que sigas el flujo de GitHub o un flujo de trabajo de pull request de GitHub, verás que se ponen en cola nuevos trabajos en la pestaña Actions del repositorio. Por ejemplo, aquí se muestran nuestras pruebas en ejecución para el paquete npm snykcon:

Flujo de trabajo de GitHub Actions para el repositorio de snykcon, que muestra una compilación exitosa de Node.js 12.x y la ejecución de pruebas de npm.

Publicar paquetes npm con GitHub Actions

Si todas las pruebas pasan y este flujo de trabajo de automatización se ejecuta en nuestra rama principal, ¡podríamos automatizar por completo el lanzamiento de nuevas versiones!

Al publicar paquetes, conviene tener en cuenta las implicaciones de seguridad de estas acciones. Revisemos algunas:

  1. Código malicioso: Si alguien agrega intencionalmente una vulnerabilidad como parte de una contribución de código y no la detectas, una publicación automática al combinar los cambios en tu rama principal hará que esté disponible para los usuarios de inmediato. Si publicas manualmente de inmediato, no hay ninguna diferencia; sin embargo, cuanto más tiempo pase entre la combinación de pull requests y las publicaciones, más tiempo tendrá la comunidad para analizar el código y ayudar a descubrir estos problemas.

  2. Dependencia vulnerable: Si alguien malintencionado agrega una dependencia con una vulnerabilidad pública conocida, esta llegará a tus usuarios cuando se descargue durante el proceso de instalación. Usar una herramienta gratuita como Snyk para conectarte a tus repositorios de git y agregar una comprobación de estado que te impida publicar versiones vulnerables de dependencias resuelve este problema.

  3. Dependencia maliciosa mediante un archivo de bloqueo: ¿Has pensado qué pasaría si una contribución para actualizar las dependencias de package.json inyectara un paquete malicioso en el archivo de bloqueo, que nadie se toma el tiempo de revisar, verdad? Escribí sobre por qué los archivos de bloqueo de npm pueden ser un punto ciego de seguridad para inyectar módulos maliciosos, así que asegúrate de profundizar en el tema si no habías considerado este vector de ataque.

  4. Robo de tu token de npm:En el pasado, bastantes intentos de publicar paquetes maliciosos giraban en torno a robar el token de npm de alguien u otra información confidencial almacenada en variables de entorno.

Lo anterior no pretende ser una lista exhaustiva de problemas de seguridad, pero sin duda destaca varios aspectos que debemos tener presentes.

Hablando de aspectos importantes de seguridad, ¿te pareció interesante e instructiva esa lista de posibles problemas? Es un proceso rápido de modelado de amenazas que puedes practicar con más frecuencia al desarrollar funcionalidades. Escribimos sobre este tema en nuestro Proceso de DevSecOps, y Alyssa Miller dio una excelente charla en SnykCon sobre Modelado de amenazas de historias de usuario: así se hace DevSecOps. ¡Échales un vistazo!

Volvamos a nuestra lista y centrémonos en el último problema de seguridad mencionado: el robo de tu token de npm. Como este artículo trata sobre la publicación de paquetes npm, necesitamos poner un token de npm a disposición del flujo de trabajo de GitHub Actions, algo que históricamente se ha desaconsejado por las siguientes razones:

  • Capacidades de npm: históricamente, para publicar paquetes npm con un token de npm, era necesario desactivar la autenticación de dos factores de tu usuario de npm. Ya no es así, ¡y veremos cómo hacerlo!

  • Robo de un token durante la instalación de paquetes maliciosos:si pones el token de npm a disposición de tu CI como variable de entorno, los paquetes maliciosos que estén en el árbol de dependencias de tu paquete (más allá de tus dependencias directas) podrían acceder a él durante, por ejemplo, el proceso de instalación de npm, que de forma predeterminada permite que los paquetes ejecuten cualquier comando.

Entonces, ¿cómo abordamos estos dos problemas de seguridad válidos?

En primer lugar, npm habilitó recientemente la autenticación de dos factores junto con la emisión de tokens de automatización, así que estas dos capacidades ya no entran en conflicto. En segundo lugar, GitHub Actions permite que la información de las variables de entorno esté disponible solo para un paso específico de un trabajo. Esto significa que podemos ponerla a disposición únicamente del comando npm publish y no de npm install, que también habría permitido el acceso de las dependencias indirectas.

Comencemos.

Primero, habilita la autenticación de dos factores para tu usuario de npm. Ve a la configuración de tu cuenta en https://npmjs.com/ y habilita el modo 2FA para la autorización y la publicación.

Pantalla de configuración de la autenticación de dos factores con las opciones «Autorización», «Autorización y publicación» o «Desactivar», con «Autorización y publicación» seleccionada

Deberás asociar un dispositivo de autenticación, por ejemplo, la aplicación Google Authenticator en tu celular o 1Password si la usas para administrar tus contraseñas.

Luego, ve a la sección de administración de Tokens de acceso en npm y crea un token nuevo.

Panel de tokens de acceso con búsqueda de paquetes y controles para generar un token nuevo y eliminar los tokens seleccionados

Asegúrate de crear un token de tipo Automation, como se muestra en la siguiente captura de pantalla. Tal como indica la descripción, omitirá la autenticación de dos factores y te permitirá usarlo en flujos de trabajo de integración continua (CI):

Formulario de nuevo token de acceso de npm con Automatización seleccionada entre los tipos de token de solo lectura, automatización y publicación

Luego, podemos poner este token a disposición de GitHub Actions. Primero, créalo como secreto en la administración de secretos del repositorio de GitHub, así:

Configuración de secretos del repositorio de GitHub que muestra un secreto NPM_TOKEN cifrado, con botones para actualizar y eliminar

Por último, actualicemos nuestro flujo de trabajo de GitHub Actions para incluir también un paso de publicación. Después del trabajo build, agrega el siguiente trabajo publish:

publish:
    if: github.ref == 'refs/heads/main'
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 14
      - name: Publish
        run: |
          npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
          npm publish --ignore-scripts
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Es fundamental tener en cuenta el argumento --ignore-scripts del comando npm publish, que es clave para un flujo de publicación seguro. Este argumento indica al comando de publicación de la CLI de npm que omita todos los scripts de ciclo de vida especificados en el manifiesto packge.json. Esto es importante: cuando se instala un paquete malicioso como parte del grafo de dependencias, puede agregar una entrada como prepack: “echo ‘do something malicious’” a tu propio paquete, que se ejecutará al usar npm publish. Como puedes imaginar, esa entrada maliciosa puede causar mucho más daño que solo imprimir algo en pantalla.

El flujo de trabajo que seguimos aquí reduce considerablemente el riesgo de que roben los tokens de npm o de que se ejecuten comandos arbitrarios dirigidos a otros objetivos, porque:

  1. En nuestro trabajo de compilación, instalamos paquetes sin permitir que ejecuten comandos arbitrarios, gracias al argumento --ignore-scripts que se pasa a npm ci.

  2. Nuestro trabajo publish comienza con una nueva extracción del repositorio. Esto significa que, aunque en el paso anterior build hubiera sido necesario permitir la ejecución de scripts durante la instalación de paquetes npm, todos los intentos maliciosos de inyectar datos en el archivo package.json del paquete serían inútiles.

  3. Como medida de precaución, nuestro paso publish incluye el argumento de comando --ignore-scripts para evitar la ejecución de scripts de ciclo de vida de npm durante esta fase.

  4. El token de automatización de npm para publicar un paquete solo está disponible para el paso de publicación.

Con todo lo anterior, probablemente dormirías mejor si exigieras la autenticación de dos factores para publicar cada versión del paquete. Sin embargo, esto requiere una configuración más compleja si necesitas habilitarla en un entorno de CI, así que no lo veremos en este artículo.

Este trabajo se ejecuta específicamente solo en la rama principal —así no publicamos durante la ejecución de pruebas de un pull request— y usa una versión específica de Node.js, en lugar de ejecutar trabajos en paralelo con distintas versiones.

Una vez que se combina un pull request de GitHub o se hace push de una confirmación a la rama principal, se ejecutará el siguiente trabajo publish y se publicará el paquete npm.

Flujo de trabajo de GitHub Actions que muestra una tarea de publicación exitosa, con los pasos de configuración, checkout, Node.js, publicación y finalización completados

Consulta el archivo del flujo de trabajo completo de GitHub Actions como referencia.

Es importante tener en cuenta que no abordamos ningún tipo de control automático de versiones semánticas para actualizar automáticamente las versiones de nuestros paquetes npm según si el push de una confirmación corresponde a una actualización patch, minor o major del código. Para eso, recomiendo evaluar semantic-release, que Snyk Advisor también señala como un paquete muy saludable:

Panel de estado del paquete de Snyk para semantic-release, con una puntuación de 90/100, tendencias de descargas y gráficos de actividad de mantenimiento.

¿Dónde se ejecuta GitHub Actions?

GitHub Actions funciona en el ámbito de un repositorio específico. Se ejecuta y administra mediante los servicios de infraestructura en la nube de GitHub, y ofrece compatibilidad con ejecutores para Mac, Windows y Linux. Sin embargo, también es posible tener ejecutores de GitHub autohospedados, por ejemplo, en la infraestructura de Google Cloud.

¿Qué es el flujo de GitHub?

GitHub Actions funciona en el ámbito de un repositorio específico. Se ejecuta y administra mediante los servicios de infraestructura en la nube de GitHub, y ofrece compatibilidad con ejecutores para Mac, Windows y Linux. Sin embargo, también es posible tener ejecutores de GitHub autohospedados, por ejemplo, en la infraestructura de Google Cloud.

En resumen

En resumen, puedes automatizar de forma segura la creación de paquetes npm y su publicación en el ecosistema npm con GitHub Actions. La ventaja de elegir GitHub Actions es que mantenemos toda la experiencia de desarrollo de un flujo de trabajo de git dentro de la plataforma de GitHub.

También recomiendo explorar otras integraciones de GitHub Actions que puedes incorporar fácilmente a tu proyecto:

Prueba mi is-website-vulnerable GitHub Action si quieres dar seguimiento a las pruebas de seguridad de extremo a extremo de un sitio web (para detectar bibliotecas de JavaScript vulnerables).

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.