14.5 保持配置属性的私密性

14.5 保持配置属性的私密性

Config Server提供的大多数配置可能并不是私密的。但是,我们可能需要ConfigServer提供一些包含敏感信息的属性,比如密码或安全token,在后端仓库中,它们最好保持私密。

Config Server提供了两种方式来支持私密的配置属性。

  • 在Git存储的属性文件中使用加密后的值。
  • 使用HashiCorp Vault作为Config Server的后端存储,补充(或替代)Git。

我们将会依次看一下这两种方案是如何与Config Server组合使用保证配置属性私密性的。首先,我们看一下如何在Git后端中写入加密的属性。

14.5.1 在Git中加密属性

除了提供非加密值以外,Config Server也可以借助存储在Git中的属性文件提供加密值。处理存储在Git中的加密数据的关键在于一个秘钥(key),即加密秘钥(encryption key)。

为了启用加密属性功能,我们使用一个加密秘钥来配置Config Server,在将属性值提供给客户端应用之前,Config Server要使用这个秘钥对属性值进行解密。Config Server支持对称秘钥和非对称秘钥。要设置对称秘钥,我们可以在ConfigServer自己的配置中将encrypt.key属性设置为加密和解密秘钥的值:

1
2
encrypt:
key: s3cr3t

很重要的一点需要注意,这个属性要设置到bootstrap配置中(例如,bootstrap.properties或bootstrap.yml)。这样的话,在自动配置功能启用Config Server之前,这个属性就会加载和启用。

为了更加安全一些,我们可以让Config Server使用非对称的RSA秘钥对或引用一个keystore。要创建这样的秘钥,我们可以使用keytool命令行工具:

1
2
3
keytool -genkeypair -alias tacokey -keyalg RSA \
-dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" \
-keypass s3cr3t -keystore keystore.jks -storepass l3tm31n

这样形成的keystore会写入到名为keystore.jks的文件中。我们可以将这个keystore.jks文件放到文件系统中或者放到应用本身。不管使用哪种方式,我们都需要在Config Server的bootstrap.yml文件中配置keystore的位置和凭证信息。

注意:为了在Config Server中使用加密功能,我们需要要安装JavaCryptography Extensions Unlimited Strength策略文件。参见Oracle的JavaSE页面了解详细信息。

例如,假设我们要将keystore打包到应用本身,将其放到类路径的根目录下,那么我们可以配置如下的属性,让Config Server使用该keystore:

1
2
3
4
5
6
encrypt:
key-store:
alias: tacokey
location: classpath:/keystore.jks
password: l3tm31n
secret: s3cr3t

秘钥和keystore就绪之后,我们需要对某些数据进行加密。Config Server暴露了一个“/encrypt”端口会帮助我们实现该功能。我们需要做的就是提交一个POST请求到“/encrypt”端点,其中包括要加密的数据。例如,我们要加密连接至MongoDB数据库的密码。借助curl,我们可以按照如下的方式加密密码:

1
2
$ curl localhost:8888/encrypt -d "s3cr3tP455w0rd"
93912a660a7f3c04e811b5df9a3cf6e1f63850cdcd4aa092cf5a3f7e1662fab7

在提交POST请求之后,我们会接收到一个加密的值作为响应。接下来,需要做的就是复制这个值并粘贴到Git仓库托管的配置文件中。

为了设置MongoDB,在Git仓库的application.yml文件中添加spring.data.mongodb. password属性:

1
2
3
4
spring:
data:
mongodb:
password: '{cipher}93912a660a7f3c04e811b5df9a3cf6e1f63850...'

需要注意,spring.data.mongodb.password被一个单括号(’)括了起来,并且带有{cipher}前缀。这样就会告诉Config Server,这是一个加密的值,而不是简单的未加密值。

在将这个变更提交并推送到Git仓库中的application.yml文件之后,Config Server就可以对外提供加密的属性了。如果要实际看一下,就使用curl命令伪装成ConfigServer的客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ curl localhost:8888/application/default | jq
{
"name": "app",
"profiles": [
"prof"
],
"label": null,
"version": "464adfd43485182e4e0af08c2aaaa64d2f78c4cf",
"state": null,
"propertySources": [
{
"name": "http://localhost:10080/tacocloud/tacocloudconfig/
application.yml",
"source": {
"spring.data.mongodb.password": "s3cr3tP455w0rd"
}
}
]
}

我们可以看到,spring.data.mongodb.password的值是以解密后的形式提供的。默认情况下,Config Server提供的所有加密值只是在后端Git仓库中处于加密的状态,它们在对外提供之前会解密。这意味着,消费这个配置的客户端应用并不需要任何特殊的代码和配置就能接收Git中已加密的属性。

如果你想要让Config Server以未解密的形式对外提供加密属性,那么可以将spring.cloud.config.server.encrypt.enabled属性设置为false:

1
2
3
4
5
6
7
8
spring:
cloud:
config:
server:
git:
uri: http://localhost:10080/tacocloud/tacocloud-config
encrypt:
enabled: false

