NGINX.COM
Web Server Load Balancing with NGINX Plus

在我们关于如何保护传输中和存储中 SSL 密钥和证书的系列博客的前两篇中,我们讨论了如何使用 HashiCorp Vault 和硬件安全模块 (HSM) 等工具来保护 NGINX 磁盘上的 SSL 密钥和证书数据:

在许多情况下,只要采取额外的安全措施来管理对这些证书的访问,就可以将 SSL 证书数据存储在磁盘上的风险控制在可以接受的范围之内。但是在一些用例中,还需要确保所有与安全相关的组件都不在磁盘上,只存储在内存中并且只能从内存中进行访问。两个最常见的用例是两类有着更高安全性要求的环境,一种是静态存储处于风险中,另一种是系统是临时的(例如容器)或者 SSL 证书和密钥本身是临时的。

在这篇文章中,我们将关注后一个用例:短生命周期的 SSL 证书密钥对。我们使用 HashiCorp Vault 来签发临时 SSL 证书,并将它们存储在 NGINX Plus 键值存储中——这是一个内存数据库。

NGINX Plus R18 及更高版本支持强大的架构,可提供安全的 SSL 密钥管理,因为 SSL 证书密钥对可加载到内存中,并通过 NGINX Plus 键值存储中的值等变量进行访问。通过结合使用 NGINX Plus、基于键值存储的 SSL 密钥存储和管理,以及 HashiCorp Vault,您可通过将敏感的临时 SSL 数据存放在键值存储中而不是磁盘上,从而为 SSL 证书创建安全的环境。

除签发 SSL 证书以外,HashiCorp Vault 还具有许多管理安全数据的功能。在主要使用临时证书来保障传输中数据的场景,并且这些证书或使用它们的系统会经常轮换的情景中,我们可将 Vault 用作证书颁发机构 (CA)。在这篇文章中,通过配置和使用 Vault,我们重点介绍了如何将实时 Vault 证书签发请求与 NGINX Plus 中的动态证书加载功能相集成。

 

先决条件

