BLOG | NGINX

Tutorial de NGINX: Cómo gestionar secretos de forma segura en contenedores

NGINX - Parte de F5 - horizontal, negro, tipo RGB
Robert Haynes Miniatura
Robert Haynes
Publicado el 14 de marzo de 2023

Esta publicación es uno de los cuatro tutoriales que te ayudan a poner en práctica los conceptos de Microservicios de marzo de 2023: Comience a entregar microservicios :

Muchos de sus microservicios necesitan secretos para funcionar de forma segura. Algunos ejemplos de secretos incluyen la clave privada para un certificado SSL/TLS, una clave API para autenticarse en otro servicio o una clave SSH para iniciar sesión de forma remota. Una gestión adecuada de secretos requiere limitar estrictamente los contextos en los que se utilizan únicamente a los lugares donde es necesario y evitar que se acceda a ellos excepto cuando sea necesario. Pero esta práctica a menudo se omite en las prisas del desarrollo de aplicação . ¿El resultado? La gestión inadecuada de secretos es una causa común de fugas de información y explotación.

Descripción general del tutorial

En este tutorial, mostramos cómo distribuir y usar de forma segura un JSON Web Token (JWT) que un contenedor de cliente utiliza para acceder a un servicio. En los cuatro desafíos de este tutorial, experimentarás con cuatro métodos diferentes para administrar secretos, para aprender no solo cómo administrar los secretos correctamente en tus contenedores, sino también sobre los métodos que son inadecuados:

Si bien este tutorial utiliza un JWT como ejemplo de secreto, las técnicas se aplican a cualquier cosa que necesite mantener en secreto en los contenedores, como credenciales de base de datos, claves privadas SSL y otras claves API.

El tutorial aprovecha dos componentes de software principales:

  • Servidor API : un contenedor que ejecuta NGINX de código abierto y algún código JavaScript NGINX básico que extrae un reclamo del JWT y devuelve un valor de uno de los reclamos o, si no hay ningún reclamo presente, un mensaje de error
  • Cliente API : un contenedor que ejecuta un código Python muy simple que simplemente realiza una solicitud GET al servidor API

Mire este vídeo para ver una demostración del tutorial en acción.

La forma más sencilla de realizar este tutorial es registrarse en Microservices March y utilizar el laboratorio basado en navegador que se proporciona. Esta publicación proporciona instrucciones para ejecutar el tutorial en su propio entorno.

Prerrequisitos y configuración

Requisitos previos

Para completar el tutorial en su propio entorno, necesitará:

  • Un entorno compatible con Linux/Unix
  • Familiaridad básica con la línea de comandos de Linux
  • Un editor de texto como nano o vim
  • Docker (incluidos Docker Compose y Docker Engine Swarm )
  • curl (ya instalado en la mayoría de los sistemas)
  • git (ya instalado en la mayoría de los sistemas)

Notas:

  • El tutorial utiliza un servidor de prueba que escucha en el puerto 80. Si ya está usando el puerto 80, use el indicador -p para establecer un valor diferente para el servidor de prueba cuando lo inicie con el comando docker run . Luego incluya el :<número_de_puerto> sufijo en host local en el rizo comandos.
  • A lo largo del tutorial se omite el aviso en la línea de comandos de Linux para que sea más fácil cortar y pegar los comandos en su terminal. La tilde ( ~ ) representa su directorio de inicio.

Configuración

En esta sección , clonará el repositorio del tutorial , iniciará el servidor de autenticación y enviará solicitudes de prueba con y sin un token.

Clonar el repositorio del tutorial

  1. En su directorio de inicio, cree el directorio microservices-march y clone el repositorio de GitHub en él. (También puede utilizar un nombre de directorio diferente y adaptar las instrucciones en consecuencia). El repositorio incluye archivos de configuración y versiones separadas de la aplicação cliente API que utilizan diferentes métodos para obtener secretos.

    mkdir ~/microservices-marchcd ~/microservices-march
    git clone https://github.com/microservices-march/auth.git
  2. Mostrar el secreto. Es un JWT firmado, comúnmente utilizado para autenticar clientes API en servidores.

    gato ~/microservicios-marzo/auth/apiclient/token1.jwt "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Si bien hay algunas formas de usar este token para la autenticación, en este tutorial la aplicación cliente API lo pasa al servidor de autenticación mediante el marco de autorización de token de portador OAuth 2.0 . Esto implica anteponer Autorización al JWT: Portador como en este ejemplo:

