专业的编程技术博客社区

网站首页 > 博客文章 正文

Rust Web编程:第十一章 在 AWS 上使用 NGINX 配置 HTTPS

baijin 2024-08-17 10:52:55 博客文章 12 ℃ 0 评论

在部署我们的应用程序时,许多教程和书籍都会介绍简单的部署,并阐明使用 HTTPS 加密进出服务器的流量的概念。 然而,HTTPS 是必不可少的,并且通常是开发人员将其网站或 API 推向世界时必须克服的最大障碍。 虽然本书的标题是 Rust Web 编程,但有必要专门用一章来真正理解 HTTPS 的工作原理,以便您可以在本地实现 HTTPS,然后在 Amazon Web Services (AWS) 云上实现。 本章将涵盖以下主题:

什么是 HTTPS?

使用 docker-compose 在本地实现 HTTPS

将 URL 附加到我们在 AWS 上部署的应用程序

在 AWS 上的应用程序上强制执行 HTTPS

在本章结束时,您将能够使用 Terraform 代码构建基础设施,该基础设施可以加密流量并锁定到我们的弹性计算云 (EC2) 实例的不需要的流量,以便它们只能接受来自负载均衡器的流量,这将是 本章稍后会进行解释。 最好的是,其中大部分都是自动化的,并且由于我们使用 Docker 来部署我们的应用程序,您将能够将此技能转移到您将来想要部署的任何 Web 项目中。 虽然这不是最好的实现,因为有整本书专门介绍云计算,但您将能够实现可靠、安全的部署,即使 EC2 实例停止服务,也可以继续为用户提供服务,因为负载均衡器可以路由到 其他情况。 如果流量需求增加,它也将能够扩展。

什么是 HTTPS?

到目前为止,我们的前端和后端应用程序都是通过HTTP运行的。 然而,这并不安全并且有一些缺点。 为了保护浏览器和 NGINX 服务器之间的流量,我们必须确保我们的应用程序使用 HTTP/2 协议。 HTTP/2协议与标准HTTP/1协议有以下区别:

二进制协议

压缩标头

持久连接

多路传输

我们可以回顾一下前面列出的要点并讨论其中的差异。

二进制协议

HTTP 使用基于文本的协议,而 HTTP/2 使用二进制协议。 二进制协议使用字节来传输数据,而不是使用美国信息交换标准代码 (ASCII) 进行编码的人类可读字符。 使用字节可以减少可能的错误数量以及传输数据所需的大小。 它还将使我们能够加密数据流,这是 HTTPS 的基础。

压缩标头

HTTP/2 在发送请求时压缩标头。 压缩标头与二进制协议具有类似的优点,这会导致传输相同请求所需的数据大小更小。 HTTP/2 协议使用 HPACK 格式。

持久连接

当使用HTTP时,我们的浏览器必须在每次需要资源时发出请求。 例如,我们可以让 NGINX 服务器提供 HTML 文件。 这将导致一个获取 HTML 文件的请求。 HTML 文件内可能引用 CSS 文件,这将导致向 NGINX 服务器发出另一个请求。 在 HTML 文件中引用 JavaScript 文件也并不罕见。 这将导致另一个请求。 因此,要加载一个标准网页,我们的浏览器最多需要三个请求。 当我们运行具有多个用户的服务器时,这不能很好地扩展。 使用 HTTP/2,我们可以拥有持久连接。 这意味着我们可以在一个连接中对 HTML、CSS 和 JavaScript 文件发出三个请求。

多路传输

使用 HTTP/1 发出请求意味着我们必须按顺序发送请求。 这意味着我们发出一个请求,等待该请求得到解决,然后发送另一个请求。 对于 HTTP/2,我们使用多路复用流,这意味着我们可以同时发送多个请求,并在返回响应时解析它们。 将多路复用流与持久连接相结合可以缩短加载时间。 任何在 20 世纪 90 年代使用过互联网的读者都会记得必须等待很长时间才能加载一个简单的页面。 诚然,当时的互联网连接速度没有那么快,但这也是使用不同连接发出多个 HTTP 顺序请求来加载多个图片、HTML 和 CSS 的结果。

现在我们已经探讨了 HTTP 和 HTTP/2 之间的区别,接下来我们可以探讨构建在 HTTP/2 之上的 HTTPS。 然而,在我们继续讨论之前,必须指出安全本身就是一个领域。 学习有关 HTTPS 的高级概念足以让我们了解我们正在实施的内容的重要性以及我们采取某些步骤的原因。 然而,它并不能让我们成为安全专家。

在我们探讨HTTPS的步骤之前,我们需要了解什么是中间人攻击,因为这种攻击激发了HTTPS的步骤。 中间人攻击顾名思义:恶意窃听者可以拦截用户和服务器之间的通信数据包。 这也意味着窃听者如果通过网络传递也可以获得加密。 只需谷歌搜索“中间人攻击”就会出现大量可以下载来实施此类攻击的教程和软件。 还有更多安全注意事项超出了本书的范围,但总而言之,如果您托管一个希望用户连接并登录的网站,则没有理由不使用 HTTPS。

当涉及 HTTPS 时,需要执行一系列步骤。 首先,在向服务器发出任何请求之前,服务器和域的所有者必须从受信任的中央机构获取证书。 周围没有多少值得信赖的中央当局。 这些机构所做的就是获取拥有该域名的人的一些身份信息以及申请证书的人拥有该域名的一些证据。 这听起来可能令人头疼,但许多 URL 提供商(例如 AWS)已经使用信息(例如付款详细信息)简化了流程,当您指向并单击购买域名时,这些信息会发送到后端的中央受信任机构。 我们必须填写一些额外的表格,但如果您有一个有效的 AWS 账户,这不会太费力。

