NGINX教程:如何安全管理容器中的密钥

NGINX | 2023年3月14日

本文是四篇教程之一,帮助您将以下内容中的概念付诸实践:2023 年 3 月微服务专题:开始交付微服务:

许多微服务需要使用密钥才能安全运行。密钥包括SSL/TLS证书的私钥、用于向其他服务进行身份验证的API密钥,或用于远程登录的SSH密钥。妥善管理密钥需要严格限制密钥的使用场景,确保仅在必要位置使用,并防止在非必要情况下访问密钥。但在匆忙开发应用时,这一做法往往会被忽视。结果如何?密钥管理不当是导致信息泄露和漏洞利用的常见原因。

教程概述

本教程将介绍如何安全地分发和使用客户端容器访问服务所需的JSON Web Token(JWT)。通过教程中的4项挑战,您将尝试4种不同的密钥管理方法,不仅学习如何在容器中正确管理密钥,还将了解哪些方法并不适用:

虽然本教程使用JWT作为机密信息示例,但这些技术适用于容器中需要保密的任何信息,例如数据库凭据、SSL私钥和其他API密钥。

本教程使用两个主要软件组件:

  • API服务器:一个运行NGINX Open Source和一些基本NGINX JavaScript代码的容器。该代码从JWT中提取声明,并返回其中一项声明的值;如果不存在声明,则返回错误消息
  • API客户端:一个运行非常简单的Python代码的容器,该代码仅向API服务器发起GET请求

观看此视频,了解本教程的实际操作演示。

完成本教程最简单的方法是注册参加“Microservices March”,并使用所提供的浏览器实验环境。本文介绍如何在您自己的环境中运行本教程。

先决条件和设置

先决条件

要在自己的环境中完成本教程,需要:

  • 与 Linux/Unix 兼容的环境
  • 基本熟悉Linux命令行
  • 文本编辑器,例如nano或vim
  • Docker(包括Docker Compose和Docker Engine Swarm)
  • curl(大多数系统已安装)
  • git(大多数系统已预装)

注意事项:

  • 本教程使用一个侦听端口 80 的测试服务器。如果端口 80 已被占用,请在使用 ‑p 标志通过 docker run 命令启动测试服务器时,为其设置其他端口值。然后,在 curl 命令中的 localhost 后添加 :<port_number> 后缀。
  • 本教程省略了Linux命令行提示符,以便将命令复制并粘贴到终端中。波浪号(~)表示主目录。

设置

在本节中,您将克隆教程代码仓库、启动身份验证服务器,并发送测试请求,分别测试携带和不携带令牌的情况。

克隆教程代码仓库

  1. 在主目录中创建microservices-march目录,并将GitHub存储库克隆到该目录中。(也可以使用其他目录名称,并相应调整说明。)该存储库包含配置文件,以及采用不同方法获取密钥的多个API客户端应用版本。
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/auth.git
  1. 显示该密钥。它是一个已签名的JWT,通常用于API客户端向服务器进行身份验证。
cat ~/microservices-march/auth/apiclient/token1.jwt"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"

虽然可通过多种方式使用此令牌进行身份验证,但在本教程中,API客户端应用使用OAuth 2.0不记名令牌授权框架将其传递给身份验证服务器。具体做法是在JWT前添加Authorization: Bearer,如下例所示:

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

构建并启动身份验证服务器

  1. 切换到身份验证服务器目录:
cd apiserver
  1. 为身份验证服务器构建Docker镜像(请注意末尾的句点):
docker build -t apiserver .
  1. 启动身份验证服务器,并确认其正在运行(为便于阅读,输出分为多行显示):
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

测试身份验证服务器

  1. 验证身份验证服务器是否会拒绝不包含JWT的请求,并返回401 Authorization Required:
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>
  1. 使用Authorization标头提供JWT。200 OK返回代码表示API客户端应用已成功通过身份验证。
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" }

挑战 1:在应用中硬编码密钥(千万别这么做!)

在开始这项挑战之前,需要明确一点:将密钥硬编码到应用中是非常糟糕的做法!您将看到,任何有权访问容器镜像的人都能轻易找到并提取硬编码的凭据。

