BLOG | NGINX

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

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 aplicación . ¿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 aplicación 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.

    cat ~/microservices-march/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:

"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

Construir e iniciar el servidor de autenticación

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

    cd apiserver
  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
    CONTAINER ID   IMAGE       COMMAND                  ...
    2b001f77c5cb   apiserver   "nginx -g 'daemon of..." ...  
    
    
        ... CREATED         STATUS          ...                                    
        ... 26 seconds ago  Up 26 seconds   ... 
    
    
        ... PORTS                                      ...
        ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ...
    
    
        ... NAMES
        ... relaxed_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>401 Authorization Required</title></head>
    <body>
    <center><h1>401 Authorization Required</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 "Authorization: Bearer `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhostHTTP/1.1 200 OK
    Server: nginx/1.23.2
    Date: Day, DD Mon YYYY hh:mm:ss TZ
    Content-Type: text/html
    Content-Length: 64
    Last-Modified: Day, DD Mon YYYY hh:mm:ss TZ
    Connection: keep-alive
    ETag: "63dc0fcd-40"
    X-MESSAGE: Success apiKey1
    Accept-Ranges: bytes
    
    
    { "response": "success", "authorized": true, "value": "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 ~/microservices-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/very_bad_hard_code.py ./app.py
  3. Echa un vistazo a la aplicación:

    cat app.pyimport urllib.request
    import urllib.error
    
    jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
    authstring = "Bearer " + jwt
    req = urllib.request.Request("http://host.docker.internal")
    req.add_header("Authorization", authstring)
    try:
        with urllib.request.urlopen(req) as response:
            the_page = response.read()
            message = response.getheader("X-MESSAGE")
            print("200  " + message)
    except urllib.error.URLError as e:
        print(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("Authorization", authstring)

    ¿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 aplicación, 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  Success apiKey1
apiclient-apiclient-1 exited with code 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 extract
  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}}"CONTAINER ID   NAMES                   IMAGE       ...
    11b73106fdf8   apiclient-apiclient-1   apiclient   ...
    ad9bdc05b07c   exciting_clarke         apiserver   ...
    
    
        ... CREATED          STATUS
        ... 6 minutes ago    Exited (0) 4 minutes ago
        ... 43 minutes ago   Up 43 minutes
  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 <container_ID>

    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:

    docker history apiclient
    IMAGE         CREATED        ...
    9396dde2aad0  8 minutes ago  ...                    
    <missing>     8 minutes ago  ...   
    <missing>     28 minutes ago ...  
                   
        ... CREATED BY                          SIZE ... 
        ... CMD ["python" "./app.py"]           622B ...   
        ... COPY ./app.py ./app.py # buildkit   0B   ... 
        ... WORKDIR /usr/app/src                0B   ...   
                 
        ... COMMENT
        ... 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”:

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

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

    docker run -e PASSWORD=123 mycontainer
  • 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 ~/microservices-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/medium_environment_variables.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 = ""
    if "JWT" in os.environ:
        jwt = "Bearer " + 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 :

    cat docker-compose.env.yml---
    version: "3.9"
    services:
      apiclient:
        build: .
        image: apiclient
        extra_hosts:
          - "host.docker.internal:host-gateway"
        environment:
          - 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  Unauthorized
    apiclient-apiclient-1 exited with code 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:

    export 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  Success apiKey1
    apiclient-apiclient-1 exited with code 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}}"CONTAINER ID   NAMES                   IMAGE      ...
    6b20c75830df   apiclient-apiclient-1   apiclient  ...
    ad9bdc05b07c   exciting_clarke         apiserver  ...
    
    
        ... CREATED             STATUS
        ... 6 minutes ago       Exited (0) 6 minutes ago
        ... About an hour ago   Up About an hour
  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.

    docker inspect <container_ID>...
    "Env": [
      "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...",
      "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "LANG=C.UTF-8",
      "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
      "PYTHON_VERSION=3.11.2",
      "PYTHON_PIP_VERSION=22.3.1",
      "PYTHON_SETUPTOOLS_VERSION=65.5.1",
      "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...",
      "PYTHON_GET_PIP_SHA256=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 ~/microservices-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 = ""
    if os.path.isfile(jotfile):
        with open(jotfile) as jwtfile:
            for line in jwtfile:
                jwt = "Bearer " + line
    ...

    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 :

    cat docker-compose.secrets.yml---
    version: "3.9"
    secrets:
      jot:
        file: token1.jwt
    services:
      apiclient:
        build: .
        extra_hosts:
          - "host.docker.internal:host-gateway"
        secrets:
          - 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 Success apiKey1
    apiclient-apiclient-1 exited with code 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 "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  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 :

    docker inspect <container_ID>
    "Env": [
      "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "LANG=C.UTF-8",
      "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D",
      "PYTHON_VERSION=3.11.2",
      "PYTHON_PIP_VERSION=22.3.1",
      "PYTHON_SETUPTOOLS_VERSION=65.5.1",
      "PYTHON_GET_PIP_URL=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 <container_ID>
  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 run/secrets/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 run/secrets/jotcat run/secrets/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 ~/microservices-march/auth/apiclient
  2. Inicializar Docker Swarm:

    docker swarm init
    Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
    ...
  3. Crea un secreto y guárdalo en token1.jwt :

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

    docker secret inspect jot[
      {
        "ID": "qe26h73nhb35bak5fr5east27",
        "Version": {
          "Index": 11
        },
        "CreatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
        "UpdatedAt": "YYYY-MM-DDThh:mm:ss.msZ",
        "Spec": {
          "Name": "jot",
          "Labels": {}
        }
      }
    ]

Utilice un secreto de Docker

Usar el secreto de Docker en el código de la aplicación 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:

    cat docker-compose.secretmgr.yml---
    version: "3.9"
    secrets:
      jot:
        external: true
    services:
      apiclient:
        build: .
        image: apiclient
        extra_hosts:
          - "host.docker.internal:host-gateway"
        secrets:
          - jot

    Por lo tanto, podemos esperar que este archivo Compose funcione con nuestro código de aplicación 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
    ...
    while True:
        time.sleep(5)
        try:
            with urllib.request.urlopen(req) as response:
                the_page = response.read()
                message = response.getheader("X-MESSAGE")
                print("200 " + message, file=sys.stderr)
        except urllib.error.URLError as e:
            print(str(e.code) + " " + e.msg, file=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
    Creating network secretstack_default
    Creating service 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 "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"CONTAINER ID  ...  
    20d0c83a8b86  ... 
    ad9bdc05b07c  ... 
    
        ... NAMES                                             ...  
        ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ...  
        ... exciting_clarke                                   ...                                 
    
        ... IMAGE              CREATED          STATUS
        ... apiclient:latest   31 seconds ago   Up 30 seconds
        ... apiserver          2 hours ago      Up 2 hours
  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 aplicación . Presione Ctrl+c para salir del comando.

    docker logs -f <container_ID>200 Success apiKey1
    200 Success apiKey1
    200 Success apiKey1
    200 Success apiKey1
    200 Success apiKey1
    200 Success 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 run/secrets/jot
cat run/secrets/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:

    docker service lsID             NAME                    MODE         ... 
    sl4mvv48vgjz   secretstack_apiclient   replicated   ... 
    
    
        ... REPLICAS   IMAGE              PORTS
        ... 1/1        apiclient:latest
  2. Eliminar el servicio secretstack_apiclient .

    docker service 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:

    docker stack deploy --compose-file 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 "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  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.

    docker logs -f <container_ID>200 Success apiKey2
    200 Success apiKey2
    200 Success apiKey2
    200 Success 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 .

    docker service 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 leave --force
  4. Matar el contenedor apiserver que se está ejecutando.

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

    docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>
  6. Elimine cualquier imagen de contenedor no deseada enumerándola y eliminándola.

    docker image list   docker image rm <image_ID>

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.