这些中央机构是有限的,因为任何拥有计算机的人都可以创建数字证书。 例如,如果我们拦截服务器和用户之间的流量,我们可以使用密钥生成我们自己的数字证书并将其转发给用户。 因此,主流浏览器只识别由少数公认机构颁发的证书。 如果浏览器获取到无法识别的证书,这些浏览器会发出警告,例如下面的 Chrome 示例:



不建议单击“高级”继续。

一旦域和服务器的所有者获得了中央机构的证书,用户就可以提出请求。 在与应用程序交换任何有意义的数据之前,服务器将带有公钥的证书发送给用户。 然后,用户创建一个会话密钥并加密该会话密钥和证书的公钥,然后将其发送回服务器。 然后,服务器可以使用不是通过网络发送的私钥来解密密钥。 因此,即使窃听者设法拦截消息并获取会话密钥,它也是加密的,因此他们无法使用它。 服务器和客户端都可以检查相互加密的消息的有效性。 我们可以使用会话密钥在服务器和用户之间发送加密消息,如下图所示:



不用担心,有一些软件包和工具可以帮助我们管理 HTTPS 进程; 我们不必执行我们自己的协议。 您将了解为什么我们必须执行某些步骤以及出现问题时如何解决问题。 在下一节中,我们将在本地使用 NGINX 实现基本的 HTTPS 协议。

使用 docker-compose 在本地实现 HTTPS

在实现 HTTPS 时,大部分工作将通过 NGINX 来完成。 尽管我们已经使用了一些 NGINX,但 NGINX 配置是一个强大的工具。 您可以实现条件逻辑、从请求中提取变量和数据并对其进行操作、重定向流量等等。 在本章中,我们将做足够多的工作来实现 HTTPS,但建议您如果有时间阅读 NGINX 配置的基础知识; 进一步阅读部分提供了阅读材料。 对于我们的deployment/nginx_config.yml 文件,我们需要具有以下布局:

worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
events {
    worker_connections  512;
}
http {
    server {
        . . .
    }
    server {
        . . .
    }
}

在这里,我们可以看到我们的 http 范围中有两个服务器范围。 这是因为我们需要强制执行 HTTPS。 我们必须记住,我们的外部端口是 80。但是,如果我们想要进行 HTTPS 连接,我们需要连接到端口 443,这是 HTTPS 的标准。 在浏览器中输入 https:// 将定位端口 443,在浏览器中输入 http:// 将定位端口 80。如果我们允许端口 80 打开,大量用户将以不安全的方式访问我们的网站,因为有些人 将在浏览器中输入http://。

黑客还会传播 HTTP 链接,因为他们希望尽可能多的人不使用安全网络。 但是,如果我们阻止端口 80,则在浏览器中输入 http:// 的人将被阻止访问该网站。 普通用户不太可能理解端口的差异、查看他们输入的内容并进行纠正。 相反,他们只会认为该网站已关闭。 因此,我们必须同时侦听端口 443 和 80。但是,当向端口 80 发出请求时,我们会将请求重定向到端口 443。 我们的第一个服务器范围可以使用以下代码进行重定向:

server {
    listen 80;
    return 301 https://$host$request_uri;
}

在这里,我们可以看到我们侦听端口 80,然后返回与发送的相同请求,但使用 HTTPS 协议,这意味着它将到达我们的 443 端口。 我们还可以看到我们引用了 $host 和 $request_uri 变量。 这些变量是 NGINX 中自动填充的标准变量。 我们可以使用以下代码行定义自己的变量:

set $name 'Maxwell';

然而,我们希望 NGINX 实例能够在我们的服务器和本地主机上工作,因此使用标准变量是这里的最佳选择。 现在我们已经定义了重新路由规则,我们可以进入下一个服务器范围; 我们使用以下代码监听 443 端口:

server {
    listen 443 ssl http2;
    ssl_certificate /etc/nginx/ssl/self.crt;
    ssl_certificate_key /etc/nginx/ssl/self.key;
    location /v1 {
        proxy_pass http://rust_app:8000/v1;
    }
    location / {
        proxy_pass http://front_end:4000/;
    }
}

查看前面的代码,必须注意的是,我们正在同一 NGINX 实例中处理和引导前端和后端应用程序的流量。 除了端口定义之外,我们还声明我们正在使用 ssl 和 http2 NGINX 模块。 这并不奇怪,因为 HTTPS 本质上是 HTTP/2 之上的 SSL。 然后,我们定义服务器证书在 NGINX 容器中的位置。 我们稍后会将它们添加到 docker-compose 卷中。 我们还可以看到我们通过 HTTP 将 HTTPS 请求传递到适当的应用程序。 如果我们尝试将这些代理更改为 HTTPS 协议,那么我们会收到错误的网关错误。 这是因为 NGINX 和我们的服务之间的握手会失败。 这不是必需的,因为我们必须记住,前端和后端应用程序公开的端口不可用于本地主机之外的任何人。 是的,在我们的本地计算机上我们可以访问它们,但这是因为它们在我们的本地计算机上运行。 如果我们要部署应用程序服务器,它将如下所示:


我们的 NGINX 配置不是最佳的。 可以在密码、缓存和管理超时方面调整一些设置。 然而,这足以让 HTTPS 协议正常工作。 如果您需要优化 NGINX 配置的缓存和加密方法,建议您寻求有关 DevOps 和 NGINX 的进一步教育材料。

现在我们已经定义了 NGINX 配置,我们必须定义我们的证书。

笔记

要定义我们自己的证书,我们必须使用以下链接按照以下步骤安装 openssl 软件包:

Linux:

https://fedingo.com/how-to-install-openssl-in-ubuntu/

Windows:

https://linuxhint.com/install-openssl-windows/

Mac:

https://yasar-yy.medium.com/installing-openssl-library-on-macos-catalina-6777a2e238a6

这可以通过以下命令来完成:

openssl req -x509 -days 10 -nodes -newkey rsa:2048
-keyout ./self.key -out ./self.crt

这将使用 x509 创建密钥,x509 是国际电信联盟标准。 我们声明证书将在 10 天后过期,并且密钥和证书的名称为 self。 它们可以被称为任何东西; 然而,对于我们来说,将证书称为 self 是有意义的,因为它是自我颁发的证书。 前面的代码片段中显示的命令将推送几个提示。 您对这些提示说什么并不重要,因为我们只会使用它们发出本地主机请求,这意味着它们永远不会到达我们本地计算机之外的任何地方。 如果您可以引用 docker-compose.yml 文件中放置的密钥和证书,我们现在可以将密钥和证书存储在部署目录中的任何位置。 在我们的 docker-compose.yml 文件中,我们的 NGINX 服务现在采用以下形式:

nginx:
container_name: 'nginx-rust'
image: "nginx:latest"
ports:
  - "80:80"
  - 443:443
links:
  - rust_app
  - front_end
volumes:
  - ./nginx_config.conf:/etc/nginx/nginx.conf
  - ./nginx_configs/ssl/self.crt:/etc/nginx/ssl/self.crt
  - ./nginx_configs/ssl/self.key:/etc/nginx/ssl/self.key

在这里,我们可以看到我选择将密钥和证书存储在名为 nginx_configs/ssl/ 的目录中。 这是因为如果您想要一些关于处理变量、条件逻辑和直接从 NGINX 提供 HTML 文件的简单快速参考,我已经在 GitHub 存储库的 nginx_configs 目录下添加了几个简单的 NGINX 配置。 虽然获取密钥和证书的位置可能有所不同,但将密钥和证书放入 NGINX 容器内的 etc/nginx/ssl/ 目录中非常重要。

现在您可以测试我们的应用程序以查看本地 HTTPS 是否正常工作。 如果您启动 docker-compose 实例,然后在浏览器中访问 https://localhost URL,您应该会收到一条警告,表明它不安全,并且您将无法立即连接到前端。 这是令人放心的,因为我们不是中央机构,所以我们的浏览器不会识别我们的证书。 浏览器有很多种,我们会在本书中浪费大量篇幅来描述如何为每种浏览器解决这个问题。 考虑到浏览器可以免费下载,我们可以通过访问 flags URL 来绕过 Chrome 中应用程序的阻止,如下所示:



在这里,我们可以看到我允许来自本地主机的无效证书。 现在我们的浏览器中启用了无效证书,我们可以访问我们的应用程序,如下所示:



这里,我们使用的是HTTPS协议; 然而,正如我们在前面的屏幕截图中看到的,Chrome 抱怨说它不安全。 我们可以通过点击“不安全”声明来检查原因,给出以下视图:



在这里,我们可以看到我们的证书无效。 我们预计该证书无效,因为它是我们颁发的,这使得它未被官方认可。 但是,我们的 HTTPS 连接正常! 了解 HTTPS 的工作原理很有趣; 但是,对于在我们的本地主机上运行的自签名证书来说,它没有用。 如果我们想利用 HTTPS,我们必须将其应用到 AWS 上的应用程序中。 在 AWS 上实施 HTTPS 之前,我们需要执行几个步骤。 在下一节中,我们将为我们的应用程序分配一个 URL。

将 URL 附加到我们在 AWS 上部署的应用程序

在上一章中,我们成功地将待办事项应用程序部署到AWS上的服务器上,并通过将服务器的IP地址输入浏览器来直接访问该应用程序。 在注册我们的 URL 时,您将遇到多个缩写词。 为了在导航 AWS 路由时感到舒适,有必要通过阅读下图来熟悉 URL 缩写词:



当我们将 URL 与应用程序关联时,我们将配置域名系统 (DNS)。 DNS 是将用户友好的 URL 转换为 IP 地址的系统。 为了使 DNS 系统正常工作,我们需要以下组件:

域名注册商:AWS、Google Cloud、Azure、GoDaddy 等组织如果收到域名付款以及域名负责人的个人详细信息,就会注册域名。 如果该 URL 用于非法活动,该组织还将处理滥用报告。

DNS 记录:一个注册的 URL 可以有多个 DNS 记录。 DNS 记录本质上定义了 URL 的路由规则。 例如,简单的 DNS 记录会将 URL 转发到服务器的 IP 地址。

区域文件:DNS 记录的容器(在我们的例子中,区域文件将由 AWS 管理)。

DNS 记录和注册商对于我们的 URL 正常运行至关重要。 尽管我们可以直接连接到 IP 地址,但如果我们想要连接到 URL,则需要几个中间人,如下所示:



从上图中可以看出,如果我们想要连接到服务器,我们会将 URL 发送到本地 DNS 服务器。 然后,该服务器按从上到下的顺序进行三个调用。 三个请求结束后,本地 DNS 服务器将获得与该 URL 相关的 IP 地址。 我们可以看到注册商负责部分映射。 这是配置我们的 DNS 记录的地方。 如果我们删除 DNS 记录,则该 URL 将不再在互联网上可用。 我们不必每次输入 URL 时都进行图 11.8 中列出的调用。

我们的浏览器和本地 DNS 服务器会将 URL 缓存到映射的 IP 地址,以减少对其他三个服务器的调用次数。 然而有一个问题; 当我们构建生产服务器时,您可能已经意识到,每次我们拆除和启动生产服务器时,IP 地址都会发生变化。 这里没有什么问题; 当我们创建EC2实例时,我们必须使用一个可用的服务器。 像AWS这样的云提供商不能只为我们保留服务器,除非我们愿意付费。 在下一节中,我们将使我们的 IP 与弹性 IP 地址保持一致。

将弹性IP附加到我们的服务器