本文基于以下假设:

  • 您对 Vault 有一定的了解
  • 您已安装并配置 Vault,Vault 已作为服务运行并解封(请参阅 Vault 文档
  • 您对 Vault 实例拥有管理员级别的访问权限
  • 您在相应的实例上配置了以下环境变量:

    • Vault 实例:

      • VAULT_ADDR=https://127.0.0.1:8200
      • VAULT_EXT=https://externally_accessible_Vault_IP_address:8200
      • VAULT_TOKEN=initial_root_token
    • SSL 管理实例:

      • VAULT_EXT=https://externally_accessible_Vault_IP_address:8200
      • VAULT_APP_TOKEN=NGINX_role_token

 

架构设计

在最基本的示例部署中,需要先使用某种类型的 SSL 请求/发布管理工具向 Vault 请求临时证书,然后再将它们加载到 NGINX Plus 键值存储中。在本示例中,我们使用简单的 curl 命令来模拟 SSL 请求/发布工具。就测试而言,无论 Vault 和 NGINX Plus 是安装在同一系统上,还是安装在不同系统上,抑或是安装在容器中,都可以实现;唯一的要求是用来请求证书的工具(在这里是 curl)以及将它们加载到 NGINX Plus 中的工具(也是 curl)能够通过 HTTPS 与 Vault 和 NGINX Plus 进行通信。

相关架构如下图所示,其中:

  1. 请求/发布工具通过 API 调用向 Vault 请求新的临时 PEM
  2. 该工具通过 NGINX Plus API 将临时 PEM 数据写入内存键值存储中,但不写入磁盘中
  3. HTTPS 客户端向使用临时证书的 NGINX Plus 请求 https://www.example.com

 

将 HashiCorp Vault 配置为临时证书的 CA

通过两大步将 Vault 配置为临时证书的 CA:

将 Vault 配置为 CA

出于测试目的,我们使用 Vault 的 CA 功能来生成临时证书并进行签名。这允许我们将 Vault 用作这些临时证书的一站式端点,但如果您的架构需要不同的工具,也可按需进行调整。

如要将 Vault 用作 CA,我们首先要配置其公钥基础设施 (PKI) 存储,以生成并签发新的临时证书和密钥。当从 localhost 实例配置 Vault 时,以下命令适用。

为 NGINX Plus 证书请求启用 PKI

首先,通过为 NGINX Plus 请求定义自定义端点并验证配置来启用 PKI 支持。

# vault secrets enable -path pki/nginx-plus-ephem-certs pki
# vault secrets list

创建 CA

我们在 Vault 中新建一个 CA,仅用于我们的临时证书请求。也可以不新建 CA,而是将现有 CA 证书导入 Vault 中;有关如何导入 CA 详细信息的说明,或关于 PKI 端点创建选项的信息,请参阅 Vault 文档

我们生成了一个有效期为一周(168 小时)的 CA,并且为了安全起见,将 JSON 格式的 CA 证书密钥对写入文件中:

# vault write -format=json pki/nginx-plus-ephem-certs/root/generate/internal common_name="Example\ Company" ttl=168h > NGINX-Plus-Ephem-CA.json

请注意,并非必须将输出保存到本地 JSON 文件中。在开发环境中或高度安全的环境中,最好重新生成 CA,而非将 CA 的任何详细信息保存在可访问磁盘中。您可以不存储生成的 JSON,或将 CA 详细信息回存到 Vault 的安全密钥存储中。请参阅 Vault 文档,了解如何使用密钥存储,或参考我们的博文使用 HashiCorp Vault 保护 NGINX 中的 SSL 私钥

在 CA 上配置端点和角色

将 Vault 配置为本地 CA 后,即可通过 Vault 签发临时证书。首先,我们配置 Vault 通过面向 NGINX Plus 实例的证书吊销列表 (CRL) 端点支持请求。

# vault write pki/nginx-plus-ephem-certs/config/urls issuing_certificates="$VAULT_EXT/v1/pki/nginx-plus-ephem-certs/ca" crl_distribution_points="$VAULT_EXT/v1/pki/nginx-plus-ephem-certs/crl"

现在我们为 NGINX Plus 实例(并且只为它们)创建一个分配给 Vault 的角色,该角色允许这些实例向 CA 请求新证书。配置新角色时,您可以按角色定义证书要求。例如,NGINX 证书请求角色有以下两种选择:一是强制证书请求需包含明确的域名,二是允许为已针对本地 CA 签名的任何公用名 (CN) 生成证书。在我们的用例中,两个示例角色都支持证书中的“主题备用名称” (SAN) 字符串。

注意:以下命令仅为示例,每条命令都会有安全影响,在应用于生产环境之前需进行检查。

将证书请求限制到特定子域并允许 SAN 条目:

# vault write pki/nginx-plus-ephem-certs/roles/nginx-cert-requests allowed_domains=example.com allow_subdomains=true allow_ip_sans=true allow_alt_names=true key_bits=2048 max_ttl=72h no_store=true

或者,允许任何 CN 和 SAN 条目:

# vault write pki/nginx-plus-ephem-certs/roles/nginx-cert-requests allow_any_name=true allow_ip_sans=true allow_alt_names=true key_bits=2048 max_ttl=72h no_store=true

为 NGINX Plus 实例创建只读策略

最后,我们向 Vault 添加 HashiCorp 配置语言 (HCL) 策略,该策略只允许特定类型的证书请求。我们不建议共享 Vault 根访问令牌或将其用于日常操作,所以我们专为与此策略相关的 NGINX Plus 生成了访问令牌。

首先,我们通过创建包含下列内容的文件 nginx-cert-requests.hcl,并将其保存在 /etc/vault 目录中,为新 PKI 端点定义 HCL 策略:

path "pki/nginx-plus-ephem-certs/issue/*" {
    capabilities = ["create","update"]
}

还有更多选项可用于进一步限制对 PKI 端点的访问,但在此示例中,我们只是授予了请求新证书的权限。

接下来,我们将策略加载到 Vault 中:

# vault policy write nginx-cert-requests /etc/vault/nginx-cert-requests.hcl

现在我们为这个新的只读策略创建一个唯一访问令牌,用于为我们的 NGINX Plus 实例请求临时证书:

# vault token create -policy=nginx-cert-requests

我们将这个命令返回的令牌的值保存到请求证书系统上的名为 VAULT_APP_TOKEN 的环境变量中。该令牌可用于通过 SSL 管理工具向 Vault 请求新的 PEM 数据,并将 SSL 数据加载到键值存储中。

在高安全的环境中,您可能希望给只读令牌设置一个短生命周期 (TTL),以便反复再生成和再分发令牌。有关令牌创建的更多详细信息和配置选项,请参阅 Vault 文档

我们现在已准备好向安全的 Vault CA 请求新的临时证书。为了验证我们能否请求新证书,我们将在系统上运行以下命令,发出请求命令并将生成的证书加载到键值存储中。该命令将必要的证书请求信息传递给 Vault(例如 CN、SAN 和持续时间),Vault 以 JSON 格式生成证书和密钥有效负载。这是我们在下面的 API 调用中以更易读的格式提交给 Vault 的 JSON:

{
  "common_name":"www.example.com",
  "ip_sans":"10.10.1.1,192.168.76.76",
  "alt_names":"dev.example.com,eng.example.com",
  "ttl":"1h",
  "format":"pem_bundle"
}
# curl -ks --header "X-Vault-Token: $VAULT_APP_TOKEN" -X POST -d '{"common_name":"www.example.com","ip_sans":"10.10.1.1,192.168.76.76","alt_names":"dev.example.com,eng.example.com","ttl":"1h","format":"pem_bundle"}' $VAULT_EXT/v1/pki/nginx-plus-ephem-certs/issue/nginx-cert-requests

生成的 JSON 中包含 SSL 证书、密钥以及与 CA 相关的数据。鉴于我们配置 Vault 的方式,这些证书数据只以 JSON 格式存在,Vault 不会存储这些数据。就测试而言,这就是我们所期望的格式,但如果您需要保存 SSL 数据以进行手动测试,则可以将输出数据传输到 jq 等工具中。

 

为 SSL 存储配置 NGINX Plus 键值存储

NGINX Plus 可通过内存键值存储存储和读取 SSL 证书和密钥,这允许其直接从内存中加载证书,而通过非磁盘。此外,NGINX Plus 可根据实时会话数据,例如服务器名称指示 (SNI) 主机或其他条件,使用不同的证书。

如要将 NGINX Plus 配置为直接从内存中加载 SSL 数据,则使用 “data:” 将参数作为前缀添加到 ssl_certificatessl_certificate_key 指令中。该前缀会指示 NGINX Plus 将字符串理解为原始证书密钥 PEM 内容,字符串可直接从内存密钥值存储中作为变量提供。

这种设计消除了从磁盘存储库存储和分发平面文件证书和密钥的必要性,而且还意味着它们不再存储在部署镜像和备份中,也无法在部署映像中进行访问等。

在以下示例中,我们构建了具有以下功能的 NGINX Plus 配置:

  • 名为 vault_ssl_pem 的键值存储,可用于保存证书密钥 PEM 字符串组合。
  • 证书会在建立连接时基于传入请求的 SNI 主机头值进行匹配(举例来说,仅当主机名称等于配置的密钥时,例如 www.example.com,才从键值存储中加载证书)。
  • SSL PEM 数据被加载到绑定端口 443 的 server 块中。
  • 启用调试日志记录(测试用),以便在访问日志中捕获用于每个连接的完整 PEM 字符串。(我们不建议在生产中使用这种配置,因为它会生成大量数据并可能引发安全风险。)
  • NGINX Plus API 端点已配置为仅接受 HTTPS 流量流向 /api 位置。

在名为 ssl_keyval.conf/etc/nginx/conf.d 中,使用下列内容创建配置文件:

log_format vault_ssl_keyval '$remote_addr [$time_local] - '
                            'ssl_server_name:"$ssl_server_name" '
                            'host:"$host" ';

keyval_zone zone=vault_ssl_pem:1m;
keyval $ssl_server_name $certificate_pem zone=vault_ssl_pem;

server {
    listen 443 ssl;
    access_log /var/log/nginx/vault-ssl-keystore-access.log vault_ssl_keyval;
    error_log  /var/log/nginx/vault-ssl-keystore-error.log debug;

    # Load PEMs from variable. Note the 'data:' prefix.
    ssl_certificate     data:$certificate_pem;
    ssl_certificate_key data:$certificate_pem;

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }
}