这样导致的结果就是Config Server在提供所有的属性值的时候完全按照Git仓库设置的样子进行发送,包括已加密的属性值。我们再次伪装成一个客户端,利用curl命令展示禁用解密的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ curl localhost:8888/application/default | jq
{
...
"propertySources": [
{
"name": "http://localhost:10080/tacocloud/tacocloudconfig/
application.yml",
"source": {
"spring.data.mongodb.password": "{cipher}AQA4JeVhf2cRXW..."
}
}
]
}

当然,如果客户端接收到了未解密的属性值,那么客户端需要自行解密。

尽管可以通过Config Server在Git中保存已加密的私密信息,但是我们可以看到加密并不是Git的原生特性。它需要我们自己对写入Git仓库的数据进行加密。另外,除非将解密的任务推给Config Server的客户端应用,否则对于任何请求配置的客户端,Config Server API对外提供的私密信息都是解密之后的形式。我们接下来看一下另一个Config Server后端方案,它能够只向已授权的用户提供私密信息。

14.5.2 在Vault中存储私密信息

HashiCorp Vault是一个私密管理工具。这意味着与Git相比,Vault的核心特性就是原生地处理私密信息。对于敏感的配置数据,Vault是一个更有吸引力的ConfigServer后端支撑方案。

为了开始使用Vault,我们需要参考Vault Web站点的安装指南下载并安装vault命令行工具。在本小节中,我们将会使用vault命令管理私密信息和启动Vault服务器。

启动Vault服务器

在使用Config Server写入和对外提供私密信息之前,我们需要启动一个Vault服务器。对于我们来讲,最简单的方式就是在开发模式下使用如下的命令启动服务器:

1
2
3
$ vault server -dev -dev-root-token-id=roottoken
$ export VAULT_ADDR='http://127.0.0.1:8200'
$ vault status

第一条命令会在开发模式下启动一个Vault服务器,其中根token(root token)的ID为roottoken。顾名思义,开发模式意味着它是一个更简单但并不完全安全的Vault运行时。它不应该在生产环境中使用,但是在开发的工作流程中,这种使用Vault的方式会非常便利。

注意:Vault是一个功能完备且健壮的私密管理工具。除了开发模式下的简单使用之外,本章没有足够的篇幅完整介绍Vault服务器的运行。我强烈建议你在尝试生产环境中使用Vault之前,通过阅读Vaul文档来更详细地了解Vault。

对Vault服务器的所有访问都需要向服务器提供一个token。根token是一个管理token,这意味着除了其他功能之外,它允许我们创建其他的token。它还能够用于读取和写入私密信息。如果在开发模式启动服务器的时候未指定根token,那么Vault服务器会为我们创建一个token并在启动的时候写入日志中。为了便于使用,建议将根token设置成一个易于记忆的值,比如roottoken。

开发模式的服务器启动之后,它将会监听本地机器的8200端口。所以,要让vault命令行知道Vault服务器在什么地方,设置VAULT_ADDR环境变量是非常重要的,这也是上述代码片段第二行所做的事情。

最后,vault status命令会校验之前的两条命令是否已经按照预期运行。你大致会看到描述Vault服务器的6个属性,包括Vault是否密闭(在开发模式下,它不应该处于密闭状态)。

使用Vault 0.10.0或之后的版本的话,Vault与Config Server协作使用之前还有其他的两条命令需要执行。Vault运行方式的一些变更会导致一个标准的私密后端与Config Server不兼容。以下两个命令会重新创建名为secret的后端,以兼容Config Server:

1
2
$ vault secrets disable secret
$ vault secrets enable -path=secret kv

如果使用更早版本的Vault,就不需要这些步骤。

写入私密信息到Vault中

借助vault命令,可以很容易将私密信息写入Vault中。例如,假设我们想要将访问MongoDB的密码(也就是spring.data.mongodb.password)存储到Vault中,而不是存储到Git里面,就可以通过vault命令完成:

1
$ vault write secret/application spring.data.mongodb.password=s3cr3t

图14.6拆分了vault write命令,阐述每个组成部分在将私密信息写入Vault的过程中扮演了什么角色。

image-20211021180046275

图14.6 通过vault命令将私密信息写入Vault

现在,我们最需要关注的就是私密信息的路径、key和值。私密信息的路径就像文件系统中的路径那样,允许我们将相关的私密信息放到一个给定的路径中,而将其他的私密信息放到不同的路径中。路径的前缀“secret/”用来识别Vault后端,在这里使用了一个key-value的后端,名为“secret”。

私密信息的key和值是我们实际要写入Vault的内容。当Config Server要对外提供已写入的私密信息时,很重要的一点在于私密信息的key要和配置属性保持一致。

我们可以使用vault read命令校验私密信息是否已经写入Vault中:

1
2
3
4
5
$ vault read secret/application
Key Value
--- -----
refresh_interval 768h
spring.data.mongodb.password s3cr3t