"Autorización: Portador eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Construir e iniciar el servidor de autenticación

  1. Cambiar al directorio del servidor de autenticación:

    servidor de cd
  2. Construya la imagen de Docker para el servidor de autenticación (tenga en cuenta el período final):

    docker build -t apiserver .
  3. Inicie el servidor de autenticación y confirme que esté en ejecución (la salida se distribuye en varias líneas para facilitar su lectura):

    docker run -d -p 80:80 apiserver docker ps ID DEL CONTENEDOR IMAGEN COMANDO ...
    2b001f77c5cb apiserver "nginx -g 'demonio de..." ... ... ESTADO CREADO ... ... Hace 26 segundos Arriba 26 segundos ... ... PUERTOS ... ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ... ... NOMBRES... relajado_proskuriakova

Probar el servidor de autenticación

  1. Verifique que el servidor de autenticación rechace una solicitud que no incluya el JWT y devuelva 401Autorización requerida :

    curl -X GET http://localhost<html>
    <head><title>Se requiere autorización 401</title></head>
    <body>
    <center><h1>Se requiere autorización 401</h1></center>
    <hr><center>nginx/1.23.3</center>
    </body>
    </html>
  2. Proporcione el JWT utilizando el encabezado de autorización . El200 El código de retorno OK indica que la aplicación cliente API se autenticó exitosamente.

    curl -i -X GET -H "Autorización: Portador `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhost HTTP/1.1 200 OK Servidor: nginx/1.23.2 Fecha: Día , DD Lun AAAA hh : mm : ss TZ Tipo de contenido: texto/html Longitud del contenido: 64 Última modificación: Día , DD Lun AAAA hh : mm : ss TZ Conexión: keep-alive ETag: "63dc0fcd-40" MENSAJE X: apiKey1 de éxito Accept-Ranges: bytes { "respuesta": "éxito", "autorizado": verdadero, "valor": "999" }

Desafío 1: Secretos de código duro en tu aplicación (¡No!)

Antes de comenzar este desafío, seamos claros: ¡codificar secretos en tu aplicación es una terrible idea! Verá cómo cualquier persona con acceso a la imagen del contenedor puede encontrar y extraer fácilmente credenciales codificadas.

En este desafío, copia el código de la aplicación cliente API en el directorio de compilación, compila y ejecuta la aplicación y extrae el secreto .

Copiar la aplicación cliente API

El subdirectorio app_versions del directorio apiclient contiene diferentes versiones de la aplicación cliente API simple para los cuatro desafíos, cada una ligeramente más segura que la anterior (consulte la Descripción general del tutorial para obtener más información).

  1. Cambiar al directorio del cliente API:

    cd ~/microservicios-march/auth/apiclient
  2. Copia la aplicación para este desafío (la que tiene un secreto codificado) al directorio de trabajo:

    cp ./app_versions/código_duro_muy_malo.py ./app.py
  3. Echa un vistazo a la aplicación:

    cat app.py importar urllib.solicitud importar urllib.error jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" authstring = "Portador " + jwt req = urllib.solicitud.Solicitud("http://host.docker.internal") req.add_header("Autorización", authstring) intentar: con urllib.request.urlopen(req) como respuesta: la_pagina = respuesta.read() mensaje = respuesta.getheader("X-MESSAGE") imprimir("200 " + mensaje) excepto urllib.error.URLError como e: imprimir(str(e.code) + " s " + e.msg)

    El código simplemente realiza una solicitud a un host local e imprime un mensaje de éxito o un código de error.

    La solicitud agrega el encabezado de Autorización en esta línea:

    req.add_header("Autorización", cadena de autenticación)

    ¿Notas algo más? ¿Quizás un JWT codificado? Llegaremos a eso en un minuto. Primero, construyamos y ejecutemos la aplicación.

Construir y ejecutar la aplicación cliente API

Estamos usando el comando docker compose junto con un archivo YAML de Docker Compose: esto hace que sea un poco más fácil entender qué está sucediendo.