弹性IP地址本质上是我们保留的固定IP地址。 然后,我们可以在我们认为合适的某个时间点将这些弹性 IP 地址附加到任何一个 EC2 实例。 这在路由时非常有用。 我们可以设置URL到弹性IP的路由,然后将弹性IP的分配切换到我们需要的服务器。 这意味着我们可以将新应用程序部署到另一台服务器,对其进行测试,然后将弹性 IP 切换到新的部署服务器,而无需触及 URL 的路由。

我们不会在每次启动生产服务器时创建弹性 IP。 因此,可以在 AWS 控制台中点击来创建并附加弹性 IP 地址。 然而,在执行此操作之前,我们需要使用之前的 NGINX 配置文件来部署生产服务器,该文件没有定义 HTTPS,而是具有以下形式:

worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
events {
    worker_connections  512;
}
http {
    server {
        listen 80;
        location /v1 {
            proxy_pass http://rust_app:8000/v1;
        }
        location / {
            proxy_pass http://front_end:4000/;
        }
    }
}

现在这对您来说应该有意义,因为 NGINX 配置仅通过外部端口 80 侦听 HTTP 请求,然后将它们传递到我们的应用程序。 我们还必须删除对自签名证书的引用,因为我们不需要它们,而且我们也不会将这些证书上传到我们的服务器。 考虑到我们缺乏对证书的引用,部署目录中的 docker-compose 实例应该具有以下 NGINX 定义:

nginx:
  container_name: 'nginx-rust'
  image: "nginx:latest"
  ports:
    - "80:80"
  links:
    - rust_app
    - front_end
  volumes:
    - ./nginx_config.conf:/etc/nginx/nginx.conf

我们现在准备在生产服务器上部署我们的构建。 请记住,我们可以使用我们在上一章中设置的deployment/run_build.py Python 脚本来完成此操作。 服务器构建完成后,我们就知道有一个带有“待办生产服务器”标签的 EC2 实例。 现在,我们已准备好为 EC2 实例分配弹性 IP 地址。

要分配弹性IP,我们首先需要导航到EC2服务,方法是在顶部AWS仪表板的搜索栏中搜索EC2并单击该服务,出现以下视图:



我们可以看到在资源面板的右侧和屏幕的左侧都可以访问弹性IP。 进入弹性 IP 地址仪表板后,我们将获得您拥有的弹性 IP 地址列表。 在屏幕的右上角,将有一个橙色按钮,标有“分配弹性 IP 地址”。 如果您点击此按钮,您将得到以下创建表单:



我们在这里所做的就是从您正在工作的区域的 IP 地址池中获取弹性 IP 地址。 每个账户最多只能使用 5 个弹性 IP 地址。 如果您认为这对您来说还不够,您将需要在网络基础设施方面发挥更多创意。 您还可以研究为主帐户创建子帐户。 与干净的代码一样,拥有一次只适用于一个项目的干净帐户是有好处的。 这将帮助您跟踪成本,并且关闭项目的所有基础设施将是干净的,因为您可以确保通过删除帐户已清除该项目的所有内容。

继续为 EC2 服务器分配弹性 IP,我们可以通过在弹性 IP 仪表板中突出显示所需的弹性 IP 地址并单击仪表板右上角的“操作”按钮来分配弹性 IP,如下所示:



在操作下,我们必须单击关联弹性 IP 地址选项,显示以下内容:



实例选项将提供我们正在运行的 EC2 实例的下拉菜单。 幸运的是,我们有在 Terraform 中定义的有用的“待办事项生产服务器”标签。 但是,如果我们没有标签,我们仍然可以从下拉菜单中选择一个实例。 然后我们可以单击“关联”按钮。 完成此操作后,我们可以在浏览器中访问我们的弹性 IP 地址,并且我们应该能够访问我们的待办事项应用程序,如下所示:



到这里,我们的应用程序就可以通过弹性 IP 访问了! 现在,如果我们愿意,我们可以启动一个新服务器,对其进行测试,如果我们愿意,可以将我们的弹性 IP 重定向到新服务器,从而在用户不知情的情况下提供无缝更新。 然而,让用户输入原始 IP 地址是不可取的。 在下一部分中,我们将注册一个域并将其连接到我们的弹性 IP 地址。

注册域名

当涉及到注册域时,这一切都可以通过 Route 53 服务在 AWS 中处理。 首先,我们导航到 Route 53,它是处理路由和 URL 注册的服务。 在 Route 53 仪表板网页的左侧,我们可以单击“已注册的域”部分,如以下屏幕截图所示:



然后,我们将看到我们已经拥有的注册域列表以及注册域的选项,如以下屏幕截图所示:


如果您单击“注册域名”按钮,系统将引导您完成一系列简单的表格来注册您的域名。 对这些步骤进行屏幕截图太过分了。 该表格要求您提供要注册的域名。 然后他们会告诉您该域名和其他此类域名是否可用。 在撰写本书时,域名平均每年花费约 12 美元。 选择域名并单击结帐后,您将通过一系列个人信息表格。 这些表格包括联系地址以及该域名是供个人使用还是供公司使用。 对这些表单进行屏幕截图会导致页面过多,几乎没有什么教育优势,因为这些表单是个人的且易于填写。建议您选择通过 DNS 进行验证,因为这是自动化的。

注册域名后,您可以转到 Route 53 主仪表板中的托管区域部分。 在这里,我们将看到您拥有的每个 URL 的托管区域列表。 托管区域本质上是 DNS 记录的集合。 如果我们单击 URL 的托管区域,将会有两个 DNS 记录:NS 和 SOA(NS — 名称服务器;SOA — 授权起始点)。 这些记录不应被删除,如果删除,知道这些记录是什么的其他人可能会通过实施这些记录来劫持您的 URL。 DNS 记录本质上是有关如何路由域名流量的记录。 每个 DNS 记录具有以下属性:

域名/子域名:记录所属URL的名称