server {
    listen 8443 ssl;
    access_log /var/log/nginx/status-api-access.log api;
    error_log  /var/log/nginx/status-api-error.log notice;

    ssl_certificate     /etc/nginx/ssl/nginx-ssl.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx-ssl.key;

    location /api {
        api write=on;
        if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|PATCH|DELETE)$) {
            return 405;
        }
    }
}

 

将 SSL PEM 数据加载到 NGINX Plus 键值存储中

现在我们可通过 NGINX Plus API 将 PEM 数据直接加载到 vault_ssl_pem 键值存储中。

请注意,我们已将 NGINX Plus API 设置为只读。我们强烈建议在加载 SSL 密钥等敏感数据时,为 NGINX Plus API 启用 SSL 和身份验证。这样,就能在端口 8443 上通过 HTTP 访问 NGINX Plus API,如下所示。

我们可通过运行下列指令,将 www.example.com 的 PEM 数据(或者仅加载 SSL PEM 密钥的第一行,如本示例所示)加载到键值存储中,测试 NGINX Plus 配置是否正确。NGINX_Plus_instance 是进行 NGINX Plus API 配置的 NGINX Plus 实例的主机名称。

# curl -s -X POST -d '{"www.example.com":"-----BEGIN RSA PRIVATE KEY-----\n..."}' https://NGINX_Plus_instance:8443/api/6/http/keyvals/vault_ssl_pem