(Tenga en cuenta que en el Paso 2 de la sección anterior cambió el nombre del archivo Python para la aplicación cliente API específica del Desafío 1 ( very_bad_hard_code.py ) a app.py. También harás esto en los otros tres desafíos. Usar app.py cada vez simplifica la logística porque no es necesario cambiar el Dockerfile . Esto significa que debes incluir el argumento ‑build en el comando docker compose para forzar una reconstrucción del contenedor cada vez).

El comando docker compose crea el contenedor, inicia la aplicação, realiza una única solicitud de API y luego apaga el contenedor, mientras muestra los resultados de la llamada de API en la consola.

El200 El código de éxito en la segunda línea antes de la última de la salida indica que la autenticación se realizó correctamente. El valor apiKey1 es una confirmación adicional, ya que muestra que el servidor de autenticación pudo decodificar la reclamación de ese nombre en el JWT:

docker compose -f docker-compose.hardcode.yml up -build ... apiclient-apiclient-1 | 200 Éxito apiKey1 apiclient-apiclient-1 salió con el código 0

Entonces, las credenciales codificadas funcionaron correctamente para nuestra aplicación cliente API, lo cual no es sorprendente. ¿Pero es seguro? ¿Quizás sea así, ya que el contenedor ejecuta este script solo una vez antes de salir y no tiene un shell?

De hecho, no, no es seguro en absoluto.

Recuperar el secreto de la imagen del contenedor

La codificación rígida de las credenciales deja abiertas a la inspección de cualquiera que pueda acceder a la imagen del contenedor, porque extraer el sistema de archivos de un contenedor es un ejercicio trivial.

  1. Crea el directorio de extracción y cámbialo:

    mkdir extractcd extraer
  2. Enumere información básica sobre las imágenes del contenedor. El indicador --format hace que la salida sea más legible (y la salida se distribuye en dos líneas aquí por la misma razón):

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTENEDOR ID NOMBRES IMAGEN ...
    11b73106fdf8 apiclient-apiclient-1 apiclient ... ad9bdc05b07c emocionante_clarke apiserver ... ... ESTADO CREADO... Hace 6 minutos Salió (0) Hace 4 minutos ... Hace 43 minutos Arriba 43 minutos
  3. Extraiga la imagen de apiclient más reciente como un archivo .tar . Para <ID del contenedor>, sustituya el valor de la RECIPIENTE IDENTIFICACIÓN campo en la salida anterior (11b73106fdf8 en este tutorial):

    docker export -o api.tar <ID del contenedor>

    Se necesitan unos segundos para crear el archivo api.tar , que incluye todo el sistema de archivos del contenedor. Un enfoque para encontrar secretos es extraer todo el archivo y analizarlo, pero resulta que hay un atajo para encontrar lo que probablemente sea interesante: mostrar el historial del contenedor con el comando docker history . (Este atajo es especialmente útil porque también funciona para contenedores que encuentre en Docker Hub u otro registro de contenedores y, por lo tanto, es posible que no tengan el Dockerfile , sino solo la imagen del contenedor).

  4. Mostrar el historial del contenedor:

    Historial de Docker Apclient Imagen creada...
    9396dde2aad0 Hace 8 minutos ...  Hace 8 minutos ...  Hace 28 minutos ... ... CREADO POR TAMAÑO... ... CMD ["python" "./app.py"] 622B ... ... COPIA ./app.py ./app.py # buildkit 0B ... ... WORKDIR /usr/app/src 0B ... ... COMENTARIO... buildkit.dockerfile.v0... buildkit.dockerfile.v0... buildkit.dockerfile.v0

    Las líneas de salida están en orden cronológico inverso. Muestran que el directorio de trabajo se configuró en /usr/app/src y luego se copió y ejecutó el archivo de código Python para la aplicación. No hace falta ser un gran detective para deducir que el código base principal de este contenedor está en /usr/app/src/app.py y, como tal, es una ubicación probable para las credenciales.

  5. Armado con ese conocimiento, extraiga solo ese archivo:

    tar --extract --file=api.tar usr/app/src/app.py
  6. Mostramos el contenido del archivo y, así de fácil, hemos obtenido acceso al JWT “seguro”:

    gato usr/app/src/app.py ... jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" ...

Desafío 2: Pasar secretos como variables de entorno (¡de nuevo, no!)