记录类型:记录的类型(A、AAAA、CNAME 或 NS)

值:目标IP地址

路由策略:Route 53 如何响应查询

TLL(生存时间):记录在客户端的 DNS 解析器中缓存的时间量,以便我们不必过于频繁地查询 Route 53 服务器,同时减少 DNS 服务器的流量与时间 以便将更新滚动到客户端

除了记录类型之外,刚刚定义的属性是不言自明的。 我们可以在 Route 53 中构建高级记录类型。但是,我们需要以下记录类型才能将我们的域路由到我们的 IP:

A:最简单的记录类型。 类型 A 仅将流量从 URL 路由到 IPv4 IP 地址。

AAAA:将流量从 URL 路由到 IPv6 地址。

CNAME:将一个主机名映射到另一个主机名(目标必须是 A 或 AAAA 记录类型)。

NS:托管区域的名称服务器(控制流量的路由方式)。

对于我们来说,我们将通过单击“创建记录”按钮来创建 DNS A 记录,如以下屏幕截图所示:


单击此按钮后,我们将得到以下布局:


我们可以看到记录类型A是默认的。 我们还可以看到我们可以添加一个子域。 这给了我们一些灵活性。 例如,如果我们愿意,api.freshcutswags.com URL 可以指向与 freshcutswags.com 不同的 IP 地址。 现在,我们将子域留空。 然后,我们将在上一节中设置的弹性 IP 地址放入“值”部分,然后单击“创建记录”。 然后,我们将创建完全相同的 DNS 记录,但子域为 www。 完成后,我们应该有两条 A 记录。 然后,我们可以使用 www.digwebinterface.com 网站检查我们的 URL 将流量映射到哪里。 在这里,我们可以输入 URL,网站会告诉我们 URL 被映射到哪里。 我们可以在这里看到我们的 URL 都映射到正确的弹性 IP:


确认映射结果后,如前面的屏幕截图所示,我们可以访问我们的 URL,并期望看到以下内容:



我们可以看到我们的 URL 现在可以工作了。 但是,该连接并不安全。 在下一节中,我们将为我们的应用程序强制执行 HTTPS 协议并将其锁定,就像现在一样,即使我们可以通过 URL 访问我们的应用程序,也没有什么可以阻止我们直接访问服务器的 IP。

在 AWS 上的应用程序上强制执行 HTTPS

目前,我们的应用程序可以运行,但在安全性方面这是一场噩梦。 到本节结束时,我们将不会拥有最安全的应用程序,因为建议进一步阅读网络和 DevOps 教科书以实现黄金标准的安全性。 但是,我们将配置安全组,锁定我们的 EC2 实例,以便外部人员无法直接访问它们,并通过负载均衡器强制加密流量,然后负载均衡器将流量引导到我们的 EC2 实例。 我们努力的结果将是以下系统:


为了实现如图11.20所示的系统,我们需要执行以下步骤:

获取我们的 URL 和变体批准的证书。

创建多个 EC2 实例来分配流量并确保服务不会出现中断。

创建负载均衡器来处理传入流量。

创建安全组。

更新我们的 Python 构建脚本以支持多个 EC2 实例。

使用 Route 53 向导将我们的 URL 连接到负载均衡器。

在上一节有关将 URL 附加到 AWS 上的应用程序的部分中,我们进行了大量的指向和单击操作。 正如前一章所述,如果可能的话,应该避免指向和点击,因为它是不可重复的,而且我们人类会忘记我们做了什么。 遗憾的是,URL 批准指向和单击是最好的选择。 在本节中,只有第一步和第六步需要指向和单击。 其余的将通过 Terraform 和 Python 来实现。 我们将对 Terraform 配置进行一些重大更改,因此建议您在更改 Terraform 配置之前运行 terraform destroy 命令。 然而,在进行任何编码之前,我们必须获取 URL 的证书。

获取我们 URL 的证书

因为我们通过由 AWS 处理的 Route 53 引入 URL,并且我们的服务器在 AWS 中运行,所以这些证书的认证和实施是一个简单的过程。 我们需要通过在服务搜索栏中输入证书管理器并单击它来导航到证书管理器。 到达那里后,我们将看到一个页面,其中只有一个标记为“请求证书”的橙色按钮。 点击此按钮,我们将进入以下页面:



我们希望我们的证书面向公众; 因此,我们对默认选择感到满意,然后单击“下一步”。 然后我们会看到以下表格:



在这里,我们输入要与证书关联的 URL。 我们可以添加另一个证书,但我们将为具有前缀的 URL 制作一个单独的证书,因为我们想要探索如何在 Terraform 中附加多个证书。 我们还可以看到 DNS 验证已经突出显示,建议这样做,因为我们的服务器位于 AWS 上,这意味着我们无需采取更多操作即可颁发证书。 然后,我们可以单击标有“请求”的按钮,我们将被重定向到包含证书列表的页面。 我发现几乎每次执行此操作时都不会出现新的证书请求。 我的猜测是有延迟。 不用担心,只需刷新页面,您就会看到列出的待处理证书请求。 单击此列表,您将看到此证书请求的详细视图。 在右侧屏幕中间,您需要单击“在 Route 53 中创建记录”按钮,如下所示:



点击按钮后按照提示操作,就会创建CNAME记录。 如果您不这样做,那么票证的待处理状态将无限期地持续下去,因为考虑到我们选择了 DNS 验证,云提供商需要颁发证书的路由。 几分钟后,应该会颁发证书。 完成此操作后,对前缀通配符执行相同的步骤。 完成此操作后,您的证书列表应如下所示:



在上面的屏幕截图中,我们可以看到我有两个证书:一个用于用户直接输入不带前缀的 URL,另一个用于覆盖所有前缀的通配符。 我们已准备好使用这些证书,但在此之前,我们必须执行一些其他步骤。 在定义流量规则之前,我们必须构建流量所在的基础设施。 在下一节中,我们将构建两个 EC2 实例。

