策略——授权
Vault 模拟了一个文件系统,Vault 中所有的信息,包括机密、配置等,都是依照各自的路径来使用的。使用 Vault 策略,我们可以使用声明式的语法来赋予或者禁止对特定路径的特定操作。 Vault 的策略默认情况下是拒绝一切访问的,所以一个空的策略不会赋予对系统的任何访问权限。
策略——授权流程
在一个人类用户或是应用程序访问 Vault 之前,管理员必须首先为其配置相应的身份认证方式。身份认证方式是 Vault 通过验证人类用户或者应用程序提供的某种信息来确认其身份以及相应权限的过程。
参考下面的图,演示了一个安全团队配置 Vault 使用 LDAP 或是活动目录作为 Vault 的身份认证方式的流程:
- 安全团队为 Vault 配置身份认证方式,具体的配置项随不同的认证方式而不同。在本例中使用的是 LDAP,Vault 需要知晓 LDAP 的服务的地址,以及是否使用 TLS。需要特别说明,Vault本身并不会保存一份 LDAP 的数据副本,而是将身份认证工作代理给 LDAP 服务。
- 安全团队编写一个策略(或是复用已有策略)授予对 Vault 中特定路径的访问权限。策略使用 HCL 语法编写,保存在本地磁盘上。
- 上传策略,保存在 Vault 存储中,每个策略都有一个名字,通过策略名引用策略。可以将策略名理解成指向策略定义的规则集合的指针,或是符号连接。
- 最重要的是,安全团队将身份认证方式中的数据与一个策略关联起来。例如,安全团队可以创建这样一个关联:
名为“dev”的 OU(Organization Unit)组的成员关联名为“readonly-dev”的Vault策略
或者:
名为“ops”的 OU(Organization Unit)组的成员关联名为“admin”和“auditor”的 Vault 策略
现在 Vault 内部有了一个身份认证系统与内部策略的关联关系。当一个用户请求 Vault 认证身份时,实际的认证工作被代理给了相关的身份认证方式,流程如下图:
- 一个用户试图使用它的 LDAP 凭证通过 Vault 的身份认证,它向 Vault 提交了它的 LDAP 用户名和密码
- Vault 建立与 LDAP 服务的连接,请求 LDAP 服务认证它收到的凭证。假设认证成功,LDAP 服务返回用户相关信息,包括用户所属的
OU
组 - Vault 使用前面叙述的安全团队预先配置的的关联关系,将 LDAP 返回的结果映射到相关策略。Vault 生成一个客户端令牌(Token),并将策略附加到该令牌上
- Vault 向客户端返回令牌。该令牌已被赋予正确的策略,一如安全团队先前设定的那样
用户可以使用 Vault 令牌执行后续的操作。如果用户重新执行身份认证操作,他会得到一个新的令牌。这个新的令牌拥有与老令牌一样的权限,但实际的令牌不同。重新执行身份认证不会吊销老令牌。
策略语法
策略使用 HCL 或是 JSON 语法编写,描述了一个人类用户或是应用程序允许访问 Vault 中哪些路径。
一个简单的例子,赋予对 secret/foo
路径的读权限:
path "secret/foo" {
capabilities = ["read"]
}
当这个策略被附加到一个令牌后,该令牌可以读取 secret/foo
,然而无法修改或删除 secret/foo
,因为没有授予其相关能力(Capability)。由于策略系统是默认拒绝的,所以令牌在Vault中没有其他权限。
另一个更加丰富的策略,包含注释:
# This section grants all access on "secret/*". Further restrictions can be
# applied to this broad policy, as shown below.
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Even though we allowed secret/*, this line explicitly denies
# secret/super-secret. This takes precedence.
path "secret/super-secret" {
capabilities = ["deny"]
}
# Policies can also specify allowed, disallowed, and required parameters. Here
# the key "secret/restricted" can only contain "foo" (any value) and "bar" (one
# of "zip" or "zap").
path "secret/restricted" {
capabilities = ["create"]
allowed_parameters = {
"foo" = []
"bar" = ["zip", "zap"]
}
}
策略基于路径匹配来验证一个请求所需要的能力。一个策略路径可以精准匹配一个确切的路径,或者可以使用 *
模式指定前缀匹配:
# Permit reading only "secret/foo". An attached token cannot read "secret/food"
# or "secret/foo/bar".
path "secret/foo" {
capabilities = ["read"]
}
# Permit reading everything under "secret/bar". An attached token could read
# "secret/bar/zip", "secret/bar/zip/zap", but not "secret/bars/zip".
path "secret/bar/*" {
capabilities = ["read"]
}
# Permit reading everything prefixed with "zip-". An attached token could read
# "secret/zip-zap" or "secret/zip-zap/zong", but not "secret/zip/zap
path "secret/zip-*" {
capabilities = ["read"]
}
另外,路径当中可以使用 +
代表路径中一个段内任意长度的字符(从 Vault 1.1 开始支持):
# Permit reading the "teamb" path under any top-level path under secret/
path "secret/+/teamb" {
capabilities = ["read"]
}
# Permit reading secret/foo/bar/teamb, secret/bar/foo/teamb, etc.
path "secret/+/+/teamb" {
capabilities = ["read"]
}
我们说过,Vault 模拟了一个文件系统,所有的操作都对应了一个路径,以及对应的能力,即使是 Vault 内部的核心配置信息也挂载于 sys/
路径下。策略可以定义一个令牌对这些路径和能力的访问权限。
Vault 采用一组具有优先级的判定规则来决定最为具体的路径匹配。如果一个匹配模式被多个策略使用并能匹配上给定路径,Vault 会取其能力的并集。如果一个路径能被多个策略定义的不同的匹配模式所匹配,那么只有最高优先级的匹配会被采用。
假设对给定路径 P
,存在两条策略都能够匹配,它们的路径匹配模式分别是 P1
和 P2
,Vault 采用如下优先级规则:
- 如果
P1
中第一个+
或是*
出现的位置早于P2
,那么采用P2
- 如果
P1
以*
结尾,而P2
不是,那么采用P2
- 如果
P1
包含更多的+
段,那么采用P2
- 如果
P1
更短,那么采用P2
- 如果
P1
得到的字典序更小,那么采用P2
举个例子,给定两个路径:secret/*
和 secret/+/+/foo/*
,由于第一个通配符的位置相同(都在 secret/
之后),并且都以 *
结尾,而后者拥有更多的通配符段(多了两个 +/
段),所以结果是使用 secret/*
路径模式的策略。
需要注意的是,*
与正则表达式中的同符号并不同义,Vault仅允许 *
出现在模式的末尾。
如果赋予了 list
能力,需要注意的是因为 list
操作总是作用于一个路径前缀上,所以策略定义的路径匹配模式必须使用前缀匹配(即以 *
结尾)。
能力(Capabilites)
除了路径匹配模式以外,每条规则都必须指定一个或多个能力来定义细颗粒度的允许-禁止规则。能力永远以一个字符串列表的形式定义,哪怕只有一个能力。
需要注意的是,我们在下面列出能力的同时,也会给出该能力相对应的 HTTP 动词。当编写策略时,可以先查阅相关 HTTP API 文档了解路径信息以及相关 HTTP 动词,然后映射到策略定义中的能力。虽然映射关系并不是严格的 1:1,但它们通常非常相似地匹配。
create
(POST/PUT
)——允许在指定路径创建数据。只有很少的Vault部件会区分create
和update
,所以大多数操作同时需要create
以及update
能力。需要区分二者的部分会在相关文档中说明。read
(GET
)——允许读取指定路径的数据update
(POST/PUT
)——允许修改指定路径的数据。对多数Vault部件来说,这隐含了在指定位置创建初始值的能力delete
(DELETE
)——允许删除指定路径的数据list
(LIST
)——允许罗列指定路径的所有值。要注意的是,经由list
操作返回的键是未经策略过滤的。请勿在键名中编码敏感信息。不是所有后端都支持list
操作
下面的能力并无对应的 HTTP 动词:
sudo
——允许访问需要根权限保护的路径。除非拥有sudo
能力,否则令牌被禁止与这些路径交互(对这种路径的操作可能同时需要其他能力,例如read
或delete
) 例如,修改审计日志后端配置就需要令牌具有sudo
特权。deny
——不允许访问。该定义总是优先于其他能力定义,包括sudo
,只要能力中存在deny
,就不允许执行任何操作
需要注意的是,上述的能力映射到的是 HTTP 动词而非实际底层执行的操作,这通常会使人感到困惑。举个例子,通过 Vault 的数据库机密引擎创建一个数据库用户名密码,底层实际执行的操作是创建,但对应的 HTTP 动词却是 GET
,所以要在对应路径上配置 read
能力。
细颗粒度控制
除却以上标准的能力,Vault 还提供了对指定路径的更细颗粒度的权限控制。与路径相关联的功能优先于对参数的控制。
参数限制
需要首先说明,Version 2
的 kv
机密引擎不支持参数限制,所以以下例子均假设 secret/
路径挂载的是 Version 1
的 kv
机密引擎。
Vault 中的数据以键值对的形式表达:key=value
。Vault 策略可以进一步限制对指定路径下特定键和值的访问。这些可选的细颗粒度控制项有:
required_parameters
—— 一组必须指定的参数:
# This requires the user to create "secret/foo" with a parameter named
# "bar" and "baz".
path "secret/foo" {
capabilities = ["create"]
required_parameters = ["bar", "baz"]
}
上述例子中,用户想要在 secret/foo
下创建数据,必须包含名为 bar
和 baz
的参数。
allowed_parameters
—— 针对指定路径允许操作的键值对的白名单。值为空白列表则代表允许使用任意值:
# This allows the user to create "secret/foo" with a parameter named
# "bar". It cannot contain any other parameters, but "bar" can contain
# any value.
path "secret/foo" {
capabilities = ["create"]
allowed_parameters = {
"bar" = []
}
}
上述例子允许在 secret/foo
下创建键为 bar
,值为任意值的数据。
给定非空列表值则意味着只能使用列表中限定的值:
# This allows the user to create "secret/foo" with a parameter named
# "bar". It cannot contain any other parameters, and "bar" can only
# contain the values "zip" or "zap".
path "secret/foo" {
capabilities = ["create"]
allowed_parameters = {
"bar" = ["zip", "zap"]
}
}
上述例子允许在 secret/foo
下创建键为 bar
,值为 zip
或 zap
的数据。
如果指定了任意的键,那么所有未被指定的键都会被拒绝,除非同时指定了 *
参数为空列表,这就允许修改其他任意键数据。被指定的键仍然受到给定的值列表的限制:
# This allows the user to create "secret/foo" with a parameter named
# "bar". The parameter "bar" can only contain the values "zip" or "zap",
# but any other parameters may be created with any value.
path "secret/foo" {
capabilities = ["create"]
allowed_parameters = {
"bar" = ["zip", "zap"]
"*" = []
}
}
上述例子中,只限制对 bar
的值必须是 zip
或 zap
,对其他键的值则没有任何限制,可以创建任意键。
重要的一点是,使用 *
可能会造成意外的后果:
# This allows the user to create or update "secret/foo" with a parameter
# named "bar". The values passed to parameter "bar" must start with "baz/"
# so values like "baz/quux" are fine. However, values like
# "baz/quux,wibble,wobble,wubble" would also be accepted. The API that
# underlies "secret/foo" might allow comma delimited values for the "bar"
# parameter, and if it did, specifying a value like
# "baz/quux,wibble,wobble,wubble" would result in 4 different values getting
# passed along. Seeing values like "wibble" or "wobble" getting passed to
# "secret/foo" might surprise someone that expected the allowed_parameters
# constraint to only allow values starting with "baz/".
path "secret/foo" {
capabilities = ["create", "update"]
allowed_parameters = {
"bar" = ["baz/*"]
}
}
在上面的例子中,我们限制对 secret/foo
只能写入键为 bar
的数据,并且值必须以 baz/
为前缀。比如 bar=baz/quux
这样的数据就是合法的。问题是,我们也可以把值设置成 baz/quux,wibble,wobble,wubble
,Vault 会接纳这种带有分隔符的值,而这样的值可能会被应用程序解析为长度为 4 的列表,这样的话我们可能会惊讶地发现即使我们限制了 bar
的值必须以 baz/
为前缀,仍然读取到了诸如 wibble
这样的值。
denied_parameters
—— 键值对的黑名单,优先级高于allowed_parameters
。
设置值为空列表会导致拒绝对对应键的任意修改。
# This allows the user to create "secret/foo" with any parameters not
# named "bar".
path "secret/foo" {
capabilities = ["create"]
denied_parameters = {
"bar" = []
}
}
上面的例子禁止在 secret/foo
下创建键为 bar
的任意键值对。
如果对 denied_parameters
赋值一个非空列表,会导致禁止参数的值包含列表中的任意元素:
# This allows the user to create "secret/foo" with a parameter named
# "bar". It can contain any other parameters, but "bar" cannot contain
# the values "zip" or "zap".
path "secret/foo" {
capabilities = ["create"]
denied_parameters = {
"bar" = ["zip", "zap"]
}
}
上述例子禁止设置 secret/foo
的 bar
的值为 zip
或是 zap
。
设置 denied_parameters
的键为 *
,则禁止操作任意键:
# This allows the user to create "secret/foo", but it cannot have any
# parameters.
path "secret/foo" {
capabilities = ["create"]
denied_parameters = {
"*" = []
}
}
如果 denied_parameters
配置了任意键,那么默认所有未被指定的键都是允许操作的,除非另有显式的 allowed_parameters
配置。
参数限制中的值也支持前缀与后缀表示:
path "secret/foo" {
capabilities = ["create"]
allowed_parameters = {
"bar" = ["foo-*"]
}
}
上面的例子规定对 secret/foo
,只能创建键为 bar
,值以 foo-
为前缀的键值对。
path "secret/foo" {
capabilities = ["create"]
allowed_parameters = {
"bar" = ["*-foo"]
}
}
而上面的例子则是限制值必须以 -foo
为后缀。
限制响应封装的有效期
我们在前文中介绍过Vault的响应封装机制。我们可以在策略中使用相关参数限制客户端可以申请的响应封装的有效期时限,精确到秒。我们可以通过 s
、m
或是 h
后缀来代表秒、分钟和小时。
在实践中,为特定路径指定值为一秒的 min_wrapping_ttl
可以达到强制必须以响应封装的形式返回相应路径数据的目的。
min_wrapping_ttl
——客户端可以指定的响应封装有效期的最小值,如果设置该值,则强制必须以响应封装的形式返回相应路径的数据max_wrapping_ttl
——允许设置的响应封装有效期的最大值。
# This effectively makes response wrapping mandatory for this path by setting min_wrapping_ttl to 1 second.
# This also sets this path's wrapped response maximum allowed TTL to 90 seconds.
path "auth/approle/role/my-role/secret-id" {
capabilities = ["create", "update"]
min_wrapping_ttl = "1s"
max_wrapping_ttl = "90s"
}
上面的例子限制了用户只能以响应封装的形式返回 auth/approle/role/my-role/secret-id
的值,并且响应封装的有效期最大为 90 秒。
当两个参数同时被指定时,最小值必须小于最大值。另外如果当不同区块的路径合并时,最低值会被采纳,目的是使得令牌的有效期尽可能的短。
内建策略
Vault 有两个内建策略:default
和 root
。本节来讨论下这两个内建策略。
Default策略
default
策略是一个无法删除的 Vault 内建策略。默认情况下,它被附加到所有令牌上,但可以通过使用身份验证方式创建令牌时显式排除之。
该策略包含了基础的功能,例如准许令牌查询有关自身的数据以及使用 Cubbyhole 数据。然而,Vault 并不限制该策略的内容,你可以根据需要修改它。Vault 永远不会覆盖您的设置。如果您想把 default
策略同步到最新的 Vault 版本的默认值,只要用新版 Vault 执行 vault server -dev
启动一个测试服务,读取它的 default
策略内容,然后写回到原来的 Vault 服务的 default
策略即可:
$ vault read sys/policy/default
Key Value
--- -----
name default
rules # Allow tokens to look up their own properties
path "auth/token/lookup-self" {
capabilities = ["read"]
}
# Allow tokens to renew themselves
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Allow tokens to revoke themselves
path "auth/token/revoke-self" {
capabilities = ["update"]
}
# Allow a token to look up its own capabilities on a path
path "sys/capabilities-self" {
capabilities = ["update"]
}
# Allow a token to look up its own entity by id or name
path "identity/entity/id/{{identity.entity.id}}" {
capabilities = ["read"]
}
path "identity/entity/name/{{identity.entity.name}}" {
capabilities = ["read"]
}
# Allow a token to look up its resultant ACL from all policies. This is useful
# for UIs. It is an internal path because the format may change at any time
# based on how the internal ACL features and capabilities change.
path "sys/internal/ui/resultant-acl" {
capabilities = ["read"]
}
# Allow a token to renew a lease via lease_id in the request body; old path for
# old clients, new path for newer
path "sys/renew" {
capabilities = ["update"]
}
path "sys/leases/renew" {
capabilities = ["update"]
}
# Allow looking up lease properties. This requires knowing the lease ID ahead
# of time and does not divulge any sensitive information.
path "sys/leases/lookup" {
capabilities = ["update"]
}
# Allow a token to manage its own cubbyhole
path "cubbyhole/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Allow a token to wrap arbitrary values in a response-wrapping token
path "sys/wrapping/wrap" {
capabilities = ["update"]
}
# Allow a token to look up the creation time and TTL of a given
# response-wrapping token
path "sys/wrapping/lookup" {
capabilities = ["update"]
}
# Allow a token to unwrap a response-wrapping token. This is a convenience to
# avoid client token swapping since this is also part of the response wrapping
# policy.
path "sys/wrapping/unwrap" {
capabilities = ["update"]
}
# Allow general purpose tools
path "sys/tools/hash" {
capabilities = ["update"]
}
path "sys/tools/hash/*" {
capabilities = ["update"]
}
# Allow checking the status of a Control Group request if the user has the
# accessor
path "sys/control-group/request" {
capabilities = ["update"]
}
创建令牌时排除 default
策略:
$ vault token create -no-default-policy
或是调用 API:
$ curl \
--request POST \
--header "X-Vault-Token: ..." \
--data '{"no_default_policy": "true"}' \
https://vault.hashicorp.rocks/v1/auth/token/create
根策略
root
策略是一个无法删除也无法修改的 Vault 内建策略。任何关联了该策略的用户都将是根用户。根用户可以在 Vault 内执行任意操作,强烈建议在生产环境中使用 Vault 前首先吊销所有的根令牌。
每当 Vault 服务被首次初始化时,都会创建一个根用户。这个根用户是用来执行 Vault 的初始化配置的。配置完成后,应创建并使用由细颗粒度策略约束的用户并启用身份认证方式,然后吊销根令牌。
要吊销根令牌可以使用命令行:
$ vault token revoke "<token>"
或是通过 HTTP API:
$ curl \
--request POST \
--header "X-Vault-Token: ..." \
--data '{"token": "<token>"}' \
https://vault.hashicorp.rocks/v1/auth/token/revoke
管理策略
您可以使用任意编辑器编写策略,策略文件可以采用 HCL 语法或是 JSON 语法。保存策略文件本地磁盘后,需要上传策略文件到 Vault 服务,随后才能使用。
枚举策略
通过命令行列出所有已注册的策略:
$ vault read sys/policy
或是通过 HTTP API:
$ curl \
--header "X-Vault-Token: ..." \
https://vault.hashicorp.rocks/v1/sys/policy
创建策略
使用命令行上传并创建策略:
$ vault policy write policy-name policy-file.hcl
使用 HTTP API:
$ curl \
--request POST \
--header "X-Vault-Token: ..." \
--data '{"policy":"path \"...\" {...} "}' \
https://vault.hashicorp.rocks/v1/sys/policy/policy-name
这两个例子里,策略的名称都是 policy-name
。您可以把策略名理解成指向策略规则的指针。令牌通过策略名关联相关策略规则。
更新策略
更新已有策略内容与创建策略操作一致,使用的是已有的策略名。使用命令行:
$ vault write my-existing-policy updated-policy.json
或者使用 HTTP API:
$ curl \
--request POST \
--header "X-Vault-Token: ..." \
--data '{"policy":"path \"...\" {...} "}' \
https://vault.hashicorp.rocks/v1/sys/policy/my-existing-policy
删除策略
通过命令行删除一个已有策略:
$ vault delete sys/policy/policy-name
或是通过 HTTP API:
$ curl \
--request DELETE \
--header "X-Vault-Token: ..." \
https://vault.hashicorp.rocks/v1/sys/policy/policy-name
删除是一个幂等操作。删除一个不存在的策略不会导致 Vault 返回错误。
关联策略
Vault 可以通过身份认证方式登录时自动在令牌上关联一组策略,相关配置随具体的身份认证方式类型而不同。简单起见,我们演示一下 Vault 内建的 userpass
认证方式。
Vault 管理员或是安全团队成员可以用如下命令行创建一个关联了一组策略的用户:
$ vault write auth/userpass/users/sethvargo \
password="s3cr3t!" \
policies="dev-readonly,logs"
如此便创建了名为 sethvargo
的用户,通过用户名密码如果成功过认证了身份,Vault 会返回一个附加了 dev-readonly
和 logs
策略的令牌。
用户可以用命令行执行身份认证,获取令牌:
$ vault login -method="userpass" username="sethvargo"
Password (will be hidden): ...
如果用户名密码正确,Vault 会创建一个令牌,将预设的策略附加在令牌上,然后返回给用户。
创建令牌时附加策略
可以在通过命令行创建令牌时关联策略:
$ vault token create -policy=dev-readonly -policy=logs
子令牌可以关联一组父令牌拥有的策略的子集。根用户可以关联任意策略。
一旦令牌被签发,其关联的策略无法再被修改。必须吊销旧令牌并申请新令牌才能得到更新后的关联策略。
然而,令牌关联的策略内容是实时解析的,也就是说,如果更新了策略内容,附加此策略的令牌下次的请求就会按照新策略内容进行权限检查。