在 AWS S3/CloudFront 中使用 Lambda@Edge 和 Terraform 添加安全标头
已发表: 2021-04-24当您登录https://app.sendgrid.com查看您的帐户详细信息或访问https://mc.sendgrid.com进行营销活动时,您正在访问我们在 AWS S3 存储桶中托管的前端 Web 应用程序CloudFront 分布在它们之上。
我们的 Web 应用程序本质上由缩小和代码拆分的 JavaScript 和 CSS 文件、HTML 文件和图像文件组成,这些文件作为 CloudFront 缓存上传到 S3 存储桶并将它们提供给我们的用户。 我们的每个 Web 应用程序环境(包括测试、暂存和生产)都有单独的 S3 存储桶和 CloudFront 分配。
这种 AWS S3 和 CloudFront 基础设施非常适合我们在内容交付网络上托管文件的大规模 Web 应用程序,但我们的初始配置缺乏安全标头形式的更严格的保护。
添加这些安全标头可以防止用户受到攻击,例如跨站点脚本、MIME 嗅探、点击劫持、代码注入和与不安全协议相关的中间人攻击。 如果置之不理,这些将对我们客户的数据以及我们公司对能够在网络上提供安全体验的信任产生严重后果。
在我们研究如何添加这些标题之前,我们首先退后一步看看我们在哪里。 在通过安全标头扫描网站运行我们的 Web 应用程序 URL 后,我们不出所料地收到了不及格的成绩,但看到了一个有用的标头列表,如下所示。
如您所见,还有很大的改进空间。 我们研究了如何配置我们的 AWS S3 和 CloudFront 资源以使用安全标头进行响应,以减轻上述风险和漏洞。
在高层次上,我们可以通过创建一个 Lambda@Edge 函数来完成此操作,该函数会在 Web 应用程序的文件返回用户浏览器之前更改原始响应标头以附加所需的安全标头。
该策略是首先通过 AWS 控制台手动测试连接。 然后,我们会将这些配置放入 Terraform 中,以将这部分基础设施保存在代码中,以供将来在其他团队和应用程序之间参考和共享。
我们要添加什么样的安全标头?
作为我们产品安全团队建议的一部分,我们的任务是添加安全标头,例如“Strict-Transport-Security”和“X-Frame-Options”。 我们建议您还查看MDN Web 安全备忘单等资源以了解最新情况。 以下是可应用于 Web 应用程序的安全标头的简短摘要。
严格的运输安全 (HSTS)
这是为了提示浏览器通过 HTTPS 而不是 HTTP 访问您的 Web 应用程序。
内容安全策略 (CSP)
这是为您在 Web 应用程序中加载或连接的资源类型设置明确的允许列表,例如脚本、图像、样式、字体、网络请求和 iframe。 这对我们来说是最难设置的,因为我们有第三方脚本、图像、样式和 API 端点来明确记录在这些策略中。
提示:使用Content-Security-Policy-Report-Only标头可帮助您在某些环境中进行测试。 如果某些资源违反了政策,我们会观察到我们需要在政策中允许的资源的有用控制台输出。
如果您想避免出现空白屏幕和 Web 应用程序无法加载的有趣事故,我们强烈建议您先在仅报告模式下试验您的策略并进行彻底测试,然后才能有足够的信心在生产中部署这些安全策略。
X-Content-Type-Options
这是为了在您的网页中维护和加载具有正确 MIME 类型的资产。
X 框架选项
这是为了提供有关您的 Web 应用程序如何在 iframe 中加载的规则。
X-XSS-保护
如果在某些浏览器中检测到跨站点脚本攻击,这会阻止页面加载。
推荐人政策
这管理“Referrer”标头在跟随外部站点或资源的链接时如何传递有关请求来源的信息。
考虑到这些安全标头,让我们回到我们今天如何设置 CloudFront 分配以及 Lambda@Edge 函数将如何帮助我们实现目标。
将 Lambda@Edge 与我们的 CloudFront 分配一起使用
对于我们的 CloudFront 分配,我们进行了如下设置:
- 要附加在 CloudFront URL 之上的域的 SSL 证书,例如https://app.sendgrid.com
- S3 存储桶来源
- 具有用于自动故障转移的主存储桶和副本存储桶的源组
- 缓存行为
特别是,这些缓存行为使我们能够控制我们希望某些类型的路径和文件的响应在世界各地的边缘服务器中缓存多长时间。 此外,缓存行为还为我们提供了一种触发 AWS Lambda 函数以响应各种事件的方法,例如源请求和源响应。 您可以将 AWS Lambda 函数视为您定义的特定代码,将运行以响应特定事件。
在我们的例子中,我们可以在原始响应头被边缘服务器缓存之前更改它。 我们的 Lambda@Edge 函数将在原始响应最终返回到边缘服务器之前以及在最终用户收到带有这些标头的 JavaScript、CSS 和 HTML 文件之前,将自定义安全标头添加到原始响应中。
我们根据这篇AWS 博客文章对我们的方法进行了建模,并对其进行了扩展,以便更轻松地更改特定的内容安全策略。 您可以按照我们为脚本、样式和连接源生成列表的设置方式为您的 Lambda@Edge 函数建模。 此函数有效地修改 CloudFront 原始响应标头,并在通过调用提供的回调函数返回之前将具有特定值的每个安全标头附加到响应中,如下所示。
我们如何测试这个 Lambda@Edge 函数?
在正式更改资产返回安全标头的方式之前,您应该在通过 AWS 控制台手动配置所有内容后验证该功能是否正常工作。 至关重要的是,您的 Web 应用程序应该能够加载并正常运行,并将安全标头添加到您的网络响应中。 您最不想听到的是由于安全标头而发生的意外中断,因此请在您的开发环境中彻底测试它们。
了解稍后将在 Terraform 代码中编写的确切内容以将此配置保存在代码库中也很重要。 如果您不了解 Terraform,它为您提供了一种通过代码编写和管理云基础架构的方法。
提示:查看 Terraform文档,看看它是否可以帮助您维护复杂的配置,而无需记住您在云控制台中执行的所有步骤。
如何开始使用 AWS 控制台
让我们开始了解如何通过 AWS 控制台手动进行设置。
- 首先,您需要在“us-east-1”区域创建 Lambda@Edge 函数。 转到 Lambda 服务页面,我们将单击“创建函数”并将其命名为“testSecurityHeaders1”。
2.您可以使用具有权限的现有角色在边缘服务器上运行该函数,或者您可以使用他们的角色策略模板之一,例如“Basic Lambda@Edge Permissions...”,并将其命名为“lambdaedgeroletest”。
3. 创建测试 Lambda 函数和角色后,您应该会看到类似这样的内容,您会注意到该函数的“添加触发器”按钮。 这是您最终将 Lambda 与在源响应事件上触发的 CloudFront 分配的缓存行为相关联的地方。
4.接下来,您需要使用我们之前制作的安全标头代码编辑功能代码,然后点击“保存”。
5. 保存函数代码后,让我们通过滚动到顶部并点击“测试”按钮来测试您的 Lambda 函数是否有效。 您将使用“cloudfront-modify-response-header”事件模板创建一个名为“samplecloudfrontresponse”的测试事件,以模拟实际的 CloudFront 源响应事件并查看您的函数如何针对它运行。
您会注意到您的 Lambda 函数代码将修改的“cf.response”标头对象之类的内容。
6. 创建测试事件后,您将再次单击“测试”按钮,应该会看到 Lambda 函数是如何针对它运行的。 它应该成功运行,日志显示结果响应并添加了这样的安全标头。
太好了,Lambda 函数看起来像是正确地将安全标头附加到响应中!
7. 让我们回到“设计器”区域并单击“添加触发器”按钮,以便您可以将 Lambda 函数与您的 CloudFront 分配在源响应事件上的缓存行为相关联。 确保选择“CloudFront”触发器并单击“部署到 Lambda@Edge”按钮。
8. 接下来,选择 CloudFront 分配(在我们的示例中,出于安全原因,我们在此处清除了输入)和与之关联的缓存行为。
然后,您选择“*”缓存行为并选择“源响应”事件以匹配到 CloudFront 分配的所有请求路径,并确保 Lambda 函数始终针对所有源响应运行。
然后,您在单击“部署”以正式部署您的 Lambda 函数之前检查确认。
9. 将您的 Lambda 函数与所有相关 CloudFront 分配的缓存行为成功关联后,您应该会在 Lambda 仪表板“设计器”区域中看到类似的内容,您可以在其中看到 CloudFront 触发器并可以选择查看或删除它们。
更改您的 Lambda 代码
每当您可能需要更改 Lambda 代码时,我们建议:
- 通过“操作”按钮下拉列表发布新版本
- 删除旧版本上的触发器(您可以单击“限定符”下拉菜单以查看您的 Lambda 的所有版本)
- 将触发器与您最近发布的最新版本号相关联
首次部署 Lambda 或发布新版本的 Lambda 并将触发器与较新的 Lambda 版本相关联后,您可能不会立即在 Web 应用程序的响应中看到安全标头。 这是因为 CloudFront 中的边缘服务器缓存响应的方式。 根据您在缓存行为中设置的生存时间,您可能需要等待一段时间才能看到新的安全标头,除非您在受影响的 CloudFront 分配中执行缓存失效。
在将您的更改重新部署到您的 Lambda 函数后,在您的响应对您的安全标头进行最新调整之前,通常需要一些时间来清除缓存(取决于您的 CloudFront 缓存设置)。
提示:为避免频繁刷新页面或坐视您的更改是否有效,请启动 CloudFront 缓存失效以加快清除缓存的过程,以便您可以看到更新的安全标头。
转到您的 CloudFront 服务页面,等待部署 CloudFront 分配的状态,这意味着所有 lambda 关联都已完成并已部署,然后转到“无效”选项卡。 单击“创建无效”并将“/ *”作为对象路径以使缓存中的所有内容无效,然后单击“无效”。 这应该不会花费太长时间,完成后,刷新您的 Web 应用程序应该会看到最新的安全标头更改。
当您根据您在 Web 应用程序中发现的违规或错误对安全标头进行迭代时,您可以重复此过程:
- 发布新的 Lambda 函数版本
- 删除旧 Lambda 版本上的触发器
- 在新版本上关联触发器
- 缓存使您的 CloudFront 分配无效
- 测试您的 Web 应用程序
- 重复直到您感到自信和安全,事情按预期工作,没有任何空白页、失败的 API 请求或控制台安全错误
一旦事情稳定下来,假设您已将 Terraform 与您的 AWS 账户集成,您可以选择继续将您刚刚手动执行的 Terraform 转换为代码配置。 我们不会从一开始就介绍如何设置 Terraform,但我们将向您展示 Terraform 代码的片段。
对由我们的 CloudFront 分布触发的 Lambda@Edge 进行地形改造
在对“us-east-1”区域中的安全标头的 Lambda@Edge 函数进行迭代之后,我们希望将其添加到我们的 Terraform 代码库中,以实现代码的可维护性和版本控制。
对于我们已经实现的所有缓存行为,我们必须将缓存行为与原始响应事件触发的 Lambda@Edge 函数相关联。
以下步骤假设您已经通过 Terraform 配置了大部分 CloudFront 分配和 S3 存储桶。 我们将重点关注与 Lambda@Edge 相关的主要模块和属性,并将触发器添加到 CloudFront 分配的缓存行为。 我们不会介绍如何通过 Terraform 从头开始设置您的 S3 存储桶和其他 CloudFront 分配设置,但我们希望您能看到自己完成这项工作所付出的努力。
我们目前将 AWS 资源分解为单独的模块文件夹,并将变量传递到这些模块中,以实现我们的配置灵活性。 我们有一个包含development
和production
子文件夹的apply
文件夹,每个文件夹都有自己的main.tf
文件,我们在其中使用某些输入变量调用这些模块来实例化或修改我们的 AWS 资源。
这些子文件夹也都有一个lambdas
文件夹,我们在其中保存 Lambda 代码,例如security_headers_lambda.js
文件。 security_headers_lambda.js
与我们手动测试时在 Lambda 函数中使用的代码相同,除了我们还将它保存在代码库中,以便我们通过 Terraform 压缩和上传。
1. 首先,我们需要一个可重用的模块来压缩我们的 Lambda 文件,然后将其上传并发布为我们的 Lambda@Edge 函数的另一个版本。 这需要一个指向我们的 Lambda 文件夹的路径,该文件夹包含最终的 Node.js Lambda 函数。
2. 接下来,我们添加到我们现有的 CloudFront 模块中,该模块还通过创建一个从压缩的 Lambda 文件构建的 Lambda 资源来包装 S3 存储桶、策略和 CloudFront 分配资源。 因为 Lambda zip 模块的输出作为变量传递到 CloudFront 模块以设置 Lambda 资源,所以我们需要将 AWS 提供程序区域指定为“us-east-1”,并使用这样的工作角色策略。
3. 在 CloudFront 模块中,我们将这个 Lambda@Edge 函数与 CloudFront 分配的缓存行为相关联,如下所示。
4. 最后,将它们放在我们的apply/development
或apply/production
文件夹的main.tf
文件中,我们调用所有这些模块并将正确的输出作为变量放入我们的 CloudFront 模块中,如下所示。
这些配置调整基本上负责我们在上一节中执行的手动步骤,以更新 Lambda 代码并将较新版本与 CloudFront 的缓存行为和源响应事件触发器相关联。 呜呼! 只要我们将这些更改应用于我们的资源,就无需经历或记住 AWS 控制台步骤。
我们如何在不同的环境中安全地推出它?
当我们第一次将 Lambda@Edge 函数与我们的测试 CloudFront 分配相关联时,我们很快注意到我们的 Web 应用程序将不再正确加载。 这主要是由于我们如何实现 Content-Security-Policy 标头以及它没有涵盖我们在应用程序中加载的所有资源。 就阻止我们的应用程序加载而言,其他安全标头构成的风险较小。 未来,我们将专注于推出安全标头,并考虑进一步迭代以微调 Content-Security-Policy 标头。
如前所述,我们发现了如何利用Content-Security-Policy-Report-Only标头来最大程度地降低风险,因为我们收集了更多资源域以添加到我们的每个策略中。
在此仅报告模式下,策略仍将在浏览器中运行,并将任何违反策略的控制台错误消息输出。 但是,它不会完全阻止这些脚本和源,因此我们的 Web 应用程序仍然可以照常运行。 我们有责任继续检查整个 Web 应用程序,以确保我们不会错过政策中的任何重要来源,否则将对我们的客户和支持团队产生负面影响。
对于每个环境,您可以推出安全标头 Lambda,如下所示:
- 手动或通过 Terraform 计划将更改发布到您的 Lambda,并首先将更改应用到具有其他安全标头和 Content-Security-Policy-Report-Only 标头的环境。
- 等待您的 CloudFront 分配状态与缓存行为关联的 Lambda 完全部署。
- 如果之前的安全标头仍然显示,或者您当前的更改需要很长时间才能显示在浏览器中,请对您的 CloudFront 分配执行缓存失效。
- 在开发人员工具打开的情况下访问并浏览您的 Web 应用程序页面,扫描控制台以查找任何“仅报告...”控制台错误消息,以改进您的 Content-Security-Policy 标头。
- 更改您的 Lambda 代码以考虑那些报告的违规行为。
- 从第一步开始重复,直到您有足够的信心将标头从 Content-Security-Policy-Report-Only 更改为 Content-Security-Policy,这意味着环境将强制执行它。
提高我们的安全标头分数
在成功将 Terraform 更改应用到我们的环境并使 CloudFront 缓存无效后,我们刷新了 Web 应用程序中的页面。 我们让开发者工具保持打开状态,以查看安全标头,例如 HSTS、CSP 以及我们网络响应中的其他标头,例如下面显示的安全标头。
我们还通过安全标头扫描报告(例如本网站上的报告)运行我们的 Web 应用程序。 结果,我们目睹了与之前不及格的成绩相比的巨大改进(A 级!),您可以在更改 S3/CloudFront 设置以设置安全标头后实现类似的改进。
使用安全标头前进
在通过 AWS 控制台手动设置安全标头或成功改造解决方案并将更改应用到您的每个环境后,您现在拥有了进一步迭代和改进现有安全标头的良好基础。
根据您的 Web 应用程序的演变,您可能必须使 Content-Security-Policy 标头在允许更严格安全性的资源方面更加具体。 或者,您可能需要完全添加新标头以用于单独目的或填充另一个安全漏洞。
通过这些未来对 Lambda@Edge 函数中的安全标头的更改,您可以在每个环境中遵循类似的发布策略,以确保您的 Web 应用程序免受 Web 上的恶意攻击,并且仍然可以在您的用户没有注意到差异的情况下运行。
有关 Alfred Lucero 撰写的更多文章,请访问他的博客作者页面: https ://sendgrid.com/blog/author/alfred/