创建多个 EC2 实例

我们将使用负载平衡器。 因此,我们至少需要两个 EC2 实例。 这意味着如果一个 EC2 实例发生故障,我们仍然可以使用另一个 EC2 实例。 我们还可以扩展我们的应用程序。 例如,如果世界上的每个人突然意识到他们需要一个待办事项应用程序来整理他们的生活,那么没有什么可以阻止我们增加 EC2 实例的数量来分配流量。 我们可以通过进入我们的deployment/main.tf 文件并使用以下 EC2 实例定义将 EC2 实例增加到两个:

resource "aws_instance" "production_server" {
    ami = "ami-0fdbd8587b1cf431e"
    instance_type = "t2.medium"
    count = 2
    key_name = "remotebuild"
    user_data = file("server_build.sh")
    tags = {
      Name = "to-do prod ${count.index}"
    }
    # root disk
    root_block_device {
      volume_size = "20"
      volume_type = "gp2"
      delete_on_termination = true
    }
}

在这里,我们可以看到我们添加了一个 count 参数并将其定义为 2。我们还更改了标签。 我们还可以看到我们通过索引访问正在创建的 EC2 实例的数量。 该索引从零开始,每次创建资源时都会增加 1。 现在我们有两个实例,我们必须使用以下代码更新部署/main.tf 文件底部的输出:

output "ec2_global_ips" {
  value = ["${aws_instance.production_server.*.public_ip}"]
}
output "db_endpoint" {
  value = "${aws_db_instance.main_db.*.endpoint}"
}
output "public_dns" {
  value =
      ["${aws_instance.production_server.*.public_dns}"]
}
output "instance_id" {
    value = ["${aws_instance.production_server.*.id}"]
}

在这里,我们可以看到除了数据库端点之外,所有其他输出都已更改为列表。 这是因为它们都引用了我们的多个 EC2 实例。 现在我们已经定义了 EC2 实例,我们可以使用负载均衡器将流量路由到我们的实例。

为我们的流量创建负载均衡器

我们可以选择一系列不同的负载均衡器。 我们已经讨论过 NGINX,它是一种流行的负载均衡器。 在本章中,我们将使用应用程序负载均衡器将流量路由到 EC2 实例并实现 HTTPS 协议。 负载均衡器可以提供多种功能,它们可以防止分布式拒绝服务 (DDoS) 攻击,攻击者会尝试通过过多的请求使服务器过载。 我们将在deployment/load_balancer.tf 文件中创建负载均衡器。 首先,我们使用以下代码收集我们需要的数据:

data "aws_subnet_ids" "subnet" {
    vpc_id = aws_default_vpc.default.id
}
data "aws_acm_certificate" "issued_certificate" {
    domain   = "*.freshcutswags.com"
    statuses = ["ISSUED"]
}
data "aws_acm_certificate" "raw_cert" {
    domain   = "freshcutswags.com"
    statuses = ["ISSUED"]
}

我们可以看到,我们使用的是数据声明,而不是资源。 我们在这里向 AWS 查询要在 Terraform 脚本的其余部分中使用的特定类型的数据。 我们获取 Virtual Private Cloud (VPC) ID。 在 Terraform 中,我们可以定义和构建 VPC,但在本书中,我们一直使用默认的 VPC。 我们可以获得负载均衡器的默认 VPC ID。 然后,我们获取我们在上一节中使用前面的代码定义的证书的数据。

我们现在必须为负载均衡器定义一个目标。 这是以目标组的形式完成的,我们可以将一组实例聚集在一起,以便负载均衡器使用以下代码进行定位:

resource "aws_lb_target_group" "target-group" {
    health_check {
        interval = 10
        path = "/"
        protocol = "HTTP"
        timeout = 5
        healthy_threshold = 5
        unhealthy_threshold = 2
    }
    name = "ToDoAppLbTg"
    port = 80
    protocol = "HTTP"
    target_type = "instance"
    vpc_id   = aws_default_vpc.default.id
}

在这里,我们可以看到我们定义了健康检查的参数。 健康检查的参数是不言自明的。 健康检查将针对目标群体的健康状况向服务发出警报。 我们不希望将流量路由到已关闭的目标组。 然后,我们定义流量的协议和端口、目标组中的资源类型以及 VPC 的 ID。 现在我们的目标组已定义,我们可以使用以下代码将 EC2 实例附加到它:

resource "aws_lb_target_group_attachment" "ec2_attach" {
    count = length(aws_instance.production_server)
    target_group_arn = aws_lb_target_group.target-group.arn
    target_id =
        aws_instance.production_server[count.index].id
}

我们可以看到,我们获取了目标 ID 的 EC2 服务器的 ID。 这样,我们的 EC2 实例就可以成为负载均衡器的目标。 现在我们有了目标,我们可以使用以下代码创建负载均衡器:

resource "aws_lb" "application-lb" {
    name = "ToDoApplicationLb"
    internal = false
    ip_address_type = "ipv4"
    load_balancer_type = "application"
    security_groups = ["${aws_security_group.
        alb-security-group.id}"]
        subnets = data.aws_subnet_ids.subnet.ids
    tags = {
        name = "todo load balancer"
    }
}

我们可以看到负载均衡器定义中的参数很简单。 但是,您可能已经注意到安全组定义。 尽管我们尚未定义任何安全组,但我们正在引用安全组。 如果您不知道什么是安全组,请不要担心 - 我们将在下一节中介绍并构建我们需要的所有安全组。 然而,在此之前,我们不妨为负载均衡器定义侦听和路由规则。 首先,我们可以为端口 80 定义 HTTP 侦听器。如果您还记得本章第一部分在我们的本地主机上运行 HTTPS 时的情况,您认为我们需要对 HTTP 流量做什么? 您不必了解具体的 Terraform 代码,但我们想要促进的一般行为是什么? 考虑到这一点,我们可以使用以下代码来实现该行为:

resource "aws_lb_listener" "http-listener" {
    load_balancer_arn = aws_lb.application-lb.arn
    port = 80
    protocol = "HTTP"
    default_action {
        type = "redirect"
        redirect {
            port        = "443"
            protocol    = "HTTPS"
            status_code = "HTTP_301"
        }
    }
}

这是正确的! 我们从端口 80 接收 HTTPS 流量,然后使用 HTTPS 协议将其重定向到端口 443。 我们可以看到,我们已使用我们创建的负载均衡器的 Amazon 资源名称 (ARN) 附加了此侦听器。 我们现在可以使用以下代码定义 HTTPS 侦听器:

resource "aws_lb_listener" "https-listener" {
    load_balancer_arn = aws_lb.application-lb.arn
    port = 443
    protocol = "HTTPS"
    certificate_arn = data.aws_acm_certificate.
                      issued_certificate.arn
    default_action {
        target_group_arn = aws_lb_target_group.target-
            group.arn
        type = "forward"
    }
}

在这里,我们可以看到我们接受 HTTPS 流量,然后将 HTTP 流量转发到我们使用目标组的 ARN 定义的目标组。 我们还可以看到我们已将其中一个证书附加到侦听器。 然而,这并不涵盖我们所有的 URL 组合。 请记住,我们还有另一份证书要附加。 我们可以使用以下代码附加我们的第二个证书:

resource "aws_lb_listener_certificate" "extra_certificate" {
  listener_arn = "${aws_lb_listener.https-listener.arn}"
  certificate_arn =
      "${data.aws_acm_certificate.raw_cert.arn}"
}

这种联系应该很容易理解。 在这里,我们仅引用 HTTPS 侦听器的 ARN 和我们要附加的证书的 ARN。 我们现在已经定义了负载均衡器资源所需的一切。 然而,交通又如何呢? 我们已经定义了负载均衡器、EC2 实例以及负载均衡器的 HTTPS 路由。 但是,是什么阻止某人直接连接到 EC2 实例,完全绕过负载均衡器和 HTTPS? 这就是安全组的用武之地。在下一节中,我们将通过创建安全组来锁定流量,以便用户无法绕过我们的负载均衡器。

创建安全组来锁定和保护流量

安全组本质上是防火墙。 我们可以定义进出已实施安全组的资源的流量。 流量规则可以是细粒度的。 单个安全组可以有多个规则来定义流量的来源(甚至是特定的 IP 地址)和协议。 当谈到我们的安全组时,我们将需要两个。 人们将接受来自世界任何地方的所有 IP 的 HTTP 和 HTTPS 流量。 这将用于我们的负载均衡器,因为我们希望我们的应用程序可供所有人使用。 另一个安全组将由我们的 EC2 实例实现; 该组会阻止除第一个安全组之外的所有 HTTP 流量。 我们还将启用 SSH 入站流量,因为我们需要 SSH 进入服务器来部署应用程序,从而为我们提供以下流量布局:



这是您必须小心在线教程的地方。 YouTube 视频和 Medium 文章并不缺乏,只需点击一下即可启动并运行负载均衡器。 但是,他们将 EC2 实例暴露在外,并且不去探索安全组。 即使有了这一部分,我们仍将暴露数据库。 我这样做是因为这是一个在“问题”部分提出的好问题。 然而,我在这里强调它是因为你需要被警告它已经暴露了。 锁定数据库的方法将在本章的答案部分中介绍。 当涉及到我们的安全组时,我们可以在deployment/security_groups.tf文件中定义它们。 我们可以使用以下代码从负载均衡器安全组开始:

resource "aws_security_group" "alb-security-group" {
    name = "to-do-LB"
    description = "the security group for the
                   application load balancer"
    ingress {
        . . .
    }
    ingress {
        . . .
    }
    egress {
        . . .
    }
    tags = {
        name: "to-do-alb-sg"
    }
}

在这里,我们在入口标签下有两个入站规则,在出口标签下有一个出站规则。 我们的第一个入站规则是允许来自任何地方的 HTTP 数据,代码如下:

ingress {
    description = "http access"
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
}

全零的 cidr 块意味着来自任何地方。

我们的第二个入站规则是来自任何地方的 HTTPS 流量。 您认为这将如何定义? 可以用下面的代码来定义:

ingress {
    description = "https access"
    from_port = 443
    to_port = 443
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
}

现在我们的出站规则。 我们必须允许所有流量和协议离开负载均衡器,因为它们来自我们的资源。 这可以通过以下代码来实现:

egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
}

必须注意的是,from_port 和 to_port 为零,这意味着我们允许来自所有端口的传出流量。 我们还将协议设置为 -1,这意味着我们允许所有协议作为传出流量。 我们现在已经为负载均衡器定义了安全组。 现在,我们可以继续使用以下代码为 EC2 实例定义安全组:

resource "aws_security_group" "webserver-security-group" {
    name = "to-do-App"
    description = "the security group for the web server"
    ingress {
        . . .
    }
    ingress {
        . . .
    }
    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
    tags = {
        name: "to-do-webserver-sg"
    }
}

出站规则将与负载均衡器相同,因为我们希望将数据返回到可请求它的任何地方。 当涉及到我们的 HTTP 入站规则时,我们只想使用以下代码接受来自负载均衡器的流量:

ingress {
    description = "http access"
    from_port = 80
    to_port = 80
    protocol = "tcp"
    security_groups = ["${aws_security_group.
                          alb-security-group.id}"]
}

在这里,我们可以看到,我们依赖于负载均衡器的安全组,而不是定义 cidr 块。 现在所有的用户流量都已经定义好了,我们只需要定义用于部署的 SSH 流量即可,代码如下:

ingress {
    description = "SSH access"
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
}

在这里,我们通过端口 22 访问 SSH。这将使我们能够通过 SSH 访问我们的服务器并部署我们的应用程序。 几乎所有事情都完成了。 我们只需将 EC2 实例附加到 EC2 安全组,可以使用以下代码完成:

resource "aws_network_interface_sg_attachment"
    "sg_attachment_webserver" {
         count = length(aws_instance.production_server)
             security_group_id = aws_security_group.
                 webserver-security-group.id
  network_interface_id = aws_instance.
                         production_server[count.index].
                         primary_network_interface_id
}

我们的 Terraform 脚本现已完成,将能够在锁定流量的同时启动多个 EC2 实例、数据库和负载均衡器。 如果您想获得一个带有 HTTPS 的基本 Web 应用程序并以有限的流量访问数据库,那么对于其他项目来说,这是一个很好的模板。

现在我们有两个不同的 EC2 实例,我们将必须更改部署脚本,以便在下一部分中都安装应用程序。

更新多个 EC2 实例的 Python 部署脚本

我们可以通过多种方法来优化部署过程,例如同时运行多个进程。 我们拥有的 EC2 实例越多,这将加快您的部署速度。 然而,我们必须记住,这是一本关于 Rust 和 Web 编程的书,其中包含一些部署章节,以便您可以使用您创建的内容。 我们可以写一本关于优化和改进部署管道的整本书。 当涉及到在deployment/run_build.py 文件中支持多个 EC2 实例时,在文件末尾,我们只需使用以下代码循环遍历输出中的全局 IP 列表:

for server_ip in data["ec2_global_ips"]["value"][0]:
    print("waiting for server to be built")
    time.sleep(5)
    print("attempting to enter server")
    build_process = Popen(f"cd {DIRECTORY_PATH} && sh
        ./run_build.sh
                            {server_ip} {args.u} {args.p}",
                            shell=True)
    build_process.wait()

就是这个。 现在支持多个服务器。 在这里,我们可以看到将 Python 文件中的部署数据管理背后的逻辑与用于在单个服务器上部署应用程序的单个 Bash 脚本分开的有效性。 保持事物隔离可以降低技术债务,从而轻松进行重构。 现在,我们所有的代码基础设施都已完成! 我们可以运行这个 Python 脚本并将我们的构建部署到 AWS 上。 一切都快完成了; 我们所要做的就是在下一节中将 URL 连接到负载均衡器。

将我们的 URL 附加到负载均衡器

这是本垒打。 我们终于接近尾声了。 我感谢您坚持阅读本章,因为它不像 Rust 编码那么令人兴奋。 但是,如果您想使用 Rust 服务器,这一点很重要。 要将我们的 URL 连接到负载均衡器,我们必须导航到您的 URL 的托管区域。 到达那里后,单击“创建记录”按钮。 在“创建记录”屏幕中,如果您不使用向导,请单击“创建记录”屏幕右上角的“切换到向导”链接以获取以下向导视图:


在这里,我们可以看到一系列奇特的流量路由方式。 但是,我们将仅选择简单路由,因为我们只需将流量传递到负载均衡器,负载均衡器正在 EC2 实例之间分配流量。 选择“简单路由”后,我们需要填写以下表格:


在这里,我们可以看到我选择了应用程序别名和经典负载均衡器来将流量路由到。 然后我选择了负载均衡器所在的位置。 这给了我一个下拉列表,我可以在其中看到要选择的 ToDoApplicationLb 负载均衡器。 单击“定义简单记录”按钮后,您将导航到要创建的记录列表。 我们再次执行创建向导过程,以考虑所有带有通配符的前缀,然后确认我们的记录创建。 这样,我们的 HTTPS 现在可以与我们的应用程序配合使用,如下所示:



至此,我们的章节就完成了。 如果您尝试直接通过 IP 地址访问我们的 EC2 实例之一,您将被阻止。 因此,我们无法直接访问我们的 EC2 实例,但可以通过我们的 URL 通过 HTTPS 访问它们。 如果您向用户提供 URL 的任何变体,甚至是该 URL 的 HTTP 链接,您的用户将很乐意通过 HTTPS 协议使用您的应用程序。

概括

目前,我们已经完成了在 AWS 上部署强大且安全的应用程序所需的所有工作,并强制执行了 HTTPS 和锁定流量。 我们已经介绍了很多内容,您在本章中获得的技能组合可以应用于您想要在 AWS 上部署的几乎任何其他项目(如果您可以将其打包在 Docker 中)。 您现在了解了 HTTPS 的优势以及实现 HTTPS 协议以及将 URL 映射到服务器或负载均衡器的 IP 地址所需的步骤。 更重要的是,我们使用 Terraform 提供的强大数据查询资源,自动将使用证书管理器创建的证书附加到 Terraform 中的负载均衡器。 最后,当我们设法使用 HTTPS 且仅使用 HTTPS 访问我们的应用程序时,这一切就完成了。 我们不仅开发了一些在未来许多项目中有用的实用技能,而且还探索了 HTTPS 和 DNS 工作原理的本质,让我们更深入地理解和欣赏当我们输入 URL 时互联网通常如何工作 到浏览器。

在下一章中,我们将探讨 Rocket 框架。 由于我们在 Actix Web 应用程序中构建 Rust 模块的方式,我们将能够直接从 Actix Web 应用程序中提取模块并将其插入 Rocket 应用程序中。 考虑到我们在本章中所做的工作,我们还可以将 Rocket 应用程序包装在 Docker 中,并将其放入此处的构建管道中,只需更改部署 docker-compose 文件中的一行代码即可。 在下一章中,您将亲眼目睹,当一切都结构良好且隔离时,更改功能和框架不会令人头疼,事实上,相当令人愉快。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表