在将私密信息写入到指定路径的时候,需要注意每次往给定路径中写入时都会覆盖之前在该路径下写入的私密信息。例如,假设我们还想要往Vault的上述路径中写入MongoDB用户名,我们不能简单地写入spring.data.mongodb.usernamesecret私密信息本身,如果这样做就会导致spring.data.mongodb.password私密信息丢失。我们需要同时将这两个属性写进去:

1
2
3
% vault write secret/application \
spring.data.mongodb.password=s3cr3t \
spring.data.mongodb.username=tacocloud

现在,我们已经往Vault中写入了一些私密信息。接下来,我们看一下如何让Vault作为Config Server的后端属性源。

在Config Server中启用Vault后端

为了将Vault添加为Config Server的后端,我们至少需要将Vault添加为激活的profile。在Config Server的application.yml文件中,将会如下所示:

1
2
3
4
5
spring:
profiles:
active:
- vault
- git

如上所示,vault和git profile均处于激活状态,允许Config Server同时从Vault和Git获取配置。一般而言,我们会将敏感的配置属性写入Vault,对于不需要私密性的属性则继续使用Git作为后端。如果你希望将所有配置都写到Vault中或者没有必要使用Git后端,那么可以将spring.profiles.active设置为vault,完全放弃Git后端。

默认情况下,Config Server会假定Vault运行在localhost并监听8200端口。但是,我们可以在Config Server的配置中修改这种默认行为,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
config:
server:
git:
uri: http://localhost:10080/tacocloud/tacocloud-config
order: 2
vault:
host: vault.tacocloud.com
port: 8200
scheme: https
order: 1

Config Server对Vault的默认假定都可以通过spring.cloud.config.server.vault.*相关的属性重写。在这里,我们告诉Config Server,Vault的API可以通过https://vault.tacocloud.com:8200 来访问。

注意,我们保留了Git配置,假定Vault和Git分担了提供配置相关的职责。order属性表明Vault提供的私密属性要优先于Git提供的属性。

在配置完Config Server使用Vault作为后端之后,我们可以使用curl命令伪装成一个客户端尝试一下:

1
2
3
4
5
6
7
8
[habuma:habuma]% curl localhost:8888/application/default | jq
{
"timestamp": "2018-04-29T23:33:22.275+0000",
"status": 400,
"error": "Bad Request",
"message": "Missing required header: X-Config-Token",
"path": "/application/default"
}

噢,不!似乎出现问题了。实际上,这个错误表明Config Server提供来自Vault的私密信息,但是请求中没有包含Vault token。

很重要的一点需要注意,对Vault的所有请求都要包含一个X-Vault-Token头信息。我们不会在Config Server本身中配置这个token,而是让每个Config Server客户端在向Config Server发送请求的时候在请求中包含X-Config-Token头信息。Config Server会接收到X-Config-Token头信息,然后将其转换成发送给Vault的X-Vault-Token头信息。

我们可以看到,因为在请求中缺少这个token,所以Config Server拒绝提供任何属性,甚至连Git中的属性都不可用了,因为在暴露私密的信息之前需要一个token。这是组合使用Vault和Git的一个有趣的副作用,除非提供一个合法的token,否则连Git属性都会被Config Server间接隐藏。

我们可以再尝试一下,在请求中添加一个X-Config-Token头信息:

1
2
$ curl localhost:8888/application/default
-H"X-Config-Token: roottoken" | jq

请求中的这个X-Config-Token头信息应该会产生更好的结果,响应中将会包含我们写入到Vault中的私密信息。这里给出的token是在我们以开发模式启动Vault的时候设置的根token,但实际上Vault服务器创建的所有合法、未过期且具有访问Vault私密后端的token都是可以的。

在Config Server客户端设置Vault token

显然,在每个微服务中,我们不能使用curl来指定消费Config Server属性的token。相反,我们应该在服务应用的本地配置中添加一点配置信息:

1
2
3
4
spring:
cloud:
config:
token: roottoken

spring.cloud.config.token属性会告诉Config Server客户端在每次向ConfigServer发送请求的时候都要带上给定的token。这个属性必须设置到应用的本地配置中(而不能存放到Config Server的Git或Vault中),Config Server才能够将其传递到Vault上,从而访问私密属性。

写入特定应用和特定profile的私密信息

在为Config Server提供服务的时候,写入application路径的属性适用于所有的应用,不管它们的名字是什么。如果我们想要写入针对给定应用的私密属性,就需要将路径中的application部分改成应用的名称。例如,如下的vault write命令会为名为ingredient-service的应用(通过其spring.application.name属性指定)写入专有的私密信息:

1
2
$ vault write secret/ingredient-service \
spring.data.mongodb.password=s3cr3t

类似的,如果我们不指定profile,写入Vault的私密信息就会成为默认profile属性的一部分。也就是说,不管哪个profile处于激活状态,客户端都能收到这些私密信息。我们可能想要将私密信息写入到特定的profile中,如下所示:

1
2
3
% vault write secret/application,production \
spring.data.mongodb.password=s3cr3t \
spring.data.mongodb.username=tacocloud

这种方式写入的私密信息只对激活profile为production的应用有效。