Monzo安全团队的目标之一是迈向完全可信的平台。这意味着,理论上,我们可以在平台上运行恶意代码,而不会有任何风险。如果没有安全团队授予特殊的访问权限,这些代码将无法与任何危险的东西交互。
在我们致力于平台研发的过程中,花了一部分时间来研究网络隔离,这意味着我们不希望可以控制Pot上镜像的服务能够与转移资金的服务进行对话。服务应该明确定义并手动批准可以与之通信的列表,其他任何内容都应该被阻止。
当服务比较少的时候,我们可以手动维护这些列表。但是,我们已经有超过1500个服务,所以允许路径的列表非常大,并且在不断变化。实际上,有超过9300个惟一连接(例如,从service.emoji到service.ledger)。
大量的服务和连接使得这个项目非常具有挑战性。我们必须找到一种方法,从代码中生成允许的路径,并用一种便于工程师管理和机器解释的方式存储规则,然后在不造成任何破坏的情况下执行它们。
这是我们在这个项目中详细列出的网络。每个服务都用点表示。每一条线都是一个强制的网络规则,它表示允许两个服务之间进行通信。
下面这幅图是同一幅图,不过删除了一些大众化的节点,并着了色:
如果你对我们为什么决定构建这种系统感兴趣,请查看我们在2016年写的关于构建现代化后台的博文,或者是我们的一位工程师做的关于如何使用微服务的演讲。
在决定隔离所有服务之前,我们决定隔离我们安全级别最高的其中一个服务service.ledger,这是客户余额和所有资金流动的事实来源。我们的一个工程原则是先发布项目,然后迭代,所以最好从小项目开始,特别是对于这样一个复杂的系统。我们首先选择如此重要的服务的原因是,财务团队非常想要锁定这项服务,并且非常愿意在我们的试验中帮助我们。
首先,我们编写了一个名为rpcmap的工具。它读取平台中的所有Go代码,并尝试查找看起来像是在向另一个服务发出请求的代码。在此过程中,工具绘制出这些服务和调用服务之间的连接。虽然并不完美,但它足以构建一个需要调用service.ledger的服务列表。然后我们手动检查这个列表以确保它的准确性。
最终,我们得到了一个简单的服务列表。接下来,我们希望强制要求只有列表中的服务才能向service.ledger发出请求。我们知道,可以使用Kubernetes提供的一个非常简单的特性NetworkPolicy资源来实现这一点(Kubernetes是我们的服务编排平台)。该策略是分类账配置的一部分,只列出了一组允许调用的服务。只允许来自具有正确标签的源的流量,例如,我们允许标记为service.pot的服务。当工程师需要向分类账添加新调用者时,他们将其添加到此列表并重新部署分类账。我们将上述文件存储在与分类账代码相同的地方,这样当你修改它时,财务团队必须对其进行审查。这使他们能够跟踪谁在调用这样一个关键的服务,这对确保我们对服务调用的掌控是很重要的。我们对新代码进行自动检查,提醒工程师在调用分类账时将他们的调用服务加入白名单列表。
对于分类账服务,我们的方法是行之有效的,但我们知道无法将其扩展到所有的服务,有几个几个方面的原因:
因此,我们希望找出测试策略的方法,重新思考我们在哪里定义规则。
我们实际上是使用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”策略来实现,比如:
这些策略只选择已经具有网络策略的内容,这意味着它们只能允许新的流量,并且没有阻塞任何流量的风险。
以上是一种告诉计算机允许哪些路径的很好的方法。但如果我们指望工程师自己更新这些标签,那将是一场噩梦。我们不能要求人们考虑是否需要将服务列入白名单,然后将服务名称转换为标签,然后找到正确的配置文件将那个标签添加进去。相反,我们想要完全自动化这个过程。
有了所有这些控制措施,尽管服务数量庞大,我们还是能够相当迅速地推广。首先,我们为所有服务生成规则文件,并开始强制维护它们以使代码被接受。从那时起,我们有了一个相当精确的服务连接网络。
接下来,我们将所有网络策略部署到我们的测试环境中,这与实际运行Monzo的平台相同。在这个过程中,我们发现并修复了许多rpcmap无法推断某个服务调用了另一个服务的情况,因为这些情况出现了流量丢弃。我们通常通过在代码中添加一个特殊的注释来修复这些情况,该注释告诉rpcmap关于链接的信息:
我们还必须手动覆盖非Go服务(我们没有很多)。在几天内,我们修复了测试环境中的所有丢弃,所以我们改变了试运行策略,现在我们可以丢弃不允许的流量。
现在,我们可以更加确信工程师不会意外地交付没有适当规则文件的代码,因为这样的代码在测试环境中会失败。
接下来,我们将网络策略部署到我们的生产平台上,禁用了丢包和日志记录功能,但是如果有任何违反规则的情况,就会发出警报(这已经是安全方面的一大胜利)。由于潜在的巨大日志量,我们还一直禁用日志记录,直到我们推出了所有允许流量的标签。一旦我们使用calico- accounting证明了日志量很小时,我们就可以通过记录丢弃的流量来发现rpcmap不够聪明的地方。这让我们把丢弃数降为零。
我们决定将丢弃策略关闭一个月,以确保没有任何很少使用的路径是不被允许的。我们仍然可以从对违规的警告中获得很多安全价值,但是当我们完全确定在正常的业务流程中不会丢失任何东西时,我们也会打开丢弃策略。
我们已经从每个服务可以调用其他1500个服务,发展到每个服务平均只能调用其他6个服务,并对每个新的配对进行了审查。解决围绕管理这些规则的人类问题是一个特别有趣的挑战。这就安全性而言是一场令人难以置信的胜利,也是迈向可信任平台的一大步。
英文原文:
We built network isolation for 1,500 services to make Monzo more secure
领取专属 10元无门槛券
私享最新 技术干货