EdgeX 安全组件

写在前面

EdgeX 是 Linux 软件基金会旗下的一个开源的,应用在边缘计算设备上的,针对 IoT 网关的软件系统。它使用 Go 语言实现,基于微服务框架。

作为大厂出品的开源 IoT 边缘计算框架,肯定不会像国内一些小的智能家居公司那样,谈到安全,就是报文加个密了事。EdgeX 的安全,涉及部署环境,安全存储,安全通信,API 权限认证等等方方面面。在 EdgeX Ireland (v2.0.0) 版本上,才最终实现了其完整的安全方案。

如果你想了解 EdgeX 关于安全的一些一手资料,以下是一些有用的链接:

  1. EdgeX 源码库里关于系统安全的简要介绍 SECURITY.md
  2. EdgeX 文档 主页面上关于系统安全的介绍
  3. EdgeX 源码。这是 Edgex 最权威,最完整的系统安全实现,包括了所有的实现细节。当然,也是阅读难度最大的资源。

如果你懒得花这么多时间精力啃上述硬骨头,那么本文的 step-by-step 介绍,可以帮助你简单理解 EdgeX 的安全机制。

读到这,你还没有放弃,说明你真感兴趣,那就让我们就出发吧。

读完本文,你发会现,安全方案简直就是个俄罗斯套娃,一层套一层,最后那个最小的套娃,放在 TPM 芯片里,变成安全的根 (root of security)。

第一层套娃:直接访问

EdgeX 基于微服务架构,每个核心功能,都被设计成一个单独的服务,并且对外提供 REST API 接口服务。假设你在电脑上,直接运行了 EdgeX 的服务,通过 HTTP REST API 可以直接访问其服务:

curl http://localhost:59881/api/v2/ping

几个细节需要说明一下:

  1. 59881 是 core-metadata 服务运行的端口。
  2. 直接运行的意思是,直接编译 EdgeX 源码,并直接在命令行执行 core-metadata 应用程序。如果你是通过 docker compose 运行了启用安全机制的 EdgeX,则 59881 端口是无法直接访问。
  3. 如果你想了解如何编译并运行 EdgeX,可参阅Getting Started - Go Developers
  4. 如果你想了解如何通过 docker compose 运行 EdgeX,可以参阅quick-start

上述细节说明信息量很大,其目的是为了给想进一步了解 EdgeX 的读者提供一些有用的资料。这些内容和本文要讨论的主题无关,不感兴趣的可直接忽略。

总之,这一节,你只要记住一个事实:EdgeX 核心服务的 API 处于“祼奔”状态,可以在不提供任何鉴权凭证的情况下,直接访问。

第二层套娃:API Gateway

这么祼奔肯定不行,EdgeX 干的第一件事情,就是给这些服务套上一层外壳,名叫 API Gateway。简单来讲,要调用这些服务的 REST API,必须把请求发送给 API Gateway,由 API Gateway 对请求进行身份检查,允许访问的,把请求转发给对应的服务,并把结果返回。不允许访问的,直接拒绝访问。

上面是大白话,不想了解技术细节的高级技术经理们都看得懂。下面就是干货了。

EdgeX 使用了 Kong Gateway 来实现其 API Gateway 功能。强烈建议想要阅读 EdgeX 源码,了解其实现细节的读者,至少花 1 小时,熟悉一下 Kong Gateway 的 Getting Started 文档。

如果你不想看 Kong Gateway 一手资料,这里有些二手的,和本文主题相关的功能简单介绍:

  1. Kong Gateway 基于 OpenResty 技术,后者又是通过 lua 扩展的方式,打造了一个 Nginx 开发平台。
  2. Kong Gateway 可以通过 REST API,来配置服务和路由,负载均衡,服务健康检查等。
  3. Kong Gateway 支持 JWT, basic auth 等技术进行安全认证。

那么,EdgeX 如何使用 Kong Gateway 呢?

  1. Kong Gateway 作为一个服务,单独运行在一个 Docker Container 里。
  2. EdgeX 通过 Kong Gateway 提供的 Admin API 来配置它。主要是把 core-metadata, core-command, core-data 等等一系列 EdgeX 的服务配置到 Kong Gateway 里,使得 Kong Gateway 在收到请求时,能够把合法的请求,转发给对应的服务进行处理。这个工作主要由 EdgeX 的 security-proxy-setup 来实现。
  3. Kong Gateway 被配置成了 JWT 认证机制,security-proxy-setup 在调用 Kong Gateway Admin API 时,需要提供 JWT 作为凭证。
  4. 每个被 Kong Gateway 保护起来的 EdgeX 服务,也被配置成 JWT 认证机制。这个是通过调用 secrets-config 来实现的。一个调用 securets-config 的例子,是 edgex-compose 仓库里的 get-api-gateway-token.sh 脚本文件。

上述内容,信息量大,包含很多实现细节。想深入研究的读者,应该可以顺藤摸瓜,找出更多有用的信息。对那些不想了解细节的读者,你只要记住一个事实:至此,EdgeX 核心服务终于套上了一层外壳。访问这些服务时,必须通过 API Gateway,且必须提供一个 JWT 作为凭证。