然后我们通过 NGINX Plus API 查询 vault_ssl_pem 键值存储,以确认 PEM 证书和密钥数据是否为与 www.example.com 密钥相关的值:

# curl https://NGINX_Plus_instance:8443/api/6/http/keyvals/vault_ssl_pem
{"www.example.com":"-----BEGIN RSA PRIVATE KEY-----\n..."}

此时,我们已将 NGINX Plus 配置为对于指向端口 443 上名为 www.example.com 的服务的所有 HTTPS 连接从内存键值存储中动态检索 PEM 证书和密钥数据。

如要移除与 www.example.com 密钥相关的 SSL PEM 数据,可向 NGINX Plus API 发起 PATCH 调用,且入参为空:

# curl -s -X PATCH -d '{"www.example.com":null}' https://NGINX_Plus_instance:8443/api/6/http/keyvals/vault_ssl_pem

 

将 Vault CA 证书请求与 NGINX Plus 键值存储相集成

请求新的临时证书/密钥 PEM 并将其加载到 NGINX Plus 键值存储中

最后一步是向 Vault 请求临时 SSL 密钥证书 PEM 包,并将其加载到 NGINX Plus 键值存储中。在处理临时证书或增强的安全环境时,理想的做法是请求新的 PEM 数据并将其加载到内部键值存储中,以避免将任何数据存放在磁盘上。要想实现这一点,我们只需将上述示例中对 Vault 的 PEM 请求与对 NGINX Plus API 的密钥值 POST 请求组合到一个命令中。我们可以从 SSL 管理工具或其他可访问 Vault API 和 NGINX Plus API 的中心位置运行该命令。

# echo "{\"www.example.com\":$( curl -ks --header "X-Vault-Token: $VAULT_APP_TOKEN" -X POST -d '{"common_name":"www.example.com","ip_sans":"10.10.1.1,192.168.76.76","alt_names":"dev.example.com,eng.example.com","ttl":"1h","format":"pem_bundle"}' $VAULT_EXT/v1/pki/nginx-plus-ephem-certs/issue/nginx-cert-requests | jq '.data | "\(.certificate)"' )}" | curl -ks -X POST -d @- https://NGINX_Plus_instance:8443/api/6/http/keyvals/vault_ssl_pem

其中:

  • echo 命令将字符串传递给 NGINX Plus API
  • 第一个 curl 命令向新建的 Vault CA 请求 JSON 格式的密钥证书
  • jq 命令只从 Vault CA 生成的响应中提取 PEM 数据
  • 最后一个 curl 命令向 NGINX Plus API 发起 POST 调用,将 PEM 数据插入 vault_ssl_pem 键值存储中

正如在上一节中所述,我们查询 vault_ssl_pem 键值存储,以确认键值存储中的证书密钥 PEM 数据是否为与 www.example.com 密钥相关的值。