Si completó la Unidad 1 de Microservicios de marzo de 2023 (Aplicar la aplicación de doce factores a las arquitecturas de microservicios), está familiarizado con el uso de variables de entorno para pasar datos de configuración a contenedores. Si te lo perdiste, no te preocupes: estará disponible a pedido después de registrarte .

En este desafío, pasa secretos como variables de entorno. Al igual que el método del Desafío 1 , ¡no recomendamos este! No es tan malo como codificar secretos, pero como verás, tiene algunas debilidades.

Hay cuatro formas de pasar variables de entorno a un contenedor:

  • Utilice la declaración ENV en un Dockerfile para realizar la sustitución de variables (establecer la variable para todas las imágenes creadas). Por ejemplo:

    PUERTO ENV $PUERTO
  • Utilice la bandera -e en el comando docker run . Por ejemplo:

    docker run -e CONTRASEÑA=123 micontenedor
  • Utilice la clave de entorno en un archivo YAML de Docker Compose.
  • Utilice un archivo .env que contenga las variables.

En este desafío, utiliza una variable de entorno para configurar el JWT y examina el contenedor para ver si el JWT está expuesto.

Pasar una variable de entorno

  1. Regrese al directorio del cliente API:

    cd ~/microservicios-march/auth/apiclient
  2. Copie la aplicación para este desafío (la que usa variables de entorno) al directorio de trabajo, sobrescribiendo el archivo app.py del Desafío 1:

    cp ./app_versions/variables_de_entorno_medio.py ./app.py
  3. Eche un vistazo a la aplicación. En las líneas de salida relevantes, el secreto (JWT) se lee como una variable de entorno en el contenedor local:

    cat app.py ... jwt = "" si "JWT" en os.environ: jwt = "Portador " + os.environ.get("JWT") ...
  4. Como se explicó anteriormente, hay varias formas de introducir la variable de entorno en el contenedor. Para mantener la coherencia, seguiremos utilizando Docker Compose. Muestra el contenido del archivo YAML de Docker Compose, que utiliza la clave de entorno para establecer la variable de entorno JWT :

    gato docker-compose.env.yml --- versión: Servicios "3.9": apiclient: compilación: . imagen: apiclient extra_hosts: - "host.docker.internal:host-gateway" entorno: - JWT
  5. Ejecute la aplicación sin configurar la variable de entorno. El401 El código no autorizado en la segunda línea antes de la última de la salida confirma que la autenticación falló porque la aplicación cliente de API no pasó el JWT:

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 401 No autorizado apiclient-apiclient-1 salió con el código 0
  6. Para simplificar, configure la variable de entorno localmente. Está bien hacer eso en este punto del tutorial, ya que no es el problema de seguridad que nos preocupa en este momento:

    exportar JWT=`cat token1.jwt`
  7. Ejecute el contenedor nuevamente. Ahora la prueba tiene éxito, con el mismo mensaje que en el Desafío 1:

    docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 200 Éxito apiKey1 apiclient-apiclient-1 salió con el código 0

Así que al menos ahora la imagen base no contiene el secreto y podemos pasarla en tiempo de ejecución, lo cual es más seguro. Pero todavía hay un problema.

Examinar el contenedor

  1. Muestra información sobre las imágenes del contenedor para obtener el ID del contenedor para la aplicación cliente de API (la salida se distribuye en dos líneas para facilitar la legibilidad):

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" CONTENEDOR ID NOMBRES IMAGEN ...
    6b20c75830df apiclient-apiclient-1 apiclient ... ad9bdc05b07c emocionante_clarke apiserver ... ... ESTADO CREADO... Hace 6 minutos Salió (0) Hace 6 minutos ... Hace aproximadamente una hora Arriba Hace aproximadamente una hora
  2. Inspeccione el contenedor para la aplicación cliente de API. Para <ID del contenedor>, sustituya el valor de la RECIPIENTE IDENTIFICACIÓN campo en la salida anterior (aquí 6b20c75830df).

    El comando docker inspect le permite inspeccionar todos los contenedores iniciados, ya sea que se estén ejecutando o no. Y ese es el problema: aunque el contenedor no se esté ejecutando, la salida expone el JWT en la matriz Env , guardado de forma insegura en la configuración del contenedor.

    inspección de Docker <ID del contenedor>...
    "Env": [ "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...", "RUTA=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "CLAVE_GPG=A035C8C19219BA821ECEA86B64E628F8D684696D", "VERSIÓN_PYTHON=3.11.2", "VERSIÓN_PIP_PYTHON=22.3.1", "VERSIÓN_HERRAMIENTAS_DE_CONFIGURACIÓN_PYTHON=65.5.1", URL de PYTHON_GET_PIP=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", "SHA256 de PYTHON_GET_PIP=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..."]