在此挑战中,您需要将API客户端应用的代码复制到构建目录,构建并运行应用,然后提取密钥。

复制API客户端应用

The app_versions 子目录位于 apiclient 目录中,其中包含用于四项挑战的不同版本的简单API客户端应用,每个版本都比前一版本略为安全(有关更多信息,请参阅“教程概述”)。

  1. 切换到API客户端目录:
cd ~/microservices-march/auth/apiclient
  1. 将本挑战所用的应用(即包含硬编码密钥的应用)复制到工作目录:
cp ./app_versions/very_bad_hard_code.py ./app.py
  1. 查看应用:
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)
  1. 该代码只是向本地主机发出请求,并输出成功消息或失败代码。请求在以下行添加Authorization标头:
req.add_header("Authorization", authstring)
  1. 还注意到其他问题了吗?比如硬编码的JWT?稍后我们会讲到这一点。先构建并运行应用。

构建并运行API客户端应用

我们将docker compose命令与Docker Compose YAML文件结合使用,这样可以更轻松地了解具体情况。

(请注意,在上一节的第 2 步中,您已将挑战 1 专用的 API 客户端应用的 Python 文件(very_bad_hard_code.py)重命名为app.py。在其他三个挑战中也需要这样做。每次都使用app.py可以简化操作,因为无需更改Dockerfile。但这意味着每次都需要在‑build参数中使用docker compose命令,以强制重新构建容器。)

The docker compose 命令会构建容器、启动应用程序、发出一次API请求,然后关闭容器,同时在控制台上显示API调用结果。

输出倒数第二行的200 Success代码表示身份验证成功。apiKey1值进一步证实了这一点,因为它表明身份验证服务器能够解码JWT中同名的声明:

docker compose -f docker-compose.hardcode.yml up -build
...
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0

因此,硬编码凭据在我们的API客户端应用中正常生效,这并不意外。但这样安全吗?或许是安全的,因为容器只运行一次此脚本便会退出,而且没有shell?

事实上并非如此,这种做法完全不安全。

从容器镜像中获取密钥

对凭据进行硬编码会使任何能够访问容器镜像的人都可以查看这些凭据,因为提取容器文件系统轻而易举。

  1. 创建extract目录并切换到该目录:
mkdir extractcd extract
  1. 列出容器镜像的基本信息。使用--format标志可提高输出的可读性(出于同样的原因,此处将输出分为两行显示):
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
  1. 将最新的apiclient镜像提取为.tar文件。对于<container_ID>,请替换为上述输出中CONTAINER ID字段的值(本教程中为11b73106fdf8):
docker export -o api.tar <container_ID>
  1. 创建api.tar归档文件需要几秒钟,其中包含容器的整个文件系统。查找密钥的一种方法是提取并解析整个归档文件,但其实有一种快捷方式可以找到可能值得关注的内容,即使用docker history命令显示容器的历史记录。(这种快捷方式尤其方便,因为它也适用于从Docker Hub或其他容器注册表中找到的容器;对于这类容器,可能只有容器镜像,而没有Dockerfile。)
  2. 显示容器的历史记录:
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
  1. 输出内容按时间倒序排列。其中显示,工作目录已设置为/usr/app/src,随后复制并运行了应用的Python代码文件。不难推断,此容器的核心代码库位于/usr/app/src/app.py,因此凭据很可能也位于此处。
  2. 了解这些信息后,只提取该文件:
tar --extract --file=api.tar usr/app/src/app.py
  1. 显示该文件的内容。就这样,我们获得了对“安全”JWT的访问权限:
cat usr/app/src/app.py
...
jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
...

挑战 2:将密钥作为环境变量传递(还是不行!)

如果您已完成 Microservices March 2023 的第 1 单元(将十二要素应用方法应用于微服务架构),应该已经熟悉如何使用环境变量向容器传递配置数据。如果错过了也不用担心,注册后可点播观看。

在本挑战中,您将机密作为环境变量传递。与挑战 1中的方法一样,我们不建议使用这种方法!虽然它不像硬编码机密那么糟糕,但您将看到,它也存在一些弱点。

可通过以下四种方式将环境变量传递给容器:

  • 在ENV语句中使用Dockerfile进行变量替换(为构建的所有镜像设置该变量)。例如:
