这篇文章是帮助您将2023 年 3 月微服务中的概念付诸实践的四个教程之一: 开始交付微服务:
您的许多微服务都需要秘密才能安全运行。 机密的示例包括 SSL/TLS 证书的私钥、用于向另一个服务进行身份验证的 API 密钥或用于远程登录的 SSH 密钥。 适当的秘密管理需要严格限制秘密的使用环境,使其仅限于需要的地方,并防止在需要之外访问秘密。 但在应用开发的匆忙中,这种做法常常被忽略。 结果如何? 机密管理不当是信息泄露和漏洞的常见原因。
在本教程中,我们展示了如何安全地分发和使用客户端容器用于访问服务的 JSON Web Token (JWT)。 在本教程的四个挑战中,您将尝试四种不同的机密管理方法,不仅可以学习如何在容器中正确管理机密,还可以了解哪些方法不够充分:
虽然本教程使用 JWT 作为示例机密,但这些技术适用于需要保密的任何容器,例如数据库凭据、SSL 私钥和其他 API 密钥。
本教程利用两个主要软件组件:
GET
请求观看此视频以了解本教程的实际演示。
完成本教程的最简单方法是注册Microservices March并使用提供的基于浏览器的实验室。 这篇文章提供了在您自己的环境中运行本教程的说明。
要在您自己的环境中完成本教程,您需要:
nano
或vim
curl
(大多数系统上已安装)git
(大多数系统上已安装)笔记:
docker
run
命令启动测试服务器时使用‑p
标志为测试服务器设置不同的值。 然后包括 :<端口号>
后缀 本地主机
在 卷曲
命令。~
) 代表您的主目录。在本节中,您将克隆教程 repo 、启动身份验证服务器以及发送带有和不带有令牌的测试请求。
在您的主目录中,创建microservices-march目录并将 GitHub 存储库克隆到其中。 (您也可以使用不同的目录名称并相应地调整说明。) 该 repo 包含配置文件和 API 客户端应用的单独版本,它们使用不同的方法获取机密。
mkdir ~/microservices-marchcd ~/microservices-march
git clone https://github.com/microservices-march/auth.git
显示秘密。 它是一个签名的 JWT,通常用于向服务器验证 API 客户端。
cat ~/microservices-march/auth/apiclient/token1.jwt"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
虽然有几种方法可以使用此令牌进行身份验证,但在本教程中,API 客户端应用程序使用OAuth 2.0 Bearer Token Authorization 框架将其传递给身份验证服务器。 这涉及在 JWT 上添加授权前缀:
持有人
就像这个例子一样:
"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
切换到认证服务器目录:
cd apiserver
为身份验证服务器构建 Docker 映像(注意最后的句点):
docker build -t apiserver .
启动身份验证服务器并确认它正在运行(为了易读,输出分布在多行上):
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
验证身份验证服务器是否拒绝不包含 JWT 的请求,并返回401
需要
授权
:
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>
使用授权
标头提供 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" }
在开始这个挑战之前,让我们明确一点:将秘密硬编码到您的应用程序中是一个糟糕的想法! 您将看到任何有权访问容器映像的人都可以轻松找到并提取硬编码凭据。
在这个挑战中,您将 API 客户端应用程序的代码复制到构建目录中,构建并运行该应用程序,然后提取机密。
apiclient目录的app_versions子目录包含针对四个挑战的简单 API 客户端应用程序的不同版本,每个版本都比前一个版本稍微安全一些(有关更多信息,请参阅教程概述)。
切换到 API 客户端目录:
cd ~/microservices-march/auth/apiclient
将此挑战的应用程序(具有硬编码秘密的应用程序)复制到工作目录:
cp ./app_versions/very_bad_hard_code.py ./app.py
看一下该应用程序:
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)
该代码只是向本地主机发出请求并打印出成功消息或失败代码。
该请求在此行添加授权
标头:
req.add_header("Authorization", authstring)
你还注意到什么了吗? 也许是硬编码的 JWT? 我们马上就讲到这一点。 首先让我们构建并运行该应用程序。
我们将使用docker
compose
命令以及 Docker Compose YAML 文件 - 这让我们更容易理解正在发生的事情。
(请注意,在上一节的第 2 步中,您将针对挑战 1 专用的 API 客户端应用程序的 Python 文件( very_bad_hard_code.py )重命名为app.py 。 您还将在其他三个挑战中做到这一点。 每次使用app.py可以简化物流,因为您不需要更改Dockerfile 。 这确实意味着您需要在docker
compose
命令中包含‑build
参数以每次强制重建容器。)
docker
compose
命令构建容器、启动应用、发出单个 API 请求,然后关闭容器,同时在控制台上显示 API 调用的结果。
这200
输出倒数第二行的成功
代码表示身份验证成功。 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?
事实上,根本不安全。
硬编码凭证会使任何可以访问容器镜像的人都能够检查它们,因为提取容器的文件系统是一项简单的操作。
创建提取目录并切换到该目录:
mkdir extractcd extract
列出容器镜像的基本信息。 --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
将最新的apiclient映像提取为.tar文件。 为了 <容器 ID>
,替换 容器
ID
上面输出中的字段(11b73106fdf8
在本教程中):
docker export -o api.tar <container_ID>
创建api.tar档案需要几秒钟,其中包括容器的整个文件系统。 查找秘密的一种方法是提取整个档案并对其进行解析,但事实证明,有一种捷径可以找到可能有趣的内容——使用docker
history
命令显示容器的历史记录。 (此快捷方式特别方便,因为它也适用于您在 Docker Hub 或其他容器注册表中找到的容器,因此可能没有Dockerfile ,而只有容器映像)。
显示容器的历史记录:
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
输出行是按时间倒序排列的。 它们表明工作目录被设置为/usr/app/src ,然后应用程序的 Python 代码文件被复制并运行。 不需要很厉害的侦探就能推断出这个容器的核心代码库位于/usr/app/src/app.py中,因此这可能是凭证的存放位置。
掌握这些知识后,只需提取该文件:
tar --extract --file=api.tar usr/app/src/app.py
显示文件的内容,就这样,我们就获得了“安全” JWT 的访问权限:
cat usr/app/src/app.py
...
jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
...
如果您完成了 2023 年 3 月微服务的第 1 单元(将十二要素应用应用于微服务架构),那么您就会熟悉使用环境变量将配置数据传递给容器。 如果您错过了,别担心——注册后即可按需获取。
在这个挑战中,您将秘密作为环境变量传递。 与挑战 1中的方法一样,我们不推荐这个方法! 它不像硬编码秘密那么糟糕,但正如你所见,它有一些弱点。
有四种方法可以将环境变量传递给容器:
使用Dockerfile中的ENV
语句进行变量替换(为所有构建的图像设置变量)。 例如:
ENV PORT $PORT
在docker
run
命令上使用‑e
标志。 例如:
docker run -e PASSWORD=123 mycontainer
环境
键。在这个挑战中,您使用环境变量来设置 JWT 并检查容器以查看 JWT 是否被暴露。
改回 API 客户端目录:
cd ~/microservices-march/auth/apiclient
将此挑战的应用程序(使用环境变量的应用程序)复制到工作目录,覆盖挑战 1 中的app.py文件:
cp ./app_versions/medium_environment_variables.py ./app.py
查看应用程序。在输出的相关行中,机密(JWT)被读取为本地容器中的环境变量:
cat app.py...
jwt = ""
if "JWT" in os.environ:
jwt = "Bearer " + os.environ.get("JWT")
...
如上所述,有多种方法可以将环境变量放入容器中。 为了保持一致性,我们坚持使用 Docker Compose。 显示 Docker Compose YAML 文件的内容,该文件使用环境
键设置JWT
环境变量:
cat docker-compose.env.yml---
version: "3.9"
services:
apiclient:
build: .
image: apiclient
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- JWT
运行应用程序而不设置环境变量。 这401
输出倒数第二行的未授权
代码确认身份验证失败,因为 API 客户端应用未传递 JWT:
docker compose -f docker-compose.env.yml up -build...
apiclient-apiclient-1 | 401 Unauthorized
apiclient-apiclient-1 exited with code 0
为了简单起见,在本地设置环境变量。 在本教程的这一点上这样做是可以的,因为这不是现在需要关注的安全问题:
export JWT=`cat token1.jwt`
再次运行容器。 现在测试成功,显示与挑战 1 相同的消息:
docker compose -f docker-compose.env.yml up -build
...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0
所以至少现在基础镜像不包含秘密,我们可以在运行时传递它,这更安全。 但仍然存在一个问题。
显示有关容器映像的信息以获取 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
检查 API 客户端应用的容器。对于 <容器 ID>
,替换 容器
ID
上面输出中的字段(此处 6b20c75830df
)。
docker
inspect
命令允许您检查所有启动的容器,无论它们当前是否正在运行。 这就是问题所在——即使容器没有运行,输出也会在Env
数组中公开 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..."
]
现在您已经了解到,硬编码秘密和使用环境变量并不像您(或您的安全团队)需要的那么安全。
为了提高安全性,您可以尝试使用本地 Docker 机密来存储敏感信息。 再次强调,这不是黄金标准方法,但了解其工作原理还是有好处的。 即使你在生产中不使用 Docker,重要的一点是如何让从容器中提取秘密变得困难。
在 Docker 中,秘密通过文件系统挂载/run/secrets/暴露给容器,其中有一个单独的文件包含每个秘密的值。
在这个挑战中,您将使用 Docker Compose将本地存储的机密传递给容器,然后验证使用此方法时该机密在容器中不可见。
正如您现在所预料的,首先要切换到apiclient目录:
cd ~/microservices-march/auth/apiclient
将本次挑战的应用程序(使用容器内机密的应用程序)复制到工作目录,覆盖挑战 2 中的app.py文件:
cp ./app_versions/better_secrets.py ./app.py
看一下 Python 代码,它从/run/secrets/jot文件读取 JWT 值。 (是的,我们应该检查该文件是否只有一行。 也许在 2024 年 3 月的微服务中?)
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
...
好的,那么我们要如何创造这个秘密呢? 答案在docker-compose.secrets.yml文件中。
查看Docker Compose文件,其中secrets
部分定义了secret文件,然后由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
运行应用程序。由于我们已使 JWT 在容器内可访问,因此身份验证成功并显示现在熟悉的消息:
docker compose -f docker-compose.secrets.yml up -build...
apiclient-apiclient-1 | 200 Success apiKey1
apiclient-apiclient-1 exited with code 0
显示有关容器映像的信息,并注意 API 客户端应用程序的容器 ID(有关示例输出,请参阅挑战 2 中的检查容器中的步骤 1):
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
检查 API 客户端应用的容器。对于 <容器 ID>
,替换 容器
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..."
]
到目前为止一切顺利,但我们的秘密在容器文件系统的/run/secrets/jot中。 也许我们可以使用与挑战 1 中的从容器映像中检索秘密相同的方法从那里提取它。
更改为提取目录(您在挑战 1 期间创建的)并将容器导出到tar存档中:
cd extractdocker export -o api2.tar <container_ID>
在tar文件中查找秘密文件:
tar tvf api2.tar | grep jot-rwxr-xr-x 0 0 0 0 Mon DD hh:mm run/secrets/jot
哦,包含 JWT 的文件可见了。 我们不是说过将秘密嵌入容器中是“安全的”吗? 情况是否和挑战 1 一样糟糕?
让我们看看——从tar文件中提取秘密文件并查看其内容:
tar --extract --file=api2.tar run/secrets/jotcat run/secrets/jot
好消息! cat
命令没有输出,这意味着容器文件系统中的run/secrets/jot文件是空的——里面没有任何秘密可看! 即使我们的容器中有一个秘密工件,Docker 也足够智能,不会在容器中存储任何敏感数据。
也就是说,尽管这种容器配置是安全的,但它有一个缺点。 它取决于运行容器时本地文件系统中是否存在名为token1.jwt的文件。 如果重命名该文件,则重新启动容器的尝试将失败。 (您可以自己尝试重命名 [不是删除!] token1.jwt并再次运行步骤 1 中的docker
compose
命令。)
所以我们已经成功了一半:容器以一种保护秘密不被轻易泄露的方式使用秘密,但秘密在主机上仍然不受保护。 您不希望将秘密以未加密的形式存储在纯文本文件中。 现在是时候引入秘密管理工具了。
机密管理器可帮助您在机密的整个生命周期内管理、检索和轮换机密。 有很多秘密管理器可供选择,它们都实现类似的目的:
您的机密管理选项包括:
为简单起见,本次挑战使用 Docker Swarm,但许多秘密管理器的原理都是相同的。
在这个挑战中,您在 Docker 中创建一个秘密,复制该秘密和 API 客户端代码,部署容器,查看是否可以提取该秘密,然后轮换该秘密。
按照现在的惯例,更改到apiclient目录:
cd ~/microservices-march/auth/apiclient
初始化Docker Swarm:
docker swarm init
Swarm initialized: current node (t0o4eix09qpxf4ma1rrs9omrm) is now a manager.
...
创建一个秘密并将其存储在token1.jwt中:
docker secret create jot ./token1.jwtqe26h73nhb35bak5fr5east27
显示有关秘密的信息。 请注意,秘密值(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": {}
}
}
]
在 API 客户端应用代码中使用 Docker 机密与使用本地创建的机密完全相同 - 您可以从/run/secrets/文件系统读取它。 您需要做的就是更改 Docker Compose YAML 文件中的秘密限定符。
查看 Docker Compose YAML 文件。 注意外部
字段中的值true
,表示我们正在使用 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
因此,我们可以期望这个Compose文件与我们现有的API客户端应用代码一起工作。 嗯,差不多。 虽然 Docker Swarm(或任何其他容器编排平台)带来了很多额外的价值,但它也带来了一些额外的复杂性。
由于docker
compose
不适用于外部机密,我们将不得不使用一些 Docker Swarm 命令,特别是docker
stack
deploy
。 Docker Stack 隐藏了控制台输出,所以我们必须将输出写入日志,然后检查日志。
为了让事情变得简单,我们还使用连续的while
True
循环来保持容器运行。
将此挑战的应用程序(使用秘密管理器的应用程序)复制到工作目录,覆盖挑战 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)
构建容器(在之前的挑战中,Docker Compose 负责了这一点):
docker build -t apiclient .
部署容器:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
Creating network secretstack_default
Creating service secretstack_apiclient
列出正在运行的容器,注意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
显示 Docker 日志文件; <容器 ID>
,替换 容器
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
命令进行检查)。
从挑战 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 允许动态更新机密)。 您也不能删除附加到正在运行的服务的秘密。
列出正在运行的服务:
docker service lsID NAME MODE ...
sl4mvv48vgjz secretstack_apiclient replicated ...
... REPLICAS IMAGE PORTS
... 1/1 apiclient:latest
删除secretstack_apiclient服务。
docker service rm secretstack_apiclient
删除机密并使用新令牌重新创建它:
docker secret rm jot
docker secret create jot ./token2.jwt
重新创建服务:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack
查找apiclient
的容器 ID(有关示例输出,请参阅部署容器并检查日志中的步骤 3):
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
显示 Docker 日志文件,其中显示一系列成功消息。 为了 <容器 ID>
,替换 容器
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。
还要注意,如果您使用允许更新秘密的秘密系统,您的代码需要频繁地重新读取秘密以获取新的秘密值。
要清理您在本教程中创建的对象:
删除secretstack_apiclient服务。
docker service rm secretstack_apiclient
删除机密。
docker secret rm jot
离开群体(假设您仅为了本教程创建了一个群体)。
docker swarm leave --force
终止正在运行的apiserver容器。
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
通过列出然后删除不需要的容器来删除它们。
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"docker rm <container_ID>
通过列出并删除任何不需要的容器映像来删除它们。
docker image list docker image rm <image_ID>
您可以使用此博客在您自己的环境中实现本教程,或者在我们的基于浏览器的实验室中尝试它(在此处注册)。 要了解有关公开 Kubernetes 服务的更多信息,请关注第 2 单元中的其他活动: 微服务秘密管理 101 。
要了解有关使用 NGINX Plus 进行生产级 JWT 身份验证的更多信息,请查看我们的文档并阅读我们博客上的使用 JWT 和 NGINX Plus 对 API 客户端进行身份验证。
“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”