Desafío 3: Utilice secretos locales

A esta altura ya debe saber que codificar secretos y usar variables de entorno no es tan seguro como usted (o su equipo de seguridad) necesita.

Para mejorar la seguridad, puedes intentar usar secretos locales de Docker para almacenar información confidencial. Nuevamente, este no es el método estándar de oro, pero es bueno entender cómo funciona. Incluso si no usas Docker en producción, lo importante es saber cómo puedes dificultar la extracción del secreto de un contenedor.

En Docker, los secretos se exponen a un contenedor a través del sistema de archivos mount /run/secrets/, donde hay un archivo separado que contiene el valor de cada secreto.

En este desafío , pasa un secreto almacenado localmente al contenedor mediante Docker Compose y luego verifica que el secreto no sea visible en el contenedor cuando se usa este método.

Pasar un secreto almacenado localmente al contenedor

  1. Como ya te puedes imaginar, empieza cambiando al directorio apiclient :

    cd ~/microservicios-march/auth/apiclient
  2. Copia la aplicación para este desafío (la que usa secretos desde dentro de un contenedor) al directorio de trabajo, sobrescribiendo el archivo app.py del Desafío 2:

    cp ./app_versions/better_secrets.py ./app.py
  3. Eche un vistazo al código Python, que lee el valor JWT del archivo /run/secrets/jot . (Y sí, probablemente deberíamos comprobar que el archivo solo tenga una línea. ¿Tal vez en Microservicios en marzo de 2024?)

    cat app.py ... jotfile = "/run/secrets/jot" jwt = "" si os.path.isfile(jotfile): con open(jotfile) como jwtfile: para línea en jwtfile: jwt = "Portador " + línea ...

    Bien, entonces ¿cómo vamos a crear este secreto? La respuesta está en el archivo docker-compose.secrets.yml .

  4. Eche un vistazo al archivo Docker Compose, donde el archivo secreto se define en la sección de secretos y luego es referenciado por el servicio apiclient :

    gato docker-compose.secrets.yml --- versión: Secretos "3.9": jot: archivo: token1.jwt servicios: apiclient: compilación: . extra_hosts: - "host.docker.internal:host-gateway" secretos: - jot

