读书笔记《developing-microservices-with-node-js》安全性和可追溯性
安全是当今系统中最大的担忧之一。 大公司泄露的信息量令人担忧,尤其是因为 90% 的信息泄露可以通过软件开发人员非常小的行动来弥补。事件记录和错误可追溯性也会发生类似的情况。在有人请求您没有的日志以审核失败之前,没有人会真正关注太多。在本章中,我们将在以下主题的帮助下讨论如何管理安全性和日志记录,以使我们的系统安全且可追溯:
可追溯性:在微服务中必须能够跟踪我们系统周围的请求 架构。我们将把这项任务用到 Seneca,并学习如何从这个奇妙的框架中获取信息。
审计:即使是虽然我们尽最大努力构建软件,意外发生。重建调用序列并准确查看发生了什么的能力很重要。我们将讨论如何启用我们的系统以便能够恢复所需的信息。
基础设施安全性通常被软件工程师忽略,因为它与他们的领域完全不同的专业知识。然而,如今,尤其是如果你的职业倾向于 DevOps,这是一个不容忽视的主题。
在本书中,我们不会深入探讨基础设施的安全性,而只是通过一些经验法则来确保您的微服务安全。
强烈建议读者阅读和了解密码学以及围绕使用 SSH 的所有含义,SSH 是 当今保持通信安全的主要资源。
在任何组织中,都有一份可以访问某些服务的人员的严格列表。通常,这些服务的身份验证是通过用户名和密码完成的,但也可以使用密钥来完成验证用户身份 .
无论使用何种身份验证方法,都应始终通过安全通道(例如 SSH)进行通信。
SSH 代表 Secure Shell,它是一个用于访问远程机器中的 shell,但它也可以是一个非常有用的工具来创建代理和隧道来访问远程服务器。
让我们使用以下命令解释它是如何工作的:
在这种情况下,我使用 Vagrant 来促进虚拟机的构建。 Vagrant 是一个非常流行的工具,用于自动化开发环境及其网站(https://www.vagrantup.com/) 包含有用的信息。
在第一行,我们执行 ssh [email protected]
命令。此命令尝试在 192.168.0.1
主机中以用户 david
的身份打开终端。
由于这是第一次对 IP 192.168.0.1
中的机器执行此命令,因此我们的计算机将不信任远程服务器。
这是通过在 /home/david/.ssh/known_hosts
文件夹下维护一个名为 known_hosts
的文件来完成的(在这种情况下)(这将取决于用户)。
此文件是具有相应密钥的主机列表。如您所见,以下两行说明 主机不可信任 并显示远程服务器持有的密钥的指纹,以便验证它:
此时,用户应该通过检查密钥来验证服务器的身份。完成后,我们可以指示 SSH 连接到服务器,这将导致打印以下日志:
现在,如果我们检查我们的 known_hosts
文件,我们可以看到密钥已添加到列表中, 如下:
known_hosts
文件中存储的这个密钥是远程服务器的公钥。
SSH 使用一种称为 的 密码算法 RSA。该算法围绕非对称加密的概念构建,如图所示在下图中:
非对称密码学依赖于一组密钥:一个公共密钥和一个私有密钥。顾名思义,公钥可以与所有人共享;而私钥必须保密。
用私钥加密的消息只能用公钥解密,反之几乎是不可能的(除非有人拿到另一半密钥)来拦截和解密消息。
此时,我们的计算机知道服务器的公钥,我们可以开始与服务器的加密会话。一旦我们得到终端,所有命令和这些命令的结果都将被加密并通过网络发送。
此密钥也可用于连接到没有密码的远程服务器。我们唯一需要做的就是在我们的机器中生成一个 SSH 密钥,并将其安装在服务器中的 .ssh 下名为
文件夹,authorized_keys
的文件中known_hosts
文件所在的位置。
在使用微服务时,您可以远程登录到相当多的不同机器上,这样这种方法就变得更有吸引力了。但是,我们需要非常小心处理私钥的方式,因为如果用户泄露该私钥,我们的基础设施可能会受到损害。
应用程序安全性变得越来越重要。随着云正在成为大公司基础设施的事实标准,我们不能依赖数据被限制在单个数据中心的事实。
通常,当有人开始一项新业务时,主要关注点是从功能的角度构建产品。安全性不是主要关注点,通常会被忽视。
这是一种非常危险的做法,我们将通过让读者了解可能危及我们的应用程序的主要安全威胁来对其进行修改。
以安全方式开发应用程序的主要四大安全点如下:
注射
跨站脚本
跨站请求伪造令牌保护
打开重定向
在本节结束时,我们将能够识别主要漏洞,但我们不会对恶意攻击者进行防御。一般来说,软件工程师应该与最新的安全技术保持同步。无论您构建的产品多么好,如果它不安全,就会有人发现并利用它。
正如我们之前所说,安全性是应用程序开发中的一个持续主题。无论您正在构建什么类型的软件,它总会存在安全隐患。
在我的职业生涯中,我发现在不成为全职安全工程师的情况下了解 Web 开发安全的最佳方法是遵循 OWASP 项目。 OWASP 代表 Open Web Application Security Project,他们产生了相当一个有趣的 文档(除其他外),每年称为 OWASP Top 10。
在上一节中,我们确定了软件开发人员可能面临的四个主要安全问题,所有这些问题都将在以下各节中提及。
Injection 是迄今为止我们可能面临的最危险的攻击。具体来说,SQL 注入是影响应用程序的最常见的注入形式,它包括攻击者在我们的一个应用程序查询中强制执行 SQL 代码,导致到可能损害我们公司数据的不同查询。
还有其他类型的注入,但我们将重点关注 SQL 注入,因为现代世界中几乎每个应用程序都使用关系数据库。
SQL 注入包括在我们的应用程序中通过来自未经验证的源(例如 Web 表单或具有任意文本输入的任何其他数据源)的输入在我们的应用程序中注入或操作 SQL 查询。
让我们考虑以下示例:
此查询将为我们提供与给定名称和密码对应的用户。为了从客户端的输入构建查询,我们可以考虑执行类似于以下代码的操作作为一个好主意:
乍一看,它看起来像一个简单的程序,它访问名为 test_db
的数据库并发出查询以检查是否存在与用户名和密码匹配的用户并将其返回给client 这样如果我们打开浏览器并尝试浏览到 http://localhost:3000/login? username=david&password=mypassword
URL,浏览器将使用以下查询的结果呈现一个 JSON 对象:
还没有什么奇怪的,但是如果客户试图入侵我们会发生什么?
看看下面的输入:
http://localhost:3000/login?username=' OR 1=1 --&password=mypassword
可以看到,它生成的查询是如下代码:
在 SQL 中,--
字符序列用于注释该行的其余部分,以便有效查询如下:
此查询返回完整的用户列表,如果我们的软件正在使用此查询的结果来解决用户是否应该登录,那么我们遇到了一些严重的问题。我们刚刚向甚至不知道有效用户名的人授予了对我们系统的访问权限。
这是关于 SQL 注入如何影响我们的众多示例之一。
在这种情况下,很明显我们正在将不受信任的数据(来自用户)连接到我们的查询中,但是相信我,当软件变得更加复杂时,识别起来并不总是那么容易。
应用程序主要通过表单与用户交互。这些表单通常包含可能导致攻击的自由文本输入字段。
防止损坏的数据进入我们的服务器的最简单方法是通过输入验证,正如 名称所暗示的那样,它包括验证用户的输入以避免前面描述的情况。
有两种类型的输入验证,如下所示:
白名单
黑名单
黑名单是一种危险技术。在大多数情况下,尝试定义输入中不正确的内容比简单地定义我们期望的内容要付出更多的努力。
推荐的方法是(并且将永远是)白名单来自用户的数据,通过 使用正则表达式:我们知道电话号码的样子,我们也知道用户名的样子,等等。
输入验证并不总是那么容易。如果您曾经遇到过电子邮件验证,您就会知道我在说什么:验证电子邮件的正则表达式绝非简单。
验证某些数据没有简单的方法这一事实不应限制我们这样做,因为省略输入验证可能会导致严重的安全漏洞。
输入验证不是 SQL 注入的灵丹妙药,但它也有助于解决其他安全威胁,例如跨站点脚本。
在上一节的查询中,我们做了一件非常危险的事情:将用户输入连接到我们的查询中。
一种解决方案可能是使用某种转义库来清理用户的输入,如下所示:
在这种情况下,使用的 mysql
库提供了一套方法来转义字符串。让我们看看它是如何工作的:
前面的小脚本转义了前面示例中作为 username
提供的字符串,结果是 \' OR 1=1 --
。
跨站脚本,也称为XSS,是一种主要影响Web应用程序的安全漏洞。这是最常见的安全问题之一,对客户的影响可能很大,因为有人可能会通过这种攻击窃取用户身份。
该攻击是一种注入第三方网站的代码,可以从客户端的浏览器中窃取数据。有几种方法可以做到这一点,但到目前为止,最常见的是来自客户端的非转义输入。
在 Internet 上的少数网站中,用户可以添加包含任意输入的评论。这种任意输入可以包含从远程服务器加载 JavaScript 的脚本标签,这些脚本可以窃取会话 cookie(或其他类型的有价值信息),让攻击者在远程计算机上复制用户会话。
XSS 攻击主要有两种 类型:persistent 和 非持久性。
persistent 类型的 XSS 包括通过 制作一个特定的文本字符串,一旦在网站上显示给用户,就会解析为攻击。此代码可以通过存储在数据库中的任意输入文本(例如论坛中的评论)注入。
non-persistent 类型的 XSS 是当攻击插入到由于糟糕的数据处理,应用程序的非持久部分。
如您所见,我们在 http://www 中搜索了一本书(本书) .amazon.co.uk/。它不会产生任何输出(因为该书尚未出版),但它指定 您的搜索“microservices nodejs”不匹配任何产品 ,它以某种方式使用来自网络浏览器的输入作为输出。更重要的是,当我点击搜索时,亚马逊将我重定向到以下 URL:
http://www.amazon.co.uk/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords=microservices+nodejs
我们知道 Amazon 是安全的,但如果它对 XSS 攻击敏感,我们可以修改 field-keywords
参数的值来制作一个在内容,导致攻击者能够窃取会话 cookie,这可能会导致网站出现一些 严重问题。
跨站请求伪造 (CSRF) 与跨站点请求脚本。在跨站点请求 脚本中,问题在于客户端信任来自服务器的数据。对于跨站点请求伪造,问题在于服务器信任来自客户端的数据。
窃取会话cookie后,攻击者不仅可以窃取用户的信息,还可以修改与cookie关联的账户信息。
这是通过通过 HTTP 请求将数据发布到服务器来完成的。
HTTP 将其请求分类为方法。方法基本上是用来指定请求要携带的操作是什么。最有趣的四种方法如下:
GET
:从服务器获取数据。它不应修改任何持久数据。POST
:这会在服务器中创建一个资源。PUT
:这会更新服务器中的资源。DELETE
:这会从服务器中删除一个资源。
还有更多方法(例如PATCH
或CONNECT
),但让我们关注这四个。如您所见,这四种方法中的三种会修改来自服务器的数据,具有有效会话的用户可能会窃取数据、创建付款、订购商品等。
避免跨站请求伪造攻击的一种方法是保护 POST
、PUT
和 DELETE
带有跨站点请求令牌的端点。
看看下面的 HTML 表单:
此表格描述了一种完全有效的情况:用户在我们的网站上注册;非常简单,但仍然有效且有缺陷。
我们指定 一个 URL 和预期参数列表,以便攻击者可以在几分钟内注册数百或数千个帐户,使用一个小脚本发出带有两个参数(email
和 password
)的 POST
请求身体。
现在,看下面的表格:
您可以看到不同之处:有一个额外的隐藏参数,称为 csrftoken
。
该参数是每次呈现表单时生成的随机字符串,以便我们可以将此额外参数添加到每个表单。
提交表单后,将验证 csrftoken
参数以仅让具有有效令牌的请求通过并生成要再次在页面上呈现的新令牌。
有时,我们的 应用程序可能需要将用户重定向到某个 URL。例如,当在没有有效身份验证的情况下点击私有 URL 时,用户通常会被重定向到登录页面:
http://www.mysite.com/my-private-page
这可能会导致重定向到以下内容:
http://www.mysite.com/login?redirect=/my-private-page
这听起来是合法的。用户被发送到登录页面,一旦他提供了一组有效的凭据,它就会被重定向到 /my-private-page
。
如果有人试图窃取我们用户的帐户会发生什么?
查看以下请求:
http://www.mysite.com/login?redirect=http://myslte.com
这是一个精心设计的请求,会将用户重定向到 myslte.com
而不是 mysite.com
(注意 l
而不是 i
)。
有人可以让 myslte.com
看起来像 mysite.com
的登录页面,并通过分发前面的内容窃取您的用户密码和用户名社交媒体中的 URL,因为用户将被重定向到恶意页面。
上述问题的解决方案很简单:不要将用户重定向到不受信任的第三方网站。
同样,执行此类任务的最佳方法是将目标主机列入白名单以进行重定向。基本上,我们不会让我们的软件将我们的客户重定向到未知网站。
减少应用程序中的安全漏洞的最最有效的方法之一是通过系统化和知情的代码审查流程。代码审查的问题在于,它们最终总是成为意见和个人偏好的转储区域,这通常不仅不会提高代码的质量,而且还会导致最后一刻的更改,从而可能暴露我们应用程序中的漏洞。
产品开发生命周期中用于安全代码审查的专门阶段有助于大幅减少交付到生产环境的错误数量。
软件工程师面临的问题是,他们的思维被训练来构建运行良好的东西,但他们没有发现缺陷的心态,尤其是围绕他们构建的东西。这就是为什么你不应该测试你自己的代码(比开发时进行的测试更进一步),更不用说安全测试你的应用程序了。
但是,我们通常在团队中工作,这使我们能够审查其他人的代码,但我们必须以有效的方式进行。
代码审查需要与编写软件一样多的脑力,尤其是在您审查复杂代码时。你不应该花费超过两个小时来审查相同的功能,否则会错过重要的缺陷,并且对细节的关注会降低到令人担忧的程度。
这在基于微服务的架构中不是什么大问题,因为功能应该足够小,以便在合理的时间内阅读,尤其是当您与作者讨论他正在尝试构建的内容时。
您应该始终遵循两阶段审查,如下所示:
快速查看代码以了解全局:它是如何工作的,它使用了哪些你不熟悉的技术,它是否做了它应该做的事情,等等
按照要查找的项目清单查看代码
此项目列表必须预先确定,并取决于您公司正在构建的软件的性质。
通常,在代码审查期间检查代码安全问题的项目列表非常大,但我们可以将其缩小到以下组件:
如果我们检查此列表,我们将能够确定我们应用程序中最大的安全问题。
可追溯性在现代信息系统中极其重要。这是微服务中的一个微妙问题,在 Seneca 中优雅地解决了,使得请求很容易在我们的系统中跟踪,以便我们可以审计失败。
Seneca 在日志记录方面非常。在 Seneca 中可以配置很多选项,以便获取有关一切工作方式(如果工作正常)的所需信息。
这是可以编写的最简单的 Seneca 应用程序。让我们按如下方式运行它:
这是使用默认日志记录配置运行应用程序的结果。除了我们在代码中使用的 console.log()
方法之外,还有一些关于 Seneca 被记录的内部信息。有时,您可能只想记录您的应用程序正在生成的内容,以便您可以调试应用程序而不会产生任何噪音。在这种情况下,只需运行以下命令:
但是,有时,系统中会出现奇怪的行为(甚至是使用的框架中的错误),而您想要获取有关正在发生的事情的所有信息. Seneca 也支持这一点,如以下命令所示:
为了减少 Seneca 生成的日志记录量,对记录到输出的内容进行了细粒度控制。让我们看一下以下几行:
它们是来自前面代码示例的日志输出的随机行,但它会为我们提供有用的信息:这些条目是 Seneca 框架上不同操作(例如插件、注册和操作)的调试级日志行。为了过滤它们,Seneca 提供了对我们想要查看的级别或操作的控制。考虑以下示例:
这只会输出与 INFO
级别相关的日志:
还可以按动作类型过滤,挺有意思的。当您使用微服务时,了解流中发生的事件链是审核故障时需要查看的第一件事。通过 Seneca 为我们提供的对日志记录的控制,只需执行以下命令即可:
如您所见,所有 前面的行都对应于 act
类型,甚至更多,如果我们从上到下跟随命令的输出,我们确切地知道 Seneca 反应的事件顺序及其顺序。
跟踪请求也是一项非常重要的活动,有时,它甚至是法律要求,特别是如果您在金融领域工作。同样,Seneca 非常擅长跟踪请求。对于每个呼叫,Seneca 都会生成一个唯一标识符。这个标识符可以在调用的所有路径中被追踪到,如下所示:
在这里,我们将包含 Seneca 中的事务 ID 的字典记录到终端。因此,如果我们执行它,我们将得到以下输出:
您可以看到 Seneca 中的所有请求是如何被跟踪的:框架分配一个 ID 并跨端点传播。在这种情况下,我们所有的端点都在本地机器上,但是如果我们将它们分布在不同的机器上,ID 仍然是相同的。
有了这个唯一的 ID,我们将能够在我们的系统中重建客户数据的旅程,并且 使用相关的时间戳对请求进行排序,我们 可以准确了解用户在做什么,每个动作花了多少时间,与延迟相关的可能问题是什么,等等。通常,结合断路器输出信息的日志记录允许工程师在非常短的时间内解决问题。
到目前为止,我们一直在使用console.log()
将数据输出到日志中。这是一个不好的做法。它破坏了日志的格式并将内容抛出到 标准输出。
再次,塞内卡来救援:
让我们看看 Seneca 产生了什么输出:
如您所见,我们现在使用记录器输出事务 ID。我们生成了 WARN
消息,而不是简单的控制台转储。从现在开始,我们可以使用 Seneca 日志过滤器来隐藏我们操作的输出,以便专注于我们正在尝试查找的内容。
Seneca 提供以下五个级别的日志记录:
DEBUG:用于在开发应用程序时调试应用程序并跟踪 生产系统中的问题。
WARN:这是警告级别。我们在系统发生不好的事情时使用它,但这并不重要,用户通常不会受到影响;但是,这表明某些事情正在以错误的方式进行。
产生不同级别的日志的一种方法是使用相关函数。正如我们之前所见,我们调用this.log.warn()
来记录警告。如果我们调用 this.log.fatal()
方法,我们将记录一个致命错误,与其他级别相同。
通常,INFO、DEBUG 和 WARN 将是最常用的日志级别。
HTTP 代码经常被忽略,但它们是标准化远程服务器响应的非常重要的机制。
当 程序(或用户)向服务器发出请求时,可能会发生一些事情,如下所示:
可能会成功
它可能无法通过验证
它可能会产生服务器错误
如您所见,可能性是无穷无尽的。我们现在遇到的问题是 HTTP 是为机器之间的通信而创建的。我们如何处理机器将读取这些代码的事实?
HTTP 以一种非常优雅的方式解决了这个问题:每个请求都必须使用 HTTP 代码来解决,并且这些代码具有指示代码性质的范围。
成功代码 用于指示 HTTP 请求的一定程度的成功。它是最常见(也是最需要的)代码。
该范围内最常见的代码如下:
200: Success
:此代码表示完全成功。即使是远程也没有出错。201: Created
:此代码主要用于客户端请求在服务器中创建新实体时的 REST API。203:非权威信息
:此代码旨在用于通过转换代理路由请求时,源以 200 响应。204: No Content
:这是一个成功的代码,但是服务器没有返回任何内容。有时,即使没有内容,API 也会返回 200。206: Partial Content
:此代码用于分页响应。发送一个标头,指定客户端将接受的范围(和偏移量)。如果响应大于范围,服务器将回复 206,表示还有更多数据要跟踪。
400 到 499 范围内的代码代表客户端产生的错误。它们表明请求存在问题。这个范围特别重要,因为它是 HTTP 服务器必须向客户端指示他们的请求有问题的方式。
该范围内的常用代码如下:
400 Bad Request
:此代码表示来自用户的请求在语法上不正确。可能缺少参数或某些值未通过验证。401 Unauthorized
:此代码表示客户端缺少身份验证。通常,有效的登录将解决此问题。403 Forbidden
:这个和401类似,但是在这种情况下,表示用户没有足够的权限。404 Not Found
:表示在服务器中找不到资源。这是您在导航到不存在的页面时遇到的错误。
在本章中,您已经学习了如何构建安全软件(而不仅仅是微服务),尽管它的主题足够大,可以写一本完整的书。安全问题在于,公司通常将安全投资视为烧钱,但这远非现实。我是 80-20 法则的忠实拥护者:20% 的时间会给你 80% 的功能,而 20% 的缺失功能需要 80% 的时间。
在安全方面,我们确实应该以 100% 的覆盖率为目标;但是,本章中显示的 80% 将涵盖大多数情况。无论如何,正如我之前提到的,软件工程师应该了解最新的安全性,因为应用程序的安全性缺陷是杀死公司的最简单方法。
我们还一直在谈论可追溯性和日志记录,这是现代软件工程中最被忽视的主题之一,它变得越来越重要,特别是如果您的软件是使用微服务方法构建的。