保护云原生应用程序需要正确理解微服务向各种消费者公开的接口(边界)。需要在每个边界上应用适当的工具和机制,以实现适当的安全级别。正确保护运行应用程序的基础架构也非常重要。这包括保护容器映像、安全运行容器运行时以及正确配置和使用容器编排系统 (Kubernetes)。
微服务安全格局
在前微服务时代,大多数应用程序都遵循 MVC 架构。今天,我们将这些称为单体应用程序。与此类应用程序相比,云原生应用程序是高度分布式的,如图 1 所示。
图 1:单体应用与云原生应用
单体应用程序通常有一个入口点。除此之外,除了数据库调用或类似的交互之外,一切都发生在一个进程中。相比之下,云原生应用程序的曝光表面要高得多。如图 1 所示,云原生应用程序通常具有多个通过网络进行通信的组件(服务)。任何给定组件的每个入口点都需要适当保护。
保护应用程序边界
让我们进一步详细说明应用程序边界并了解我们需要担心的实际边界。
识别云原生应用程序中的通信边界
典型的云原生应用程序的后端架构将包含多个业务域。每个业务领域都封装了一组微服务。以零售系统为例;订单处理和库存管理可以是两个拥有自己的微服务集合的业务领域。
业务领域内的微服务将能够无边界地安全地相互通信。这在图 2 中显示为“域内东西向流量”。可以使用双向 TLS 实现该域内的安全通信。可以使用服务网格实现双向 TLS 。一种更轻量级的方法可能是传递由其中一个网关颁发的授权令牌。我们将在本文后面讨论这种方法。
图 2:云原生应用架构
跨业务域的微服务不应该能够自由地相互通信,除非它们被公开为 API 并明确允许通信。
这在图 2 中显示为“域间东西向流量”。该业务域边界的概念在论文“基于单元的架构”中进一步解释。
有一个清晰的边界将所有微服务与客户端应用程序(Web/移动应用程序)分开。这在图 2 中显示为“南北交通”。
API 和 API 网关的使用来保护云原生应用程序
接下来,让我们确定我们定义为 API 和微服务的内容。任何需要暴露在给定边界之外的微服务(或集合)都需要定义为 API。API 通常具有 OpenAPI、GraphQL、AsyncAPI 等规范。API 网关用于跨边界公开 API。API网关的主要任务如下:
- 接受来自呼叫客户端的消息。
- 确保客户端拥有正确级别的身份验证/授权。
- 将消息转发到正确的目标(微服务)。
如图 2 所示,API 网关保护南北通道以及域间东西通道的云原生应用程序。
OAuth2.0 在保护云原生应用程序中的作用
调用 API 的客户端需要先从令牌服务获取 OAuth2.0 访问令牌,然后才能与 API 对话。API 网关在允许访问目标之前验证令牌并确保它是由受信任的机构颁发的。这如图 3 所示。尽管获得对 API 的访问权很常见,但令牌的类型以及获取它们的方式会根据这些令牌的用例而有所不同。
图 3:获取和使用 OAuth2.0 访问令牌的工作流程
OAuth2.0 规范有一个称为授予类型的概念,它定义了获取访问令牌的步骤。Prabath Siriwardena 所著的 Advanced API Security是一本了解 OAuth2.0 概念及其用例的好书。
云原生应用授权
拥有有效的访问令牌是客户端访问 API 的主要要求。访问令牌的主要好处之一是它不仅允许您调用 API,而且还可以指定您可以使用它执行的操作类型。
使用 OAuth2.0 范围进行授权
想象一个产品目录 API 有两个操作:一个用于检索产品列表(GET /product-list),另一个用于修改产品列表(PUT /product-list)。在零售商店应用程序中,所有用户都应该能够检索产品列表,而只有选定的用户应该能够修改产品列表。
在 API 上对此进行建模的标准方法是说产品列表更新操作需要一个特殊的“范围”。除非用于访问此 API 的令牌具有此范围,否则将不允许该请求。OpenAPI规范允许将范围绑定到操作。
一旦客户端知道它需要一个特殊的范围来访问一个操作,它就会请求令牌服务发出一个带有所需范围的令牌。当验证请求用户/客户端被授权获取请求的范围时,令牌服务将相关范围绑定到令牌。此工作流程如图 4 所示。
图 4:使用范围访问 API
我们可以看到身份验证和授权在 API 网关处终止。但是在某些情况下,实际的微服务需要了解用户/客户端访问服务以执行业务逻辑的详细信息。此要求是通过 API 网关发出辅助 JWT 格式令牌(不是访问令牌)并将其转发到目标服务来完成的。这个辅助令牌可以在该域内的微服务中传递,并用于在该域内建立相互信任。
OPA 授权
除了权限之外,我们可能还需要在云原生应用程序中实现其他授权规则。考虑限制对工作日上午 8 点到下午 6 点之间可用的某些应用程序功能的访问。虽然这些可以在微服务的源代码中实现,但这不是一个好的做法。
这些是可以改变的组织策略。最佳实践是将此类策略从代码外部化。
Open Policy Agent (OPA) 是一个轻量级的通用策略引擎,不依赖于微服务。授权规则可以在Rego中实现并挂载到 OPA。
图 5 说明了 OPA 可用于授权规则的模式。
图 5:使用 OPA 进行授权
使用 Docker 保护容器
Docker 是最流行的打包和分发微服务的工具。Docker 容器封装了微服务及其依赖项,并存储在容器注册表(私有或公共)中。
图 6:Docker 构建和推送
外部化应用程序秘密
微服务通常依赖于数据库、第三方 API、其他微服务等。要连接到这些类型的系统,微服务可能依赖于敏感信息(秘密),例如证书和密码。在单体应用程序中,这些类型的信息存储在服务器配置文件中。
只有特权用户才能访问服务器配置文件。但在微服务世界中,开发人员通常将此信息与微服务代码一起存储在属性文件中。当开发人员构建这样的容器并将其推送到容器注册表时,任何可以访问容器映像的人都可以使用此信息!
为了防止这种情况发生,我们需要将应用程序机密从代码中外部化。让我们看一下执行此操作的 Java 程序中的示例 Dockerfile:
FROM openjdk:17-jdk-alpine
ADD builds/sample-java-program.jar \
sample-java-program.jar
ENV CONFIG_FILE=/opt/configs/service.properties
ENTRYPOINT ["java", "-jar", "sample-java-program.jar"]
此 Dockerfile 中的第三行指示 Docker 创建一个名为的环境变量并将CONFIG_FILE其指向该/opt/configs/service.properties位置。与其在源代码中硬编码秘密或从固定文件位置读取代码,不如编写微服务的代码,以便它查找此环境变量的值以确定配置文件位置并将其内容加载到内存中。有了这个,我们成功地避免了代码中的秘密。如果我们用这个文件构建一个 Docker 容器,它不会包含任何敏感信息。接下来,让我们看看如何将我们需要的值外化。
在运行从上述 Dockerfile 构建的 Docker 映像之前,我们需要将其挂载到具有正确值的实际配置文件的位置。这可以通过以下 Dockerrun命令完成:
:\> docker run -p 8090:8090 --mount type=bind, \ source="/hostmachine/configs/service.properties" \
target="/opt/configs/service.properties"
该source部分包含容器主机上文件系统的路径。该target部分包含容器文件系统上的路径。该--mount命令指示 Docker 运行时将源挂载到目标上,这意味着service.properties文件现在可以安全地维护在主机的文件系统上,并在启动容器之前挂载到容器运行时。这样,我们将敏感信息从 Docker 上的微服务本身外部化。
Docker 内容信任
现代软件由许多依赖项组成。软件供应链是从应用程序代码到 CI/CD 一直到生产的软件依赖项的集合。由于恶意软件通过其依赖链进入应用程序运行时,软件供应链攻击非常频繁。
在 Docker 上运行的云原生应用程序依赖于从一个或多个存储库中提取的 Docker 映像。毫无戒心的开发人员可能会依赖恶意 Docker 映像,该映像随后会危及他们的应用程序。为了防止这种情况,Docker 引入了一种称为 Docker Content Trust (DCT) 的机制,该机制允许镜像发布者使用加密密钥对镜像进行签名,并且 Docker 镜像的用户可以在使用前验证镜像。在您的开发和 CI/CD 流程中使用DCT将确保您仅依赖于云原生应用程序中受信任且经过验证的 Docker 映像。
开发人员需要设置一个名为的环境变量DOCKER_CONTENT_TRUST并将其值设置为 1,以在使用 Docker 的所有环境中强制执行 DCT。例如::\> export DOCKER_CONTENT_TRUST=1。设置此环境变量后,它将影响以下 Docker 命令:push、build、create、pull和run。这意味着如果您尝试对docker run未经验证的图像发出命令,您的命令将失败。
docker特权
任何操作系统都有一个称为 root 的超级用户。默认情况下,所有 Docker 容器都以 root 用户身份运行。这不一定是坏事,这要归功于 Linux 内核上的命名空间分区。但是,如果您在容器中使用文件挂载,则获得容器运行时访问权限的攻击者可能非常有害。以 root 访问权限运行容器的另一个问题是,它授予攻击者访问容器运行时的权限,以便将其他工具安装到容器中。这些工具可能会以各种方式损害应用程序,例如扫描开放端口等。
Docker 提供了一种以非特权用户身份运行容器的方法。Linux 中的 root 用户 ID 为 0。Docker 允许我们通过传入用户 ID 和组 ID 来运行 Docker 容器。以下命令将在用户 ID 900 和组 ID 300 下启动 Docker 容器。由于这是一个非 root 用户,因此它可以对容器执行的操作是有限的。
Docker run --name sample-program --user 900:300 nuwan/sample-program:v1
结论
正确保护云原生应用程序并非易事。API 网关和相互信任是确保我们的通信渠道安全且我们拥有零信任架构的关键。OAuth2.0、范围和 OPA(或类似)是确保 API 得到正确身份验证和授权的基础。
超出这个范围,我们还需要关注在 Kubernetes 上使用正确的安全最佳实践、正确处理机密(密码)、保护事件驱动的 API 等等。API、微服务和容器是云原生应用程序的基础。每个开发人员都需要及时了解最新的安全进步和最佳实践。