Esta publicación forma parte de una serie de cuatro tutoriales que le ayudarán a poner en práctica los conceptos de Marzo de los microservicios de 2023: empiece a implementar microservicios:
- Cómo implementar y configurar microservicios
- Cómo gestionar secretos de forma segura en contenedores (esta publicación)
- Cómo usar GitHub Actions para automatizar lanzamientos canary de microservicios
- Cómo usar el rastreo de OpenTelemetry para comprender tus microservicios
Muchos de tus microservicios necesitan secretos para funcionar de forma segura. Algunos ejemplos de secretos son la clave privada de un certificado SSL/TLS, una clave de API para autenticarse en otro servicio o una clave SSH para iniciar sesión de forma remota. Una gestión adecuada de los secretos exige limitar estrictamente los contextos en los que se utilizan únicamente a aquellos donde son necesarios e impedir que se acceda a ellos salvo cuando sea preciso. Sin embargo, esta práctica suele pasarse por alto debido a las prisas durante el desarrollo de aplicaciones. ¿El resultado? Una gestión inadecuada de los secretos es una causa habitual de filtraciones de información y vulnerabilidades explotables.
Descripción general del tutorial
En este tutorial, mostramos cómo distribuir y utilizar de forma segura un token web JSON (JWT) que un contenedor cliente utiliza para acceder a un servicio. En los cuatro retos de este tutorial, experimentarás con cuatro métodos diferentes para gestionar secretos, con el fin de aprender no solo a gestionarlos correctamente en tus contenedores, sino también a identificar los métodos que no son adecuados:
- Codifica los secretos directamente en tu aplicación
- Pasar secretos como variables de entorno
- Usar secretos locales
- Usa un gestor de secretos
Aunque este tutorial utiliza un JWT como ejemplo de secreto, estas técnicas se pueden aplicar a cualquier elemento de los contenedores que necesites mantener en secreto, como credenciales de bases de datos, claves privadas SSL y otras claves de API.
El tutorial utiliza dos componentes de software principales:
- Servidor de API – Un contenedor que ejecuta NGINX Open Source y código básico de NGINX JavaScript que extrae una declaración del JWT y devuelve el valor de una de las declaraciones o, si no hay ninguna, un mensaje de error
- Cliente de API – Un contenedor que ejecuta un código Python muy sencillo que simplemente realiza una solicitud
GETal servidor de API
Mira este vídeo para ver una demostración práctica del tutorial.
La forma más sencilla de seguir este tutorial es inscribirse en Microservices March y utilizar el laboratorio proporcionado, al que se accede desde el navegador. En esta publicación se explica cómo ejecutar el tutorial en su propio entorno.
Requisitos previos y configuración
Requisitos previos
Para completar el tutorial en tu propio entorno, necesitas:
- Un entorno compatible con Linux/Unix
- Conocimientos básicos de la línea de comandos de Linux
- Un editor de texto como
nanoovim - Docker (incluidos Docker Compose y Docker Engine Swarm)
curl(ya está instalado en la mayoría de los sistemas)git(ya está 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ás utilizando el puerto 80, usa la opción
‑ppara asignar un valor diferente al servidor de prueba cuando lo inicies con el comandodockerrun. A continuación, añade el sufijo:<port_number>alocalhosten los comandoscurl. - A lo largo del tutorial se omite el prompt de la línea de comandos de Linux para facilitar que pueda copiar y pegar los comandos en su terminal. La virgulilla (
~) representa su directorio personal.
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 token.
Clona el repositorio del tutorial
- En tu directorio personal, crea el directorio microservices-march y clona en él el repositorio de GitHub. (También puedes usar otro nombre de directorio y adaptar las instrucciones según corresponda). El repositorio incluye archivos de configuración y versiones independientes de la aplicación cliente de la API que utilizan distintos métodos para obtener secretos.
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/auth.git- Muestra el secreto. Es un JWT firmado que se utiliza habitualmente para autenticar clientes de API ante servidores.
cat ~/microservices-march/auth/apiclient/token1.jwt"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"Aunque hay varias formas de usar este token para la autenticación, en este tutorial la aplicación cliente de la API lo envía al servidor de autenticación mediante el marco de autorización con tokens de portador de OAuth 2.0. Para ello, se antepone al JWT Authorization: Bearer como en este ejemplo:
"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"Compilar e iniciar el servidor de autenticación
- Cambia al directorio del servidor de autenticación:
cd apiserver- Crea la imagen de Docker para el servidor de autenticación (ten en cuenta el punto final):
docker build -t apiserver .- Inicia el servidor de autenticación y confirma que está en ejecución (la salida se muestra 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_proskuriakovaPrueba el servidor de autenticación
- Comprueba que el servidor de autenticación rechaza una solicitud que no incluye el JWT y devuelve
401AuthorizationRequired:
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>- Proporciona el JWT mediante la cabecera
Authorization. El código de respuesta200OKindica que la aplicación cliente de la API se ha autenticado correctamente.
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" }Reto 1: Codifica secretos directamente en tu aplicación (¡no lo hagas!)
Antes de comenzar este reto, dejemos una cosa clara: incluir secretos directamente en el código de tu aplicación es una idea pésima. Verás que cualquiera que tenga acceso a la imagen del contenedor puede encontrar y extraer fácilmente las credenciales incluidas en el código.
En este reto, copiarás el código de la aplicación cliente de la API en el directorio de compilación, compilarás y ejecutarás la aplicación, y extraerás el secreto.
Copiar la aplicación cliente de la API
El subdirectorio app_versions del directorio apiclient contiene distintas versiones de la aplicación cliente de API sencilla para los cuatro desafíos, cada una ligeramente más segura que la anterior (consulte Descripción general del tutorial para obtener más información).
- Cambie al directorio del cliente de la API:
cd ~/microservices-march/auth/apiclient- Copia la aplicación de este reto —la que tiene un secreto codificado de forma fija— en el directorio de trabajo:
cp ./app_versions/very_bad_hard_code.py ./app.py- 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 y muestra un mensaje de éxito o un código de error. La solicitud añade la cabecera «
Authorization» en esta línea:
req.add_header("Authorization", authstring)- ¿Notas algo más? ¿Quizás un JWT codificado de forma fija? Volveremos a ello en un momento. Primero, vamos a compilar y ejecutar la aplicación.
Compilar y ejecutar la aplicación cliente de la API
Estamos utilizando el comando docker compose junto con un archivo YAML de Docker Compose, lo que facilita un poco la comprensión de lo que está ocurriendo.
(Ten en cuenta que, en el paso 2 de la sección anterior, cambiaste el nombre del archivo de Python de la aplicación cliente de la API específico del reto 1 (very_bad_hard_code.py) a app.py. También harás lo mismo en los otros tres retos. Usar app.py cada vez simplifica la gestión, ya que no es necesario modificar el Dockerfile. Sin embargo, esto implica que debes incluir el argumento ‑build en el comando docker compose para forzar que el contenedor se vuelva a compilar cada vez).
El comando docker compose crea el contenedor, inicia la aplicación, realiza una única solicitud a la API y, a continuación, detiene el contenedor mientras muestra en la consola los resultados de la llamada a la API.
El código 200 Success de la penúltima línea de la salida indica que la autenticación se ha realizado correctamente. El valor apiKey1 lo confirma, ya que demuestra que el servidor de autenticación ha podido descodificar la declaración con 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 0Así que las credenciales codificadas directamente funcionaron correctamente en nuestra aplicación cliente de la API, lo cual no es de extrañar. Pero ¿es seguro? Puede que sí, ya que el contenedor solo ejecuta este script una vez antes de finalizar y no dispone de un intérprete de comandos.
De hecho, no: no es seguro en absoluto.
Recuperar el secreto de la imagen del contenedor
Incluir credenciales directamente en el código permite que cualquiera que pueda acceder a la imagen del contenedor las inspeccione, ya que extraer el sistema de archivos de un contenedor es una tarea trivial.
- Cree el directorio extract y acceda a él:
mkdir extractcd extract- Muestra información básica sobre las imágenes de contenedor. La opción «
--format» hace que la salida sea más legible (y, por el mismo motivo, aquí se muestra dividida en dos líneas):
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- Extraiga la imagen más reciente de apiclient como un archivo .tar. En
<container_ID>, sustituya el valor del campoCONTAINERIDde la salida anterior (11b73106fdf8en este tutorial):
docker export -o api.tar <container_ID>- Se tarda unos segundos en crear el archivo api.tar, que incluye todo el sistema de archivos del contenedor. Una forma de buscar secretos consiste en extraer el archivo completo y analizarlo, pero resulta que hay un atajo para encontrar lo que probablemente sea de interés: mostrar el historial del contenedor con el comando
dockerhistory. (Este atajo resulta especialmente útil porque también funciona con los contenedores que se encuentran en Docker Hub o en otro registro de contenedores y de los que, por tanto, puede que no se disponga del archivo Dockerfile, sino únicamente de la imagen del contenedor). - Muestra 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 aparecen en orden cronológico inverso. Muestran que el directorio de trabajo se estableció en /usr/app/src, tras lo cual se copió y ejecutó el archivo de código Python de la aplicación. No hace falta ser un gran detective para deducir que el código principal de este contenedor se encuentra en /usr/app/src/app.py, por lo que es probable que allí haya credenciales.
- Con esa información, extrae únicamente ese archivo:
tar --extract --file=api.tar usr/app/src/app.py- Muestra el contenido del archivo y, así de fácil, habremos obtenido acceso al JWT «seguro»:
cat usr/app/src/app.py
...
jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
...Reto 2: Pasar secretos como variables de entorno (de nuevo, ¡no!)
Si completaste la Unidad 1 de Microservices March 2023 (Aplicar la metodología de aplicaciones de doce factores a las arquitecturas de microservicios), ya sabrás cómo usar variables de entorno para pasar datos de configuración a los contenedores. Si te la perdiste, no te preocupes: está disponible bajo demanda tras registrarte.
En este reto, se pasan secretos como variables de entorno. Al igual que el método del reto 1, ¡no recomendamos usarlo! No es tan malo como incluir los secretos directamente en el código, pero, como verás, presenta algunas debilidades.
Hay cuatro formas de pasar variables de entorno a un contenedor:
- Usa la instrucción
ENVen un archivo Dockerfile para sustituir variables (establece la variable para todas las imágenes creadas). Por ejemplo:
ENV PORT $PORT- Usa la opción
‑een el comandodockerrun. Por ejemplo:
docker run -e PASSWORD=123 mycontainer- Usa la clave
environmenten un archivo YAML de Docker Compose. - Usa un archivo .env que contenga las variables.
En este reto, usarás una variable de entorno para configurar el JWT y examinarás el contenedor para comprobar si el JWT queda expuesto.
Pasar una variable de entorno
- Vuelve al directorio del cliente de la API:
cd ~/microservices-march/auth/apiclient- Copia la aplicación de este reto —la que utiliza variables de entorno— en el directorio de trabajo, sobrescribiendo el archivo app.py del reto 1:
cp ./app_versions/medium_environment_variables.py ./app.py- Echa un vistazo a la aplicación. En las líneas de salida pertinentes, 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")
...- Como se ha explicado 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 «
environment» 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- Ejecuta la aplicación sin configurar la variable de entorno. El código
401Unauthorizedde la penúltima línea de la salida confirma que la autenticación falló porque la aplicación cliente de la API no proporcionó el JWT:
docker compose -f docker-compose.env.yml up -build...
apiclient-apiclient-1 | 401 Unauthorized
apiclient-apiclient-1 exited with code 0- Para simplificar, configura la variable de entorno localmente. No hay problema en hacerlo en este punto del tutorial, ya que ahora mismo no es el problema de seguridad que nos ocupa:
export JWT=`cat token1.jwt`- Vuelve a ejecutar el contenedor. Ahora la prueba se completa correctamente, con el mismo mensaje que en el Reto 1:
docker compose -f docker-compose.env.yml up -build
...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0Así que, al menos ahora, la imagen base no contiene el secreto y podemos proporcionarlo en tiempo de ejecución, lo cual es más seguro. Pero sigue habiendo un problema.
Examina el contenedor
- Muestra información sobre las imágenes de contenedor para obtener el ID del contenedor de la aplicación cliente de la API (la salida se distribuye en dos líneas para facilitar su lectura):
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- Inspecciona el contenedor de la aplicación cliente de la API. En lugar de
<container_ID>, introduce el valor del campoCONTAINERIDde la salida anterior (en este caso,6b20c75830df). El comandodockerinspectpermite inspeccionar todos los contenedores iniciados, tanto si están en ejecución como si no. Y ese es el problema: aunque el contenedor no esté en ejecución, la salida expone el JWT en la matrizEnv, guardado de forma no segura 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: Usa secretos locales
A estas alturas, ya has aprendido que incluir secretos directamente en el código y usar variables de entorno no es tan seguro como tú (o tu equipo de seguridad) necesitáis.
Para mejorar la seguridad, puedes probar a usar secretos locales de Docker para almacenar información confidencial. Una vez más, este no es el método de referencia, pero conviene entender cómo funciona. Aunque no uses Docker en producción, lo importante es aprender cómo dificultar la extracción del secreto de un contenedor.
En Docker, los secretos se ponen a disposición de un contenedor mediante el punto de montaje del sistema de archivos /run/secrets/ donde hay un archivo independiente que contiene el valor de cada secreto.
En este reto, pasarás al contenedor un secreto almacenado localmente mediante Docker Compose y, a continuación, comprobarás que el secreto no está visible en el contenedor cuando se utiliza este método.
Pasar un secreto almacenado localmente al contenedor
- Como ya cabría esperar, primero debes cambiar al directorio apiclient:
cd ~/microservices-march/auth/apiclient- Copia la aplicación de este reto —la que utiliza secretos desde dentro de un contenedor— en el directorio de trabajo, sobrescribiendo el archivo app.py del reto 2:
cp ./app_versions/better_secrets.py ./app.py- Echa 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. ¿Quizá en Microservices March 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, ¿cómo vamos a crear este secreto? La respuesta está en el archivo docker-compose.secrets.yml.
- Echa un vistazo al archivo de Docker Compose, donde el archivo secreto se define en la sección
secretsy, a continuación, el servicioapiclienthace referencia a él:
cat docker-compose.secrets.yml---
version: "3.9"
secrets:
jot:
file: token1.jwt
services:
apiclient:
build: .
extra_hosts:
- "host.docker.internal:host-gateway"
secrets:
- jotVerifica que el secreto no sea visible en el contenedor
- Ejecuta la aplicación. Como hemos hecho que el JWT sea accesible dentro del contenedor, la autenticación se realiza correctamente y aparece el mensaje que ya conocemos:
docker compose -f docker-compose.secrets.yml up -build...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0- Muestra información sobre las imágenes de contenedor y anota el ID del contenedor de la aplicación cliente de la API (para ver un ejemplo de salida, consulta el paso 1 de Examinar el contenedor del reto 2):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"- Inspeccione el contenedor de la aplicación cliente de la API. En lugar de
<container_ID>, introduzca el valor del campoCONTAINERIDque aparece en la salida del paso anterior. A diferencia de la salida del paso 2 de Examinar el contenedor, no hay ninguna líneaJWT=al principio de la secciónEnv:
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 aquí, todo bien, pero nuestro secreto se encuentra en el sistema de archivos del contenedor, en /run/secrets/jot. Quizá podamos extraerlo de ahí utilizando el mismo método que en «Recuperar el secreto de la imagen del contenedor» del reto 1.
- Ve al directorio extract (que creaste durante el reto 1) y exporta el contenedor a un archivo tar:
cd extractdocker export -o api2.tar <container_ID>- Busca 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- Vaya, el archivo que contiene el JWT está visible. ¿No habíamos dicho que incluir secretos en el contenedor era «seguro»? ¿Está la cosa tan mal como en el Reto 1?
- Veamos: extraigamos el archivo de secretos del archivo tar y examinemos su contenido:
tar --extract --file=api2.tar run/secrets/jotcat run/secrets/jot- ¡Buenas noticias! El comando
catno genera ninguna salida, lo que significa que el archivo run/secrets/jot del sistema de archivos del contenedor está vacío. ¡No hay ningún secreto que ver ahí! Aunque haya un artefacto de secretos en nuestro contenedor, Docker es lo bastante inteligente como para no almacenar datos confidenciales en él.
Dicho esto, aunque la configuración de este contenedor es segura, tiene un inconveniente. Al ejecutar el contenedor, depende de que exista un archivo llamado token1.jwt en el sistema de archivos local. Si cambia el nombre del archivo, no se podrá reiniciar el contenedor. (Puede comprobarlo cambiando el nombre de token1.jwt —¡sin eliminarlo!— y volviendo a ejecutar el comando docker compose del paso 1).
Así que ya estamos a medio camino: el contenedor utiliza secretos de forma que evita que se vean comprometidos fácilmente, pero el secreto sigue sin estar protegido en el host. No conviene almacenar secretos sin cifrar en un archivo de texto sin formato. Es hora de incorporar una herramienta de gestión de secretos.
Desafío 4: usar un gestor de secretos
Un gestor de secretos te ayuda a gestionar, recuperar y rotar secretos a lo largo de todo su ciclo de vida. Hay muchos gestores de secretos entre los que elegir y todos cumplen una función similar:
- Almacenar secretos de forma segura
- Controlar el acceso
- Distribuirlos en tiempo de ejecución
- Habilitar la rotación de secretos
Entre las opciones para gestionar secretos se incluyen:
- Todos los proveedores de servicios en la nube disponen de un servicio de gestión de secretos (por ejemplo, AWS Secrets Manager, Secret Manager de Google Cloud Platform y Key Vault de Microsoft Azure)
- Kubernetes tiene el objeto Secret object
- Hashicorp Vault es un popular gestor de secretos multiplataforma
- OpenShift dispone de un servicio de gestión de secretos
- Docker Swarm dispone de un servicio de gestión de secretos
Para simplificar, este reto utiliza Docker Swarm, pero los principios son los mismos para muchos gestores de secretos.
En este reto, crearás un secreto en Docker, copiarás el secreto y el código del cliente de la API, desplegarás el contenedor, comprobarás si puedes extraer el secreto y rotar el secreto.
Configurar un secreto de Docker
- Como ya es habitual, ve al directorio apiclient:
cd ~/microservices-march/auth/apiclient- Inicializa Docker Swarm:
docker swarm init
Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
...- Crea un secreto y guárdalo en «token1.jwt»:
docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27- Muestra información sobre el secreto. Ten en cuenta que el valor del 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": {}
}
}
]Usar un secreto de Docker
Usar el secreto de Docker en el código de la aplicación cliente de la API es exactamente igual que usar un secreto creado localmente: puede leerlo desde el sistema de archivos /run/secrets/. Solo tiene que cambiar el calificador del secreto en el archivo YAML de Docker Compose.
- Echa un vistazo al archivo YAML de Docker Compose. Observa el valor
trueen el campoexternal, que indica que estamos utilizando 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 tanto, podemos esperar que este archivo de Compose funcione con el código existente de nuestra aplicación cliente de la API. Bueno, casi. Aunque Docker Swarm (o cualquier otra plataforma de orquestación de contenedores) aporta mucho valor añadido, también introduce cierta complejidad adicional. Dado que
dockercomposeno funciona con secretos externos, tendremos que utilizar algunos comandos de Docker Swarm, en concretodockerstackdeploy. Docker Stack oculta la salida de la consola, por lo que tenemos que escribirla en un registro y, después, examinarlo. Para facilitar las cosas, también utilizamos un bucle continuowhileTruepara mantener el contenedor en ejecución. - Copia la aplicación de este reto —la que utiliza un gestor de secretos— en el directorio de trabajo, sobrescribiendo el archivo app.py del reto 3. Al mostrar el contenido de app.py, vemos que el código es prácticamente idéntico al del reto 3. La única diferencia es que se ha añadido el bucle
whileTrue:
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)Despliega el contenedor y comprueba los registros
- Crea el contenedor (en los retos anteriores, Docker Compose se encargaba de ello):
docker build -t apiclient .- Despliega el contenedor:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
Creating network secretstack_default
Creating service secretstack_apiclient- Enumere los contenedores en ejecución y anote el ID del contenedor secretstack_apiclient (como antes, la salida se distribuye en varias líneas para facilitar su 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- Muestra el archivo de registro de Docker; en lugar de
<container_ID>, usa el valor del campoCONTAINERIDque aparece en la salida del paso anterior (en este caso,20d0c83a8b86). El archivo de registro muestra una serie de mensajes de operación correcta porque añadimos el buclewhileTrueal código de la aplicación. PulsaCtrl+cpara 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
...
^cIntenta acceder al secreto
Sabemos que no hay ninguna variable de entorno confidencial configurada (aunque siempre puedes comprobarlo con el comando docker inspect, como en el paso 2 de «Examinar el contenedor» del reto 2).
Por el reto 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.
Rotar el secreto
Por supuesto, con los privilegios adecuados podemos crear un servicio y configurarlo para que escriba el secreto en el registro o lo establezca como variable de entorno. Además, puede que hayas observado que la comunicación entre nuestro cliente de API y el servidor no está cifrada (se transmite en texto sin formato).
Por tanto, la filtración de secretos sigue siendo posible con casi cualquier sistema de gestión de secretos. Una forma de limitar los posibles daños resultantes es rotar (sustituir) los secretos periódicamente.
Con Docker Swarm, solo se pueden eliminar los secretos y volver a crearlos posteriormente (Kubernetes permite actualizarlos de forma dinámica). Tampoco se pueden eliminar los secretos asociados a servicios en ejecución.
- Enumera los servicios en ejecución:
docker service lsID NAME MODE ...
sl4mvv48vgjz secretstack_apiclient replicated ...
... REPLICAS IMAGE PORTS
... 1/1 apiclient:latest- Elimina el servicio secretstack_apiclient.
docker service rm secretstack_apiclient- Elimina el secreto y vuelve a crearlo con un token nuevo:
docker secret rm jot docker secret create jot ./token2.jwt- Vuelva a crear el servicio:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack- Busque el ID del contenedor de
apiclient(para ver un ejemplo de salida, consulte el paso 3 de «Desplegar el contenedor y comprobar los registros»):
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"- Muestre el archivo de registro de Docker, que contiene una serie de mensajes de operación correcta. En lugar de
<container_ID>, introduzca el valor del campoCONTAINERIDque aparece en la salida del paso anterior. PulseCtrl+cpara 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 rotado el secreto.
En este tutorial, el servidor de la API sigue aceptando ambos JWT, pero en un entorno de producción puedes dejar de admitir los JWT más antiguos exigiendo determinados valores para las declaraciones del JWT o comprobando las fechas de caducidad de los JWT.
Ten en cuenta también que, si utilizas un sistema de gestión de secretos que permite actualizar el secreto, el código debe volver a leerlo con frecuencia para obtener los nuevos valores.
Limpieza
Para eliminar los objetos que ha creado en este tutorial:
- Elimina el servicio secretstack_apiclient.
docker service rm secretstack_apiclient- Elimina el secreto.
docker secret rm jot- Abandona el swarm (suponiendo que hayas creado uno solo para este tutorial).
docker swarm leave --force- Detén el contenedor apiserver en ejecución.
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill- Elimina los contenedores que no quieras; primero, enuméralos y, después, elimínalos.
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>- Elimine las imágenes de contenedor que no necesite; para ello, primero enumérelas y, a continuación, elimínelas.
docker image list docker image rm <image_ID>Próximos pasos
Puedes usar este blog para seguir el tutorial en tu propio entorno o probarlo en nuestro laboratorio accesible desde el navegador (regístrate aquí). Para obtener más información sobre cómo exponer servicios de Kubernetes, continúa con las demás actividades de la Unidad 2: Introducción a la gestión de secretos en microservicios.
Para obtener más información sobre la autenticación JWT apta para entornos de producción con NGINX Plus, consulta nuestra documentación y lee Autenticación de clientes de API con JWT y NGINX Plus en nuestro blog.

Sobre el autor

Artículos relacionados del blog
Únete a F5 NGINX en AppWorld 2024
Descubre las numerosas razones para asistir al evento insignia de F5, que incluirá un emocionante anuncio sobre la cartera de productos NGINX.
Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source
La función de diccionario compartido permite configurar NGINX sin necesidad de reiniciarlo al renovar los certificados SSL/TLS.
Exposición de APIs en Kubernetes
La unidad 2 de Microservices March 2022 se centra en cómo exponer tus API en Kubernetes. Aquí tienes enlaces a los recursos que necesitas: la retransmisión en directo del 14 de marzo, artículos de blog y otros recursos de preparación, y el laboratorio práctico «Protege las API de Kubernetes mediante la limitación de solicitudes».
NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto
Hoy, en NGINX Sprint 2.0, anunciamos cómo respaldaremos nuestra visión del futuro de las aplicaciones modernas con una mayor presencia en la comunidad de código abierto, incluida la participación en el proyecto comunitario Kubernetes, nuevos proyectos de software de código abierto y la arquitectura de referencia para aplicaciones modernas.
Presentamos NGINX Plus R22
NGINX Plus R22 incorpora nuevas funcionalidades, como la compatibilidad con OCSP para verificar la revocación de certificados en TLS mutuo, el uso de varios proveedores de identidad OIDC en una configuración, el seguimiento en tiempo real de la limitación de solicitudes y conexiones en el panel de NGINX Plus y mejoras en NGINX JavaScript.
Presentamos NGINX Plus R17
NGINX Plus R17 incorpora compatibilidad con la limitación de velocidad en dos fases y con TLS 1.3, la versión más reciente del protocolo Transport Layer Security. La configuración de OpenID Connect es más sencilla y NGINX ModSecurity WAF es el doble de rápido que antes. También se ha actualizado el módulo JavaScript de NGINX.