ENV PORT $PORT
  • 在‑e docker run命令中使用该标志。例如:
docker run -e PASSWORD=123 mycontainer
  • 在Docker Compose YAML文件中使用environment键。
  • 使用包含这些变量的​.env文件。

在此挑战中,您将使用环境变量设置JWT,并检查容器以确认JWT是否暴露。

传递环境变量

  1. 切换回API客户端目录:
cd ~/microservices-march/auth/apiclient
  1. 将本挑战中使用环境变量的应用复制到工作目录,并覆盖挑战 1 中的“app.py”文件:
cp ./app_versions/medium_environment_variables.py ./app.py
  1. 查看该应用。在相关的输出行中,密钥(JWT)作为本地容器中的环境变量读取:
cat app.py...
jwt = ""
if "JWT" in os.environ:
    jwt = "Bearer " + os.environ.get("JWT")
...
  1. 如上所述,可通过多种方式将环境变量传入容器。为保持一致,我们继续使用Docker Compose。显示Docker Compose YAML文件的内容,该文件使用environment键设置JWT环境变量:
cat docker-compose.env.yml---
version: "3.9"
services:
  apiclient:
    build: .
    image: apiclient
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - JWT
  1. 在不设置环境变量的情况下运行应用。输出倒数第二行中的401 Unauthorized代码表明身份验证失败,原因是API客户端应用未传递JWT:
docker compose -f docker-compose.env.yml up -build...
apiclient-apiclient-1  | 401 Unauthorized
apiclient-apiclient-1 exited with code 0
  1. 为简化操作,请在本地设置环境变量。在本教程的当前阶段,这样做没有问题,因为这并非目前关注的安全问题:
export JWT=`cat token1.jwt`
  1. 再次运行容器。现在测试成功,并显示与挑战 1 中相同的消息:
docker compose -f docker-compose.env.yml up -build
... 
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0

这样一来,至少基础镜像不再包含密钥,并且可以在运行时传入密钥,因此更加安全。但仍然存在一个问题。

检查容器

  1. 显示容器镜像信息,以获取API客户端应用的容器ID(为便于阅读,输出分为两行显示):
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
  1. 检查API客户端应用的容器。对于“<container_ID>”,请将其替换为上述输出中“CONTAINER ID”字段的值(此处为6b20c75830df)。使用“docker inspect”命令可以检查所有已启动的容器,无论它们当前是否正在运行。问题也正在于此:即使容器未运行,输出仍会暴露Env数组中的JWT,而该JWT并未安全地保存在容器配置中。
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..."
]

挑战 3:使用本地密钥

现在您已经了解到,硬编码密钥和使用环境变量的安全性无法满足您(或安全团队)的要求。

为提高安全性,可以尝试使用本地Docker secrets存储敏感信息。再次说明,这并非理想方法,但了解其工作原理仍有帮助。即使生产环境中不使用Docker,关键也在于了解如何提高从容器中提取secret的难度。

在Docker中,密钥通过文件系统挂载点向容器公开:/run/secrets/。其中,每个密钥的值分别存储在单独的文件中。

在此挑战中,您将使用 Docker Compose 将本地存储的密钥传递给容器,然后验证使用此方法时密钥不可见。

将本地存储的密钥传递给容器

  1. 和前面的操作一样,首先切换到apiclient目录:
cd ~/microservices-march/auth/apiclient
  1. 将本挑战所用的应用(即从容器内使用密钥的应用)复制到工作目录,并覆盖挑战 2 中的app.py文件:
cp ./app_versions/better_secrets.py ./app.py
  1. 查看这段Python代码,它会从/run/secrets/jot文件中读取JWT值。(没错,我们或许还应该检查该文件是否只有一行。也许会在2024年3月的Microservices March中介绍?)
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
...
  1. 那么,如何创建这个密钥呢?答案就在 docker-compose.secrets.yml 文件中。
  2. 查看Docker Compose文件,其中密钥文件在secrets部分中定义,然后由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

验证密钥在容器中不可见

  1. 运行应用。由于我们已使容器能够访问JWT,身份验证成功,并显示现在已经熟悉的消息:
docker compose -f docker-compose.secrets.yml up -build...
apiclient-apiclient-1  | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0
  1. 显示容器镜像的相关信息,并记下API客户端应用的容器ID(有关示例输出,请参阅挑战2中“检查容器”的步骤1):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  1. 检查API客户端应用的容器。对于<container_ID>,请替换为上一步输出中CONTAINER ID字段的值。与检查容器第2步的输出不同,Env部分的开头没有JWT=行:
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..."
]
  1. 目前一切顺利,但我们的密钥位于容器文件系统的/run/secrets/jot中。或许可以使用挑战 1 中从容器镜像中检索密钥所采用的相同方法,从该位置提取密钥。
  2. 切换到extract目录(在挑战 1 中创建),然后将容器导出为tar归档文件:
cd extractdocker export -o api2.tar <container_ID>
  1. 在tar文件中查找密钥文件:
tar tvf api2.tar | grep jot-rwxr-xr-x  0 0      0           0 Mon DD hh:mm run/secrets/jot
  1. 糟糕,包含JWT的文件竟然可见。我们不是说过,将机密信息嵌入容器是“安全”的吗?情况是否和挑战 1 中一样糟糕?
  2. 来看看。请从tar文件中提取密钥文件,并查看其内容:
tar --extract --file=api2.tar run/secrets/jotcat run/secrets/jot
  1. 好消息!cat命令没有输出,这意味着容器文件系统中的run/secrets/jot文件为空,里面没有任何密钥!即使容器中存在密钥构件,Docker也不会在容器中存储任何敏感数据。

不过,尽管此容器配置是安全的,但仍有一个不足之处。运行容器时,它依赖本地文件系统中名为token1.jwt的文件。如果重命名该文件,容器将无法重启。(可以自行尝试:重命名[不要删除!]token1.jwt,然后再次运行步骤 1 中的docker compose命令。)

目前只完成了一半:容器使用密钥的方式可降低密钥轻易泄露的风险,但主机上的密钥仍未受到保护。不要将密钥以未加密的形式存储在纯文本文件中。现在该引入密钥管理工具了。

挑战 4:使用密钥管理器

密钥管理器可帮助您在密钥的整个生命周期内对其进行管理、检索和轮换。市面上有多种密钥管理器可供选择,它们的用途大致相同:

  • 安全存储密钥
  • 控制访问权限
  • 在运行时分发
  • 启用密钥轮换

密钥管理选项包括:

为简化操作,本挑战使用Docker Swarm,但这些原则同样适用于许多密钥管理器。

在此挑战中,您将在 Docker 中创建密钥,复制密钥和 API 客户端代码,部署容器,尝试提取密钥,然后轮换密钥。

配置Docker Secret

  1. 按照惯例,切换到apiclient目录:
cd ~/microservices-march/auth/apiclient
  1. 初始化 Docker Swarm:
docker swarm init
Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
...
  1. 创建一个密钥,并将其存储在token1.jwt中:
docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27
  1. 显示有关该密钥的信息。请注意,密钥值(JWT)本身不会显示:
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": {}
    }
  }
]

使用Docker Secret

在API客户端应用程序代码中使用Docker secret,与使用本地创建的secret完全相同。可以从/run/secrets/文件系统中读取。只需更改Docker Compose YAML文件中的secret限定符即可。

  1. 查看Docker Compose YAML文件。请注意,true是external字段的值,表示我们使用的是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
  1. 因此,可以预期此Compose文件能够与现有的API客户端应用程序代码配合使用。不过,还差一点。Docker Swarm(或任何其他容器编排平台)虽然能带来许多额外价值,但也会增加一些复杂性。由于docker compose不支持外部密钥,因此必须使用一些Docker Swarm命令,尤其是docker stack deploy。Docker Stack会隐藏控制台输出,因此必须将输出写入日志,然后查看日志。为简化操作,我们还使用一个持续运行的while True循环,使容器保持运行。
  2. 将本挑战中使用密钥管理器的应用复制到工作目录,并覆盖挑战3中的app.py文件。查看app.py的内容,可以看到该代码与挑战3中的代码几乎完全相同。唯一的区别是增加了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)

