Monzo 安全团队的目标之一是迈向完全可信的平台。这意味着,理论上,我们可以在平台上运行恶意代码,而不会有任何风险。如果没有安全团队授予特殊的访问权限,这些代码将无法与任何危险的东西交互。
在我们致力于平台研发的过程中,花了一部分时间来研究网络隔离,这意味着我们不希望可以控制 Pot 上镜像的服务能够与转移资金的服务进行对话。服务应该明确定义并手动批准可以与之通信的列表,其他任何内容都应该被阻止。
当服务比较少的时候,我们可以手动维护这些列表。但是,我们已经有超过 1500 个服务,所以允许路径的列表非常大,并且在不断变化。实际上,有超过 9300 个惟一连接(例如,从 service.emoji 到 service.ledger)。
服务和连接数量太多
大量的服务和连接使得这个项目非常具有挑战性。我们必须找到一种方法,从代码中生成允许的路径,并用一种便于工程师管理和机器解释的方式存储规则,然后在不造成任何破坏的情况下执行它们。
这是我们在这个项目中详细列出的网络。每个服务都用点表示。每一条线都是一个强制的网络规则,它表示允许两个服务之间进行通信。
下面这幅图是同一幅图,不过删除了一些大众化的节点,并着了色:
如果你对我们为什么决定构建这种系统感兴趣,请查看我们在 2016 年写的关于构建现代化后台的博文,或者是我们的一位工程师做的关于如何使用微服务的演讲。
隔离一个服务
在决定隔离所有服务之前,我们决定隔离我们安全级别最高的其中一个服务 service.ledger,这是客户余额和所有资金流动的事实来源。我们的一个工程原则是先发布项目,然后迭代,所以最好从小项目开始,特别是对于这样一个复杂的系统。我们首先选择如此重要的服务的原因是,财务团队非常想要锁定这项服务,并且非常愿意在我们的试验中帮助我们。
用工具分析代码
首先,我们编写了一个名为 rpcmap 的工具。它读取平台中的所有 Go 代码,并尝试查找看起来像是在向另一个服务发出请求的代码。在此过程中,工具绘制出这些服务和调用服务之间的连接。虽然并不完美,但它足以构建一个需要调用 service.ledger 的服务列表。然后我们手动检查这个列表以确保它的准确性。
最终,我们得到了一个简单的服务列表。接下来,我们希望强制要求只有列表中的服务才能向 service.ledger 发出请求。我们知道,可以使用 Kubernetes 提供的一个非常简单的特性 NetworkPolicy 资源来实现这一点(Kubernetes 是我们的服务编排平台)。该策略是分类账配置的一部分,只列出了一组允许调用的服务。只允许来自具有正确标签的源的流量,例如,我们允许标记为 service.pot 的服务。当工程师需要向分类账添加新调用者时,他们将其添加到此列表并重新部署分类账。我们将上述文件存储在与分类账代码相同的地方,这样当你修改它时,财务团队必须对其进行审查。这使他们能够跟踪谁在调用这样一个关键的服务,这对确保我们对服务调用的掌控是很重要的。我们对新代码进行自动检查,提醒工程师在调用分类账时将他们的调用服务加入白名单列表。
问题
对于分类账服务,我们的方法是行之有效的,但我们知道无法将其扩展到所有的服务,有几个几个方面的原因:
在推出这些策略之前,我们没有一个安全的方法来测试,而且检查我们生成的策略需要大量的手动工作。我们需要能够推出一种策略,明确地知道它是否能减少流量,但不是实际地减少流量。这将使我们自动生成策略,而不是手动检查它们,然后只需等待几天,看看它们是否正确。
虽然这种方法对于分类账服务行之有效,但团队并不总是想要审查每一个服务的新调用者。因此,将允许的服务列表作为接收服务的属性并不理想。
工程师们必须编辑 Kubernetes 的配置文件才能做出这样一份白名单。这通常是他们不习惯做的事情,因此很容易出错。
回滚服务变得非常危险。如果你返回到服务的早期版本以纠正代码更改,你也可能会回滚到服务调用者被加入白名单之前,从而突然阻塞其流量。这突出了这种方法的一个基本问题。service.emoji 调用 service.ledger 是 emoji 的一种属性,应该这样部署。但在上面我们使它成为 ledger 的一个属性。白名单的服务成为共享状态,可能会不同步,难以维护。我们希望遵循单一责任原则。
因此,我们希望找出测试策略的方法,重新思考我们在哪里定义规则。
测试策略
我们实际上是使用Calico来实现 Kubernetes 的网络策略的。Calico 是一个网络软件,它可以让我们的服务互相通信。我们与 Calico 社区讨论了测试网络策略的问题,发现我们可以使用 Calico 的一些特性来测试我们的策略,而这些特性是 Kubernetes 无法访问的。
当我们将网络策略应用到服务时,我们决定给它一个标签。然后,我们使用一种 Calico 网络策略,它有一些 Kubernetes 没有提供给我们的额外特性,让我们可以声明以下内容:
进入服务的流量上是否有网络策略;
该流量不在其策略允许范围内;
记录流量,然后允许该流量。
该策略就像下面这个样子:
本质上,由于处于高阶域,这个策略会在 Kubernetes 网络策略之后运行。因此,如果流量到达了这个策略,它一定没有被 Kubernetes 的策略所允许。
有能力只选择已经具有策略的服务至关重要。否则,像上面这样的一个预演策略将在没有自己的策略的情况下捕获服务的所有流量,而且我们不知道,如果没有预演策略,流量会降低多少。
观察网络
有了这个预演策略,流量不会有任何丢弃。而且,在流量丢弃时,我们还会得到一个日志项。我们使用一个名为kube-iptables-tailer的工具可以很方便地查看这些日志,这样我们就可以创建一些图表,显示哪些服务的流量正在丢弃以及从哪里丢弃。
我们有点担心日志记录过多(来自会丢弃流量负载的不正确的策略)会在我们的平台上产生问题。所以我们决定,只有在我们证明了这个几率很低的情况下,才会启用日志功能。为此,我们编写了一个新工具calico- accounting,它能够计算给定服务将丢弃多少包,而不需要实际记录它们。这个计数对于查看哪些服务具有糟糕的策略非常有用。但是在某些情况下,我们仍然需要日志,因为日志还显示了“被丢弃”流量的源服务。
即使我们开始实施网络策略以丢弃不允许的流量,我们仍然保持日志功能,这样我们就可以快速地提醒工程师有流量被丢弃,无论是来自 Bug 还是攻击者。
重新设计表示服务连接的方法
我们考虑了很多假设,从 service.emoji 到 service.ledger 的流量应该表示为 emoji 的属性,并且想出了不少点子。本质上,我们希望每个服务以某种方式指定它需要与其他哪些服务进行通信,然后进行汇总,从而明确 service.ledger 的入站服务。
一开始,我们想为每个链接写一个网络策略(emoji→ledger)。但是大量的策略会导致严重的性能问题。然后我们意识到,我们可以简单地给服务贴上它们需要的标签。如果你需要和 service.ledger 通信,你可以贴个标签 monzo.com/egress-s-ledger。然后,service.ledger 的网络策略可能是这样的:
这是一个突破性进展!现在,我们可以为所有服务编写直观、一致的策略,并且有一个非常简单的方法来表示服务调用者的目的地服务。
编写服务策略
我们知道,我们还必须覆盖那些需要与大量服务进行对话的服务。例如,我们的监控服务几乎可以与任何东西对话。我们不希望监控服务针对平台中的每个服务都有一个标签,因为这是一个庞大且不断变化的列表。
我们可以在每个服务的策略中添加一些东西来允许监视。但是,要添加调用所有内容的新服务,我们必须使用新策略部署所有服务。我们决定添加另一个标签 service-type,对可以调用它们的服务进行松散的分组。
例如,如果你有后端服务的类型标签,你可以被一组服务调用。如果你有 api 标签,你的调用组会稍微不同。我们通过一些“catch-all”策略来实现,比如:
这些策略只选择已经具有网络策略的内容,这意味着它们只能允许新的流量,并且没有阻塞任何流量的风险。
管理规则
以上是一种告诉计算机允许哪些路径的很好的方法。但如果我们指望工程师自己更新这些标签,那将是一场噩梦。我们不能要求人们考虑是否需要将服务列入白名单,然后将服务名称转换为标签,然后找到正确的配置文件将那个标签添加进去。相反,我们想要完全自动化这个过程。
首先,我们更新了 rpcmap,这样,如果我们在一个服务上运行它,它将扫描它所调用的任何内容,并生成“规则文件”。每个被调用的服务都有一个简单的文件,它表示 A 调用 B 的事实。service.emoji 会有一个文件 service.emoji/egress/service.ledger.rule。
我们将 rpcmap 设置为在每个人的代码上运行,只要他们将代码推送到 GitHub。这可以提醒工程师保持规则的更新。它还在接受代码之前检查规则文件。当然,规则文件也需要人工查看。
如果一个团队想要跟踪关键服务的调用者,他们可以指示 GitHub 要求他们对/service.ledger.rule 进行审核。因此,即使规则文件位于另一个团队的服务中,也会要求他们查看规则文件。
接下来,我们更新了部署管道,将上面的规则文件转换为服务标签。我们能够保证目标服务的所有者可查看对服务的调用,并使服务之间的连接非常容易理解。这也使得找出什么服务调用分类账变得非常容易——你只需查找名为 service.ledger.rule 的文件。在我们的平台上强制执行规则的好处是,它为我们提供了可证明的信息——我们知道分类账的调用者名单是详尽无遗的。
推广到 1500 个微服务
有了所有这些控制措施,尽管服务数量庞大,我们还是能够相当迅速地推广。首先,我们为所有服务生成规则文件,并开始强制维护它们以使代码被接受。从那时起,我们有了一个相当精确的服务连接网络。
接下来,我们将所有网络策略部署到我们的测试环境中,这与实际运行 Monzo 的平台相同。在这个过程中,我们发现并修复了许多 rpcmap 无法推断某个服务调用了另一个服务的情况,因为这些情况出现了流量丢弃。我们通常通过在代码中添加一个特殊的注释来修复这些情况,该注释告诉 rpcmap 关于链接的信息:
我们还必须手动覆盖非 Go 服务(我们没有很多)。在几天内,我们修复了测试环境中的所有丢弃,所以我们改变了试运行策略,现在我们可以丢弃不允许的流量。
现在,我们可以更加确信工程师不会意外地交付没有适当规则文件的代码,因为这样的代码在测试环境中会失败。
接下来,我们将网络策略部署到我们的生产平台上,禁用了丢包和日志记录功能,但是如果有任何违反规则的情况,就会发出警报(这已经是安全方面的一大胜利)。由于潜在的巨大日志量,我们还一直禁用日志记录,直到我们推出了所有允许流量的标签。一旦我们使用 calico- accounting 证明了日志量很小时,我们就可以通过记录丢弃的流量来发现 rpcmap 不够聪明的地方。这让我们把丢弃数降为零。
我们决定将丢弃策略关闭一个月,以确保没有任何很少使用的路径是不被允许的。我们仍然可以从对违规的警告中获得很多安全价值,但是当我们完全确定在正常的业务流程中不会丢失任何东西时,我们也会打开丢弃策略。
只调用其他六个服务
我们已经从每个服务可以调用其他 1500 个服务,发展到每个服务平均只能调用其他 6 个服务,并对每个新的配对进行了审查。解决围绕管理这些规则的人类问题是一个特别有趣的挑战。这就安全性而言是一场令人难以置信的胜利,也是迈向可信任平台的一大步。
英文原文:
We built network isolation for 1,500 services to make Monzo more secure
评论