系统设计中的安全考量
作者:Jason Taylor
发布日期:2021年5月20日
阅读时间:12分钟
我作为一名开发者和安全专业人士,在学习系统设计时是从这个角度出发的。虽然我熟悉许多相关概念,但我决定深入且认真地学习。现在,我确信每位开发者和安全专业人士都应该理解这些概念。对于像我一样想了解更多的人,这里有一个概述,帮助你们从应用安全的心态思考系统设计。
如果你是一名刚接触系统设计的开发者,这将向你介绍这个话题,同时悄悄加入一些应用安全概念。如果你是一名安全专业人士,这将作为一个框架和心理模型,帮助你思考系统设计,使你在帮助开发团队构建现代、高度可扩展、云优先系统时更有效。
你不需要阅读以下所有部分。相反,选择最适用于你的系统的部分,开始与其他工程师和架构师进行有意义的关于安全的对话。
现代系统设计全是关于性能、一致性和可靠性的扩展。
当你需要理解现有系统或设计新系统时,从一组澄清问题开始:
- 用例是什么?
- 未来一年系统预计有多少用户?
- 系统预计处理和持久化多少数据?
- 是否有关于事务、延迟、内存或数据存储的约束?
- 安全要求和客户期望是什么?
- 需要保护的资产是什么?
- 过去在这个系统或类似系统上看到过哪些攻击?
这将让你理解系统的形状——你学到的其他一切都融入这些问题的答案中。
接下来深入关键特性:
- 每个功能的预期使用情况?
- 涉及哪些角色,授权如何处理?
- 这如何转化为每秒请求数?
- 这如何转化为数据存储?
- 读取与写入的比例?
一些功能将表示为UI,一些作为API。在有API的地方,理解最重要的API定义。例如:
postComment(userID, comment_text, user_location, timestamp)
你可能立即有一些问题或担忧,关于可能出错的地方。看上面的API,我想知道:
- userID是如何生成、认证和保护的?
- comment_text是否以任何方式验证?
- comment_text在下游如何使用?例如,它是否会回显到HTML,是否需要在使用前编码?
- 考虑到潜在的隐私问题,user_location是否必要?如果是,数据如何使用和保护?
因此,即使只有一个API定义,你也可以开始深入细节并开始浮现安全担忧。任何入口点都是开始安全讨论的可能方式,这可以带来成果。
接下来,查看高级架构图很有用。一些系统已经有这个,但许多没有。如果不存在,创建它。与架构师一起构建图表比要求一个并等待,希望他们会完成更容易。
图表不必复杂就有用。只需足够了解发生了什么,理解信任边界,并开始看到输入源。
此时询问权衡非常有用。每个系统都有。必须做出哪些困难决策,利弊是什么。有时这有文档记录,但通常在某人的头脑中。尝试理解这一点,因为它会让你了解系统优化了什么,什么不那么重要。它还可能从安全角度给你一些弱点的提示。
从系统设计角度看,以下问题很有用:
- 是否有瓶颈?例如,大量读取请求和少量写入可能表示需要主/从复制。或者一组用户生成异常流量,可能表示需要缓存最常用的数据。
- 是否有单点故障?
- 如果数据库宕机会发生什么?
- 如果Web服务器宕机会发生什么?
- 如果事务数量急剧增加会发生什么?
- 负载均衡器在架构中的位置?
- 负载均衡器如何均匀分布?
- 如果负载均衡器宕机会发生什么?
- 请求节流发生在哪里?
- 如果受到攻击,能否禁止特定IP、块或区域?
下一层要考虑的是数据库。
- 关键表是什么,它们持有哪些信息?这可以深入了解系统中重要的主要对象,例如:
- User: UserID, Name, Email, DoB, CreationData, LastLogin
- Comment: CommentID, Content, CommentLocation, NumberOfReads, TimeStamp
- 数据库是NoSQL还是SQL?
这将告诉你数据模型是基于复杂的关系策略还是专注于快速访问的非规范化策略。
虽然这不总是真的,因为SQL数据库可以与非规范化数据一起使用,而NoSQL数据库可以将连接推入代码。但如果这些情况中的任何一个是真的,值得理解为什么做出这些决策。
- 数据如何索引;你期望每个表有什么性能特征?
- 你是否有缓存或物化视图可能存储敏感数据?
最后,是时候深入围绕可扩展性做出的决策了。这确实是系统设计的核心,值得理解每个策略:
- 通过使用更强大的服务器垂直扩展。
- 通过使用更多服务器水平扩展。
- 使用负载均衡在冗余服务器之间分散负载。这可以在每一层完成。
- 使用复制扩展读取,同时添加冗余。
- 分区或分片数据库以水平扩展读取和写入,但这增加了复杂性并减少了冗余。
- 内存缓存可用于提高将重用数据的性能。关键问题是:你想缓存哪些数据,缓存应该多大,数据如何从缓存中过期?例如:
- 可以缓存查询返回的对象(例如,哈希是记录索引)。
- 可以缓存实际查询结果(例如,哈希是查询字符串)。
- 可以缓存索引到db以使查找更快。
- 如果缓存被破坏或损坏会发生什么?
异步操作应用于慢任务,如将动态数据预处理为静态数据,或服务长用户请求。这不一定会减少处理时间,但会增加用户对系统响应性的感知。这些仍然需要与其他同步操作相同的认证、授权、会话和数据安全标准保护。
还有许多其他关键概念与系统设计相关,理解这些对于掌握现代架构很重要。最重要的收集如下。
系统设计概念
异步
预处理
提前完成工作,并在用户请求时准备好资源。例如,将动态内容转换为静态内容——定期将页面渲染为静态文件并本地存储以供服务。
作业/消息队列
将耗时的作业发送到作业队列,然后告诉用户正在处理,稍后准备好。在工作完成时,用户不会被阻止以其他方式使用站点。
缓存
基于文件
预处理动态内容到静态内容并服务。缺点是布局更改更困难,因为你需要重新处理所有内容。例如,Craigslist这样做。
基于内存
查询缓存(例如MySQL)
查询及其结果被缓存,以便下次执行该查询时,响应来自内存。查询是键/值对中的键。很难知道数据何时在DB中更新但未在缓存中。
缓存对象
将检索的db数据存储为类对象,然后放入缓存。使根据需要或数据更改使整个对象过期更容易。对象可以从DB在多线程上组装以进一步提高性能。
Memcached
键值存储,仅字符串。查询缓存,如果数据在那里将被检索,否则将从db拉入缓存并检索。当满时,旧缓存数据FIFO移除,因此最旧的是垃圾收集。垂直扩展良好,因为它是多线程的。
Redis
键值存储,各种数据类型。更多过期选项。非易失性,也存储在磁盘上。水平扩展良好,因为它是单线程的。
你也可以在代码中创建自己的缓存。
缓存的好东西: 用户会话;完全渲染的页面;活动流;用户之间的关系。
容器
容器是类似VM的OS虚拟化块。它们可用于运行微服务。可以在任何基础设施、计算机、云上运行。可以使用CloudFormation、Kubernetes或类似工具组装成更完整的系统。运行应用程序所需的一切(除了服务器上的共享操作系统)都打包在容器对象内:代码、运行时、系统工具、库和依赖项。
示例
- Docker
- AWS Fargate
- Google Kubernetes - 与Docker一起使用以大规模管理容器
- Amazon ECS
哈希
哈希对于缓存对象以快速查找很有用。由于哈希对象可能导致冲突,需要一种处理机制。你可以检测并生成另一个哈希,或者如果这变成性能瓶颈,通过预生成哈希创建密钥库并按需使用。哈希也用于保护密码,尽管在这种情况下必须使用加密安全哈希。不要加密密码,因为那增加了密码被 uncovered 的风险。
负载均衡
选项
使用DNS条目,在这种情况下请求将由DNS服务器轮询路由。通常也用于在负载均衡器集之间平衡。
这可能导致不平衡,因为会话将坚持每个服务器(DNS缓存),并且一些会话将需要更多工作来服务。
在必要服务器前使用负载均衡器并使用轮询策略。
可能由于与上述相同的原因导致不平衡。现在你需要管理会话,因为否则请求将命中负载均衡器后的不同服务器。为了解决这个问题,你可以:
- 在负载均衡器层或负载均衡器本身上管理会话在数据库服务器上。
- 或者负载均衡器可以跟踪每个客户端首先命中哪个服务器(cookie中的sessionID)和该服务器上的未来请求(类似于DNS缓存)。
负载均衡器可以查询服务器负载并选择最不忙的。
这可能变得复杂,因为它需要负载均衡器和服务器之间的查询API以及做出正确选择的逻辑。
每个资源类型有一个服务器(图像、视频、静态内容、动态内容)。
这可能导致不平衡。它无助于冗余。因此,你可能仍然需要负载均衡在负载过重的服务器上。
负载均衡也可以作为服务(例如Amazon Elastic Load Balancer)或硬件(例如Citrix或F5)完成。
微服务
微服务提供将应用程序分解为可独立部署的服务(团队、语言、工具等)的自由。
微服务的好处:
- 小团队开发者可以比大团队更灵活地工作。
- 如果部分宕机,应用程序仍然功能,因为微服务允许旋转替换。
- 当微服务只需要扩展必要组件时,满足需求更容易。
- 微服务的各个组件可以更容易地适应持续交付管道。
微服务与其他动态和可扩展技术(如Kubernetes和无服务器技术)配合良好。然而,这种异质性可能使安全更复杂。
解决方案是:
- 可追踪。使查看所有组件部分容易。将安全集成到开发者的工作流中。尽可能自动化。
- 持续可见。不依赖调查、电子表格、仪表板。基于依赖、互联网暴露、涉及资产自动化服务风险排名。专注于最高风险服务。
- 隔离。减少攻击面。减少移动敏感数据的需要(例如令牌保险库)。
无状态微服务仅处理请求和服务响应。有状态微服务需要存储运行,因为它们维护状态。
示例技术:
- RESTful APIs通过HTTP(S)通信
- Redis用于数据存储。单线程,因此你避免锁。
- Prometheus用于监控。
- RabbitMQ用于消息/任务队列。
- AWS Lambda用于按需无基础设施运行微服务。
NoSQL
不需要规范化数据。提高性能,因为:
- 可以根据业务需求优化读取、写入或数据一致性。
- 缺乏连接,尽管连接可能仍然需要在代码中完成。
当缺乏关系或强制执行非规范化策略时,NoSQL可能有意义。SQL可能有意义,因为它广泛使用、成熟、有清晰的扩展范式(分片、主/从、复制),并被FB、Twitter、Google使用。在索引查找上(无连接)它可以与NoSQL一样快。
分区
除非你需要,否则不要分区/分片,因为它增加了显著复杂性。如果你的工作集太大无法放入内存或你的数据库无法跟上写入量,则分片。
类型:
垂直
DB按功能分区(用户配置文件、消息等)。或按区域(例如早期Facebook年代的Harvard vs MIT)。
基于键
可以基于简单键,如用户名字母顺序,但分区可能不均匀。基于哈希是分配N服务器然后将数据放在mod(key, n)服务器上。如果你添加服务器,数据将需要重新分配,这是昂贵的。
基于目录
创建查找表以找到数据位置。服务器可以轻松添加,但查找可能是瓶颈和单点故障。
如果存在热DB分区问题怎么办?更改键以进行一致哈希,以便负载更均匀分布。
分区的缺点:
- 跨服务器连接太慢——必须在代码中连接。
- 跨服务器数据完整性可能变得棘手——必须在代码中强制执行。
- 重新平衡难以无停机完成。
分区的优点:
- 高可用性——如果一个盒子宕机,其他仍然操作(尽管有部分数据)。
- 更快查询。
- 更多写入带宽。
- 每个服务器上的小数据集有助于缓存。
- 更多平衡和优化选项。
- 不需要复制。
复制和克隆
应用层服务器克隆
使用模板确保每个服务器在代码库和配置上相同。会话存储在另一个服务器上的DB或缓存中。
DB层主/从复制
提供冗余和更快读取时间。对于每个主,有一组从数据复制到。读取可以在从中负载均衡。写入转到主然后复制到从。如果你进一步扩展到需要多个主来处理写入量,那么写入也需要复制到其余主,增加复杂性。使用多个主也提高冗余,在主宕机的情况下。
示例参考架构
用户从互联网来并命中负载均衡器。LB路由到Web服务器,同时更新cookie以指定该用户将来应该去哪个服务器。在下一个请求上,负载均衡器使用此cookie数据路由到同一服务器,以便维护状态。在Web服务器后面是一组负载均衡器用于将读取路由到DB从,另一组负载均衡器用于将写入路由到主。写入触发复制到每个其他主然后向下到每个他们的从。为了额外冗余,扩展到多个数据中心以减少电源故障或建筑故障的风险。在DNS级别执行负载均衡以在数据中心之间平衡。防火墙策略:
- 使用防火墙锁定端口。例如仅80/443进入负载均衡器。
- 然后负载均衡器可以终止SSL连接并传递到端口80到Web服务器。
- 仅在他们需要的端口(例如3306)与DB服务器通信。
- 这导致在每个负载均衡器层前放置防火墙:在服务层前,在数据层前。