部署容器并检查日志

  1. 构建容器(在之前的挑战中,此操作由 Docker Compose 完成):
docker build -t apiclient .
  1. 部署容器:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
Creating network secretstack_default
Creating service secretstack_apiclient
  1. 列出正在运行的容器,并记下secretstack_apiclient的容器ID(与之前一样,为便于阅读,输出分为多行显示)。
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
  1. 显示Docker日志文件。对于“<container_ID>”,请将其替换为上一步输出中“CONTAINER ID”字段的值(此处为20d0c83a8b86)。日志文件会显示一系列成功消息,因为我们在应用程序代码中添加了“while True”循环。按Ctrl+c退出命令。
docker logs -f <container_ID>200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
200 Success apiKey1
...
^c

尝试访问密钥

我们知道当前未设置任何敏感环境变量(但您随时可以按照挑战 2 中“检查容器”的第 2 步,使用 docker inspect 命令进行检查)。检查容器 in Challenge 2).

从挑战3中,我们还知道/run/secrets/jot文件为空,但可以通过以下方式检查:

cd extractdocker export -o api3.tar  tar --extract --file=api3.tar run/secrets/jot cat run/secrets/jot

成功!无法从容器中获取该密钥,也无法直接从Docker密钥中读取。

轮换密钥

当然,拥有适当权限后,我们可以创建一项服务,并将其配置为把密钥写入日志,或将密钥设置为环境变量。此外,您可能已经注意到,我们的API客户端与服务器之间的通信未加密(明文传输)。

因此,几乎任何密钥管理系统都仍有可能发生密钥泄露。定期轮换(替换)密钥是降低由此造成损害的一种方法。

使用Docker Swarm时,只能先删除再重新创建密钥(Kubernetes允许动态更新密钥)。此外,也无法删除已附加到正在运行的服务的密钥。

  1. 列出正在运行的服务:
docker service lsID             NAME                    MODE         ... 
sl4mvv48vgjz   secretstack_apiclient   replicated   ... 


    ... REPLICAS   IMAGE              PORTS
    ... 1/1        apiclient:latest
  1. 删除secretstack_apiclient服务。
docker service rm secretstack_apiclient
  1. 删除该密钥,然后使用新令牌重新创建:
docker secret rm jot docker secret create jot ./token2.jwt
  1. 重新创建服务:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
  1. 查找apiclient的容器ID(有关示例输出,请参阅“部署容器并检查日志”中的第3步):
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
  1. 显示Docker日志文件,其中会显示一系列成功消息。对于“<container_ID>”,请替换为上一步输出中“CONTAINER ID”字段的值。按Ctrl+c退出命令。
docker logs -f <container_ID>200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
200 Success apiKey2
...
^c

看到从“apiKey1”变为“apiKey2”了吗?您已轮换密钥。

在本教程中,API服务器仍接受这两个JWT。但在生产环境中,可以通过要求JWT中的声明具有特定值,或检查JWT的到期日期,来停用较旧的JWT。

另请注意,如果使用的密钥系统允许更新密钥,代码需要频繁重新读取密钥,以获取新的密钥值。

清理

要清理在本教程中创建的对象,请执行以下操作:

  1. 删除secretstack_apiclient服务。
docker service rm secretstack_apiclient
  1. 删除密钥。
docker secret rm jot
  1. 退出该 swarm(假设您专门为本教程创建了一个 swarm)。
docker swarm leave --force
  1. 终止正在运行的apiserver容器。
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
  1. 先列出不需要的容器,然后将其删除。
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>
  1. 列出并删除所有不需要的容器镜像。
docker image list docker image rm <image_ID>

后续步骤

可以参考本博客,在自己的环境中完成本教程,也可以在我们的浏览器实验室中试用(在此注册)。如需进一步了解如何公开Kubernetes服务,请继续完成第2单元:微服务密钥管理入门中的其他活动。

要详细了解如何使用NGINX Plus实现生产级JWT身份验证,请参阅我们的文档,并阅读博客文章《使用JWT和NGINX Plus对API客户端进行身份验证》。

横幅上写着“2023年3月微服务活动:免费报名,立即注册”


分享
标签: Security, F5 NGINX, Tech

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。