token=(paste output of `make get-token` here)
curl -k https://{kong-ip}:8443/core-data/api/v2/ping -H "Authorization: Bearer $token"

可是依然有两个安全漏洞没有堵上:

问题一:EdgeX 的官方文档说,类似 core-metadata 这些服务被 Kong Gateway 保护起来后,外部不能直接访问了。这是怎么实现的呀?core-metadata 开放的端口是 59881,我直接向这个端口发请求,不就绕过 Kong Gateway 上的权限检查了么?

答案是,通过 Docker compose 网络隔离实现的。在 EdgeX docker compose 文件中,通过配置 Docker 的虚拟网络,使得这些 EdgeX 服务,只允许 Kong Gateway 访问,不允许 Docker 之外的服务访问。这就是为什么 EdgeX 官网提到,不能直接在生产服务器上运行 EdgeX Native Service,而必须部署在 Docker Container 中运行的原因。

问题二:上述步骤提到,使用 Kong Gateway Admin API 时,需要用到一个 JWT 作为凭证,这个 JWT 要保存在哪里?如果随便保存在一个位置,不怀好意的人拿到了,可以修改 Kong Gateway 的配置,从而绕过 REST API 安全检查。

这个问题的答案就没那么简单了。为了搞清楚这个问题,我们必须再翻开一层俄罗斯套娃。

第三层套娃:安全存储

为了管理这些敏感信息,包括上文提到的 Kong Gateway Admin API 的访问凭证 JWT,以及 redis 数据库用户名密码,系统私钥等等,EdgeX 使用 Vault 来实现安全存储。

Vault 使用 Go 语言实现,即有商业版,也有开源版。它的核心功能包括:

  1. 通过 REST API 存储系统敏感信息。Vault 会把这些敏感信息加密后再保存到磁盘。
  2. 生成动态密钥。比如,应用程序要访问 AWS 的某个服务,可以让 Vault 生成一个动态的访问凭证,这个凭证还有时效性,到期自动作废。
  3. 数据加密。Vault 可以把接受输入数据进行加密,把密文输出给其他应用程序,如数据库,进行存储。
  4. 密钥刷新。管理的密钥到期可以自动刷新。
  5. 密钥吊销。手动吊销不想让它继续生效的密钥。

强烈建议花 1 小时阅读一下 Valut Quick Start 文档。特别是 Deploy Vault 章节的内容,它是 Vault 安全的根。

简单的理解,Vault 是一个保险箱,里面放了很多敏感信息。而 Deploy Vault 这一章节,则告诉我们如何生成这个保险箱的钥匙。在 Vault 初始化时,Vault 会返回 5 个密钥和一个 Root Token,如下所示:

Unseal Key 1: 4jYbl2CBIv6SpkKj6Hos9iD32k5RfGkLzlosrrq/JgOm
Unseal Key 2: B05G1DRtfYckFV5BbdBvXq0wkK5HFqB9g2jcDmNfTQiS
Unseal Key 3: Arig0N9rN9ezkTRo7qTB7gsIZDaonOcc53EHo83F5chA
Unseal Key 4: 0cZE0C/gEk3YHaKjIWxhyyfs8REhqkRW/CSXTnmTilv+
Unseal Key 5: fYhZOseRgzxmJCmIqUdxEm9C3jB5Q27AowER9w4FC2Ck

Initial Root Token: s.KkNJYWF5g0pomcCLEmDdOVCW

其中,Root Token 是访问 Vault 数据的凭证。假设你在 Vault 里保存了一个密钥,想要取回这个密钥时,可以通过 Vault 提供的 REST API 接口取回,前提是必须提供 Root Token 作为访问凭证。

5 个 Unseal Key 是解封密钥。Vault 启动时,默认处于锁定状态,此时无法读取存储在里面的敏感信息。默认配置下,必须使用这 5 个 Unseal Key 中的 3 个来解封,才能读取敏感信息。Vault 描述的应用场景是,这 5 个 Unseal Key 分别给 5 个不同的人保存,在系统服务启动的时候,必须由这 5 个人中的 3 个分别输入这些 Key,才能解锁。

当然,在 EdgeX 中,不存在让 5 个人分开保存 Unseal Key 的需求和场景。EdgeX 直接把生成的 Unseal Key 保存到文件系统中。在系统启动时,重新从文件系统中读回,调用 Vault REST API 解封。

EdgeX 不会把 Root Token 保存到文件系统中,而是每次启动时,都通过 Unseal Key 重新生成新的 Root Token。每次退出时,吊销 Root Token。

细心的读者应该发现,这里又有一个大坑:Vault 的 Unseal Key 这么重要的数据,相当于金库大门钥匙,竟然直接保存在文件系统中了?

你说对了,EdgeX 确实是这样实现的,默认情况下,通过 Docker Compose 启动带安全组件的 EdgeX 时,就会在容器的 /vault/config/assets/resp-init.json 目录中找到这个金库大门钥匙,大概长这样:

$ docker exec -it edgex-security-secretstore-setup cat /vault/config/assets/resp-init.json | python3 -mjson.tool
{
    "keys": [
        "bef7e153db320c5f5d25bbc032c424be91417379bdd3c013320d662ede922ecd36",
        "5711476a2553fa94606c45e676924a62538d458574f05682b86aadc0972f98a9e3",
        "ac454c7068644c15634e6a8d2fb575d1ae5437fadaace8e7cc937e9affbe602a6e",
        "7f8214cf1438e590628a2f1929baf444f25273d6371eff6ff13ad16b95332c4cf0",
        "1e93df0e720c6a9e184f37717cbaa82679278b45d774046ff7e0045054c280faaf"
    ],
    "keys_base64": [
        "vvfhU9syDF9dJbvAMsQkvpFBc3m908ATMg1mLt6SLs02",
        "VxFHaiVT+pRgbEXmdpJKYlONRYV08FaCuGqtwJcvmKnj",
        "rEVMcGhkTBVjTmqNL7V10a5UN/rarOjnzJN+mv++YCpu",
        "f4IUzxQ45ZBiii8ZKbr0RPJSc9Y3Hv9v8TrRa5UzLEzw",
        "HpPfDnIMap4YTzdxfLqoJnkni0XXdARv9+AEUFTCgPqv"
    ]
}

而且,你在启动 EdgeX 时,也会看到下面这条 Log:

$ docker logs edgex-security-secretstore-setup | grep master
level=INFO ts=2021-08-15T09:18:07.7356078Z app=security-secretstore-setup source=init.go:134 msg="vault master key encryption not enabled. IKM_HOOK not set."

Vault 的初始化以及上述 Vault Unseal Key 的处理,都在 EdgeX security-secretstore-setup 里实现。对实现细节感兴趣的读者,可以进一步阅读源码。

读到这,你可能还是有点困惑。不是开玩笑吧?好不容易整了个 Vault 这个保险箱来管理敏感数据,最后却把保险箱的钥匙直接挂在了门口?猜得出来,EdgeX 肯定不会这么大意。要揭开这个谜底,我们需要再剥开一层套娃。

第四层套娃:TPM 安全存储

实际上,EdgeX 提供了 IKM_HOOK 这个环境变量,来实现加密保存 Vault Unseal Key。IKM_HOOK 环境变量指向一个可执行文件,当其不为空时,EdgeX 会执行这个命令,读取这个命令在 stdout 的输出,并以这个输出(hex 编码)作为生成密钥的原材料,通过选定的 KDF 算法为每个 Vault Unseal Key 生成密钥,使用 AES-GCM 256 算法分别加密存储。

加密后的数据还是放在 /vault/config/assets/resp-init.json,大概长这样:

{
    "encrypted_keys": [
        "3137e26dec05af30e42a2d4b61349dca7f2e4d441aaf10ebd59a6a1a38f36f01babe3055f2d2775e1402cc970bfb34fb72",
        "3a886b5028e3256f46e4a95898d6af9cad923e0c3d4f24db47858e6df7a49d1e2fb13cd3e3713cc66f0ea2963e29e3efd6",
        "e880b1f14f7692ed771d3d820f227386efcf39b3431feb40464d67b9acf1e2eddb68725df31e343cd7db299465984d3b10",
        "e94f76857ae688a40313a82756342b5354f56a6ed9e10ba5cdcf12cf31b290e9dcb1c0cb08c8977c944081ee75ebdcb686",
        "bb8d3222dd1b81dca5c8dba686e9115dfde561409f118ae5d2a0bc904444a30635af8abdb8fdfaa580ccae110b3091c472"
    ],
    "nonces": [
        "e4f8579c214f7190b120046d",
        "ed9bb86dfd62c75d5ebf396b",
        "f51542e01da8ad10a3190b20",
        "3adff146c3221b96250e6007",
        "dca51bef25cf77fa97282fc4"
    ]
}

要使用时,再次通过 IKM_HOOK 生成密钥原材料(跟上次生成的肯定要一样,否则就无法解密了),通过相同的 KDF 算法,算出密钥(重新算出来的密钥,和加密时算出来的密钥是一样的)。使用这个密钥进行解密,得到 Vault Unseal Key。

至此,安全的核心问题转化到了 IKM_HOOK 这个环境变量指向的可执行程序。实际上,在实际应用中,它应该指向 TPM 的一个应用程序,它使用 TPM 生成密钥原材料。Intel 开发者论坛上,有个示例教程,演示如何通过 TPM 来制作一个可以给 EdgeX 使用的 IKM_HOOK 可执行程序,最终用来生成经由 TPM 芯片保护的密钥。

套娃终于翻完了,最小的那个套娃就是 IKM_HOOK 环境变量以及它所指向的,由 TPM 芯片保护的密钥生成程序。这就是整个 EdgeX 框架的安全根(security of root)。

(完)


Post by Joey Huang under edge on 2021-08-14(Saturday) 23:20. Tags: edgex, security,


Powered by Pelican and Zurb Foundation. Theme by Kenton Hamaluik.