# curl https://NGINX_Plus_instance:8443/api/6/http/keyvals/vault_ssl_pem
{"www.example.com":"-----BEGIN RSA PRIVATE KEY-----…"}

将新的 PEM 数据字符串加载到 www.example.com 域的键值存储后,指向适用 NGINX Plus 服务的所有新 HTTPS 连接都将使用该证书和密钥来保护连接。我们运行下列命令来测试这是否有效:

# curl https://www.example.com

假设键值存储中具有与 www.example.com 密钥对应的有效 PEM 数据,则 HTTPS 请求成功。您也可以使用除 www.example.com 以外的其他内容来测试与键值存储中 PEM 数据的 SNI 匹配是否正常运行,例如在面向 NGINX Plus 实例的 curl 命令中使用与 www.example.com 名称相对应的 IP 地址。这会导致 SSL 握手失败,您可以在错误日志 (/var/log/nginx/vault-ssl-keystore-error.log) 中查看。

# curl https://IP-address-for-www.example.com

更新和吊销证书

如需更改、吊销或移除与主机相关的 PEM 数据,则可遵循上述类似方法,使用 API PATCH 方法更新键值存储中与 www.example.com 相关的值。如果是定期(例如每天)轮换 SSL 证书或者需要撤销证书时,这可能是一个很好的用例。

如要向 Vault 请求新证书以更新键值存储中与 www.example.com 相关的值,首先运行一个与上节中的命令相类似的命令,然后在第二个 curl 命令中将 HTTP 方法从 POST 更改为 PATCH,以通过 API 将更新的 PEM 包推送到键值存储中:

# echo "{\"www.example.com\":$( curl -ks --header "X-Vault-Token: $VAULT_APP_TOKEN" -X POST -d '{"common_name":"www.example.com","ip_sans":"10.10.1.1,192.168.76.76","alt_names":"dev.example.com,eng.example.com","ttl":"1h","format":"pem_bundle"}' $VAULT_EXT/v1/pki/nginx-plus-ephem-certs/issue/nginx-cert-requests | jq '.data | "\(.certificate)"' )}" | curl -ks -X PATCH -d @- https://NGINX_Plus_instance:8443/api/6/http/keyvals/vault_ssl_pem

如要彻底移除或撤销与 www.example.com 相关的值,则使用 PATCH 且入参为空:

# curl -s -X PATCH -d '{"www.example.com":null}' https://NGINX_Plus_instance:8443/api/6/http/key-vals/vault_ssl_pem

 

结语

确认一切都按预期运行后,可将这些命令编写到 Vault 和 NGINX Plus API 中,以根据需要轮换证书。您还可将此工作流添加到用于 SSL 证书和密钥管理、NGINX Plus 部署或 NGINX Plus 键值存储管理的任何 CI/CD 流水线。也可以向 Vault 提供更多证书请求详细信息,例如唯一 CN 和 SAN 指令,以生成仅支持特定服务名称以及其他指定条件的证书。这种架构只允许特定系统在内存中生成和存储定制的临时证书,从而避免将内容写入磁盘中。

对于允许将 SSL 密钥存储在磁盘上的部署,请参阅该系列博客的其他两篇文章:

如欲试用 NGINX Plus,请立即下载 30 天免费试用版,或与我们联系以讨论您的用例

Hero image
免费 O'Reilly 电子书:
《NGINX 完全指南》

更新于 2022 年,一本书了解关于 NGINX 的一切

关于作者

Alan Murphy

高级产品经理

Alan is a technology communications leader and systems architect with a unique background in system engineering and architecture, application delivery, content marketing, and technology evangelism. He has provided global tech evangelism, analyst, press, and channel support throughout NA, EMEA, and APAC, with a particular focus on the country-by-country application delivery networking, cloud, and security markets in the Japan, ASEAN, and ANZ regions.

关于 F5 NGINX

F5, Inc. 是备受欢迎的开源软件 NGINX 背后的商业公司。我们为现代应用的开发和交付提供一整套技术。我们的联合解决方案弥合了 NetOps 和 DevOps 之间的横沟,提供从代码到用户的多云应用服务。访问 nginx-cn.net 了解更多相关信息。