Verifique que el secreto no esté visible en el contenedor

  1. Ejecute la aplicación. Dado que el JWT es accesible dentro del contenedor, la autenticación se realiza correctamente con el mensaje ya conocido:

    docker compose -f docker-compose.secrets.yml up -build ... apiclient-apiclient-1 | 200 Éxito apiKey1 apiclient-apiclient-1 salió con el código 0
  2. Muestra información sobre las imágenes del contenedor, anotando el ID del contenedor para la aplicación cliente de API (para ver un ejemplo de salida, consulta el Paso 1 en Examinar el contenedor del Desafío 2):

    docker ps -a --format "tabla {{.ID}}\t{{.Nombres}}\t{{.Imagen}}\t{{.RunningFor}}\t{{.Estado}}"
  3. Inspeccione el contenedor para la aplicación cliente de API. Para <ID del contenedor>, sustituya el valor de la RECIPIENTE IDENTIFICACIÓN campo en la salida del paso anterior. A diferencia de la salida del Paso 2 de Examinar el contenedor , no hay ninguna línea JWT= al comienzo de la sección Env :

    inspección de Docker <ID del contenedor>
    "Env": [
    "RUTA=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "LANG=C.UTF-8",
    "CLAVE_GPG=A035C8C19219BA821ECEA86B64E628F8D684696D",
    "VERSIÓN_PYTHON=3.11.2",
    "VERSIÓN_PIP_PYTHON=22.3.1",
    "VERSIÓN_HERRAMIENTAS_DE_CONFIGURACIÓN_PYTHON=65.5.1",
    "URL_GET_PIP_PYTHON=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..."

    Hasta ahora todo bien, pero nuestro secreto está en el sistema de archivos contenedor en /run/secrets/jot . Tal vez podamos extraerlo de allí usando el mismo método que en Recuperar el secreto de la imagen del contenedor del desafío 1.

  4. Cambie al directorio de extracción (que creó durante el Desafío 1) y exporte el contenedor a un archivo tar :

    cd extractdocker export -o api2.tar <ID del contenedor>
  5. Busque el archivo de secretos en el archivo tar :

    tar tvf api2.tar | grep jot -rwxr-xr-x 0 0 0 0 Mon DD hh :mm ejecutar/secretos/jot

    Uh oh, el archivo con el JWT está visible. ¿No dijimos que incrustar secretos en el contenedor era “seguro”? ¿Las cosas están tan mal como en el Desafío 1?

  6. Veamos: extraiga el archivo de secretos del archivo tar y observe su contenido:

    tar --extract --file=api2.tar ejecutar/secretos/jotcat ejecutar/secretos/jot

    ¡Albricias! No hay salida del comando cat , lo que significa que el archivo run/secrets/jot en el sistema de archivos contenedor está vacío: ¡no hay ningún secreto para ver allí! Incluso si hay un artefacto secreto en nuestro contenedor, Docker es lo suficientemente inteligente como para no almacenar ningún dato confidencial en el contenedor.

Dicho esto, aunque esta configuración de contenedor es segura, tiene un inconveniente. Depende de la existencia de un archivo llamado token1.jwt en el sistema de archivos local cuando ejecuta el contenedor. Si cambia el nombre del archivo, fallará el intento de reiniciar el contenedor. (Puedes probar esto tú mismo renombrando [¡no eliminando!] token1.jwt y ejecutando el comando docker compose del Paso 1 nuevamente).

Así que estamos a mitad de camino: el contenedor usa secretos de una forma que los protege contra una fácil vulneración, pero el secreto todavía no está protegido en el host. No quieres que los secretos se almacenen sin cifrar en un archivo de texto sin formato. Es hora de implementar una herramienta de gestión de secretos.

Desafío 4: Utilice un administrador de secretos

Un administrador de secretos le ayuda a administrar, recuperar y rotar secretos a lo largo de sus ciclos de vida. Hay muchos administradores de secretos para elegir y todos cumplen un propósito similar:

  • Almacenar secretos de forma segura
  • Control de acceso
  • Distribuirlos en tiempo de ejecución
  • Habilitar la rotación secreta

Sus opciones para la gestión de secretos incluyen:

Para simplificar, este desafío utiliza Docker Swarm, pero los principios son los mismos para muchos administradores de secretos.

En este desafío, crea un secreto en Docker , copia el secreto y el código del cliente API , implementa el contenedor , ve si puedes extraer el secreto y rota el secreto .

Configurar un secreto de Docker

  1. Como ya es tradición, cambie al directorio apiclient :

    cd ~/microservicios-march/auth/apiclient
  2. Inicializar Docker Swarm:

    docker swarm init Swarm inicializado: el nodo actual (t0o4eix09qpxf4ma1rrs9omrm) ahora es un administrador. ...
  3. Crea un secreto y guárdalo en token1.jwt :

    docker secret crea jot ./token1.jwt qe26h73nhb35bak5fr5east27
  4. Mostrar información sobre el secreto. Tenga en cuenta que el valor secreto (el JWT) no se muestra:

    Inspeccionar secreto de Docker Jot [ { "ID": "qe26h73nhb35bak5fr5east27", "Versión": { "Índice": 11 }, "CreadoEn": " AAAA - MM - DD T hh : mm : ss . ms Z", "Actualizado a las": " AAAA - MM - DD T hh : mm : ss . ms Z", "Especificación": { "Nombre": "jot", "Etiquetas": {} } } ]

Utilice un secreto de Docker

Usar el secreto de Docker en el código de la aplicação cliente API es exactamente lo mismo que usar un secreto creado localmente: puedes leerlo desde el sistema de archivos /run/secrets/ . Todo lo que necesita hacer es cambiar el calificador secreto en su archivo YAML de Docker Compose.

  1. Eche un vistazo al archivo YAML de Docker Compose. Tenga en cuenta el valor verdadero en el campo externo , lo que indica que estamos usando un secreto de Docker Swarm:

    gato docker-compose.secretmgr.yml --- versión: "3.9" secretos: jot: externo: verdadero servicios: apiclient: compilación: . imagen: apiclient extra_hosts: - "host.docker.internal:host-gateway" secretos: - jot

    Por lo tanto, podemos esperar que este archivo Compose funcione con nuestro código de aplicação cliente API existente. Bueno, casi. Si bien Docker Swarm (o cualquier otra plataforma de orquestación de contenedores) aporta mucho valor adicional, también implica cierta complejidad adicional.

    Dado que Docker Compose no funciona con secretos externos, tendremos que usar algunos comandos de Docker Swarm, en particular Docker Stack Deploy . Docker Stack oculta la salida de la consola, por lo que tenemos que escribir la salida en un registro y luego inspeccionarlo.

    Para facilitar las cosas, también utilizamos un bucle while True continuo para mantener el contenedor en ejecución.

  2. Copie la aplicación para este desafío (la que usa un administrador de secretos) al directorio de trabajo, sobrescribiendo el archivo app.py del Desafío 3. Al mostrar el contenido de app.py , vemos que el código es casi idéntico al código del Desafío 3. La única diferencia es la adición del bucle while True :

    cp ./app_versions/best_secretmgr.py ./app.pycat ./app.py ... mientras sea verdadero: time.sleep(5) intentar: con urllib.request.urlopen(req) como respuesta: the_page = response.read() mensaje = response.getheader("X-MESSAGE") imprimir("200 " + mensaje, archivo=sys.stderr) excepto urllib.error.URLError como e: imprimir(str(e.code) + " " + e.msg, archivo=sys.stderr)

Implementar el contenedor y verificar los registros

  1. Construya el contenedor (en desafíos anteriores, Docker Compose se encargó de esto):

    docker build -t apiclient .
  2. Implementar el contenedor:

    docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack Creando la red secretstack_default Creando el servicio secretstack_apiclient
  3. Enumere los contenedores en ejecución, anotando el ID del contenedor para secretstack_apiclient (como antes, la salida se distribuye en varias líneas para facilitar la lectura).

    docker ps --format "tabla {{.ID}}\t{{.Nombres}}\t{{.Imagen}}\t{{.RunningFor}}\t{{.Estado}}" ID DEL CONTENEDOR ...  
    20d0c83a8b86 ... ad9bdc05b07c ... ... NOMBRES ... ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ... ... emocionante_clarke ... ... ESTADO DE CREACIÓN DE LA IMAGEN... apiclient:latest Hace 31 segundos Activa hace 30 segundos... apiserver Hace 2 horas Activa hace 2 horas
  4. Mostrar el archivo de registro de Docker; para <ID del contenedor>, sustituya el valor de la RECIPIENTE IDENTIFICACIÓN campo en la salida del paso anterior (aquí, 20d0c83a8b86). El archivo de registro muestra una serie de mensajes de éxito, porque agregamos el bucle while True al código de la aplicação . Presione Ctrl+c para salir del comando.

    registros de Docker -f <ID del contenedor>200 éxito apiKey1
    200 éxito apiKey1
    200 éxito apiKey1
    200 éxito apiKey1
    200 éxito apiKey1
    200 éxito apiKey1
    200 éxito apiKey1
    ...
    ^c

Intenta acceder al secreto

Sabemos que no se establecen variables de entorno sensibles (pero siempre puedes comprobarlo con el comando docker inspect como en el Paso 2 de Examinar el contenedor en el Desafío 2).

Del Desafío 3 también sabemos que el archivo /run/secrets/jot está vacío, pero puedes comprobarlo:

cd extractdocker export -o api3.tar
tar --extract --file=api3.tar ejecutar/secretos/jot
cat ejecutar/secretos/jot

¡Éxito! No puedes obtener el secreto del contenedor ni leerlo directamente desde el secreto de Docker.

Gira el secreto

Por supuesto, con los privilegios adecuados podemos crear un servicio y configurarlo para leer el secreto en el registro o establecerlo como una variable de entorno. Además, es posible que hayas notado que la comunicación entre nuestro cliente API y el servidor no está cifrada (texto sin formato).

Por lo tanto, la fuga de secretos aún es posible con casi cualquier sistema de gestión de secretos. Una forma de limitar la posibilidad de que se produzcan daños es rotar (reemplazar) los secretos periódicamente.

Con Docker Swarm, solo puedes eliminar y luego volver a crear secretos (Kubernetes permite la actualización dinámica de secretos). Tampoco puedes eliminar secretos asociados a servicios en ejecución.

  1. Enumere los servicios en ejecución:

    servicio docker ls ID NOMBRE MODO ... sl4mvv48vgjz secretstack_apiclient replicado ... ... REPLICAS IMAGEN PUERTOS... 1/1 apiclient:último
  2. Eliminar el servicio secretstack_apiclient .

    servicio docker rm secretstack_apiclient
  3. Elimina el secreto y vuelve a crearlo con un nuevo token:

    docker secret rm jot
    docker secret create jot ./token2.jwt
  4. Recrear el servicio:

    Implementación de la pila Docker: archivo de composición. Docker-compose.secretmgr.yml. Secretstack
  5. Busque el ID del contenedor para apiclient (para obtener un ejemplo de salida, consulte el Paso 3 en Implementar el contenedor y verificar los registros ):

    docker ps --format "tabla {{.ID}}\t{{.Nombres}}\t{{.Imagen}}\t{{.RunningFor}}\t{{.Estado}}"
  6. Muestra el archivo de registro de Docker, que muestra una serie de mensajes de éxito. Para <ID del contenedor>, sustituya el valor de la RECIPIENTE IDENTIFICACIÓN campo en la salida del paso anterior. Presione Ctrl+c para salir del comando.

    registros de Docker -f <ID del contenedor>200 éxitos apiKey2
    200 éxitos apiKey2
    200 éxitos apiKey2
    200 éxitos apiKey2
    200 éxitos apiKey2
    ...
    ^c

¿Ves el cambio de apiKey1 a apiKey2 ? Has girado el secreto.

En este tutorial, el servidor API aún acepta ambos JWT, pero en un entorno de producción puedes dejar obsoletos los JWT más antiguos al requerir ciertos valores para los reclamos en el JWT o al verificar las fechas de vencimiento de los JWT.

Tenga en cuenta también que si está usando un sistema de secretos que permite actualizar su secreto, su código debe volver a leer el secreto con frecuencia para captar nuevos valores secretos.

Limpiar

Para limpiar los objetos que creaste en este tutorial:

  1. Eliminar el servicio secretstack_apiclient .

    servicio docker rm secretstack_apiclient
  2. Eliminar el secreto.

    docker secret rm jot
  3. Abandona el enjambre (suponiendo que hayas creado un enjambre sólo para este tutorial).

    Docker Swarm deja --force
  4. Matar el contenedor apiserver que se está ejecutando.

    docker ps -a | grep "apiserver" | awk {'imprimir $1'} |xargs docker kill
  5. Elimine los contenedores no deseados enumerándolos y luego eliminándolos.

    docker ps -a --format "tabla {{.ID}}\t{{.Nombres}}\t{{.Imagen}}\t{{.RunningFor}}\t{{.Estado}}"docker rm <ID del contenedor>
  6. Elimine cualquier imagen de contenedor no deseada enumerándola y eliminándola.

    lista de imágenes de Docker imagen de Docker RM <ID de imagen>

Próximos pasos

Puedes utilizar este blog para implementar el tutorial en tu propio entorno o probarlo en nuestro laboratorio basado en navegador ( regístrate aquí ). Para obtener más información sobre el tema de la exposición de servicios de Kubernetes, siga las otras actividades de la Unidad 2: Gestión de secretos de microservicios 101 .

Para obtener más información sobre la autenticación JWT de nivel de producción con NGINX Plus, consulte nuestra documentación y lea Autenticación de clientes API con JWT y NGINX Plus en nuestro blog.


"Esta publicación de blog puede hacer referencia a productos que ya no están disponibles o que ya no reciben soporte. Para obtener la información más actualizada sobre los productos y soluciones F5 NGINX disponibles, explore nuestra familia de productos NGINX . NGINX ahora es parte de F5. Todos los enlaces anteriores de NGINX.com redirigirán a contenido similar de NGINX en F5.com.