之前一篇文章介绍了 Hashicorp Vault 的解封/密封算法。这篇打算记录一下这个软件的其它方面。当前最新版本为 v0.7.2 。
Vault 是一个相当复杂的系统,总括而言,它是一个集中管理各类敏感信息(如密码/Key/证书等等)的软件(服务)。Vault 采用类似 Unix “一切皆文件” 的方式组织及暴露各类信息:所有操作都是对某个路径的 read/write (实际上是对某个 HTTP 路径 POST/GET/LIST/DELETE 等) ,例如:
/sys
目录下是各种配置路径,此目录不可被卸载,其下路径各有用途,如/sys/seal
和/sys/unseal
这两个路径是用来密封/解封 vault 的。/secret
目录下挂载的是 Generic Secret Backend,用于存放一般用途的敏感信息,其下路径组织结构由用户自行决定,我们平时实际使用访问最多的应该也是这个。- … 更多目录路径说明可参看 API 文档
读取/写入的数据一般都是 JSON 格式。
Backends
Vault 主要由几类 backends 合作组成:
Auth -> Secret -> Storage(Physical) -> Audit
认证 -> 实际操作 -> 落盘储存 -> 日志
Auth(entication) Backend
Auth Backend
完成的是认证工作:访问者是谁。有多种可通过挂载添加,默认情况下它们会挂载在
auth/<type>
下,以下是其中一些认证方式:
- token 这个 Backend 是 Vault 的核心认证方式,默认挂载,不可卸载,非常重要,下一小节详细描述
- userpass 用户名密码认证
- github 用 Github 的认证服务
- cert 用 tls 证书认证
- approle TODO
Token (令牌)
对外部而言,顾名思义,有了令牌就能通行,它是访问者身份的象征;实际上,Vault
对外 API 中绝大部分(除了像 seal/unseal 这种)都需要令牌才能访问:访问的
HTTP 请求头部需要加上 X-Vault-Token: xxxxxx
(命令行其实也是调用 HTTP API 的,Token 保存在 ~/.vault_token
里以供其使用)
对内部而言,Token 是 Vault 里多个组件间的结合点,关联对应许多信息:
- 树状层次:除了 root tokens 和 orphan tokens 外都有父 token,故所有 tokens 形成一个树(森林)状层次结构,当父 token 被吊销时,其所有子孙 tokens 都会同时被吊销
- 限时:token 是有使用时限的(TTL),超时时会被吊销,可以续租延长时限的(但不超过某个最大值),也可以是周期性的 token(可通过周期性地续租无限延长使用时间)
- 限次:token 可以限制使用次数,默认是不限制,超过使用次数后会被吊销,这个可以用作实现 one-time-password
- 访问控制策略(Access Control Policies):这是最关键的,每个 token
都关联一个访问控制策略列表(如
["default", "dev"]
),访问控制策略就是 Vault 里头的权限控制机制,例如default
策略是这样的:
$ vault policies default # 查看名为 default 的控制策略
# 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"]
}
...
归功于“一切皆文件”的统一设计,能干什么不能干什么(create/read/update/delete/list/sudo/deny)都统一到一组路径上的读写权限。详情可参看 https://www.vaultproject.io/docs/concepts/policies.html
整个流程应该是这样子的:用户(人/机器)携带着令牌调用某个 API,Vault 检查这个令牌是否已被吊销,是否超过使用限制,访问的操作/路径是否被允许,如果都通过了这些检查,才会实际执行操作。
那么好了,令牌本身又是从哪里来的呢?方法有几个:
- root tokens 可以在 Vault 初始化的时候获得,也可以在之后通过
vault generate-root
命令或 /sys/generate-root API 创建新的(需要 unseal key),root token 是 token 中的 superuser,啥都可以做,且一般没有超时时间 - 通过
vault token-create
命令或 /auth/token/create API 为当前 token 手动创建子 token,子 token 的访问控制策略只能是当前 token 的子集 - 其它 auth backends 完成对认证后,其实返回的也是 token(所以从这个角度看,Vault 其实就只有 token 一种认证方式),至于返回的是什么样的 token,则决定于这个 auth backend 的配置是怎么样的;例如 Github 可以关联某个 team 或者某个 user 到指定访问控制策略,当这个 team 的成员或这个用户用 Github 的 Personal Access Token 完成认证后,就返回关联此策略的一个 token
关于 token 的更详细文档可以参看这里: https://www.vaultproject.io/docs/concepts/tokens.html
Secret Backend
用来存放(生成)秘密的地方;不同的 backend 类似于不同的虚拟文件系统(tmpfs,udev 等),可以挂载到不同的目录,读写到不同的 backend 下会有各自不同的作用,如:
- generic 前文也提及过,一般用途的 backend
- database 用来管理数据库连接信息,具体参看另一篇博文
- pki 用来管理(生成) X.509 证书 TODO
- cubbyhole
类似于 generic secret backend 可以存放任意秘密,但不同之处在于它是 token
scoped 的
- 解释:举个类比,很多 Unix 系统有一个文件夹
/dev/fd
,像一个私有的空间,每一个进程访问都只能看到本进程的打开了的文件描述符;cubbyhole 类似于此,每一个 token 能访问到的都是一个只有自己能访问到的私有空间,当这个 token 销毁的时候,存储在这里面的所有秘密也一并被销毁 - 用处:例如我们从 Vault 中读取了一些秘密,这个秘密可能会通过很多中间环节才能送到最终的使用者手上,中间环节越多,泄漏的可能性就越大(例如不小心被 log 下来了等),那么可以通过创建一个寿命非常短(例如几秒)且只能用一次的 token,把密码放到此 token 的 cubbyhole 里,中间环节传输的则是这个 token,即使泄漏了,由于其限时限次,秘密泄漏的可能性就大大降低了,同时如果泄漏了,日志里面必定会有所记录(偷取访问一次/正常访问一次,第二次失败),这是所谓 Response Wrapping;其实这不就有点像 OAuth2 的流程一样,拿一个很短命的 code 去换取 Access Token (秘密)
- https://www.hashicorp.com/blog/cubbyhole-authentication-principles
- https://www.vaultproject.io/docs/secrets/cubbyhole/index.html
- 解释:举个类比,很多 Unix 系统有一个文件夹
Storage (Physical) Backend
数据真正落盘的地方(注:到达 Storage Backend 时,数据都已经处于加密状态了,安全并不依赖于 Storage backend,实际上它是 untrusted 的,下面代码里有提到),需要在服务启动时在配置里面指定;这类 backend 同样有很多种,例如:
- inmem 存放在内存中(dev 模式下即是使用 inmen backend 的,一旦重启,数据就丢失了)
- file 直接存放在本地文件里
- mysql 存放在 MySQL 数据库里
- consul/etcd 存放在 Key-Value 集群中(高可用)
例如使用 mysql backend,vault init
后可以看到数据库里头实际上创建了一个表而已,此表就两个字段 vault_key
和 vault_value
:
vault_key
是形如sys/policy/default
logical/12345678-1234-1234-1234-123456790abc/ca
等等这类内部路径名称。注意:路径是没有加密的,所以 Vault 的文档某个地方(我忘了在哪里了)指出不要把敏感信息暴露在路径上vault_value
大部分是加密后的二进制数据,也有些是明文的,如core/seal-config
记录密封算法跟参数:
{"type":"shamir","secret_shares":5,"secret_threshold":3,"pgp_keys":null,"nonce":"","backup":false,"stored_shares":0}
这部分信息是在没解封前就要用到的,所以只能用明文;我尝试了下改了参数,例如把
secret_threshold
改成 1
看看能不能只要一个 key 就能解封,结果…当然是不能啦 :-)
看了一下相关的代码,发现其实这部分很好理解,实际上只要提供以下这个 interface 就能用作 storage backend 了(其实就是一个可以枚举的 Key-Value store 即可):
// Backend is the interface required for a physical
// backend. A physical backend is used to durably store
// data outside of Vault. As such, it is completely untrusted,
// and is only accessed via a security barrier. The backends
// must represent keys in a hierarchical manner. All methods
// are expected to be thread safe.
type Backend interface {
// Put is used to insert or update an entry
Put(entry *Entry) error
// Get is used to fetch an entry
Get(key string) (*Entry, error)
// Delete is used to permanently delete an entry
Delete(key string) error
// List is used ot list all the keys under a given
// prefix, up to the next prefix.
List(prefix string) ([]string, error)
}
注意这里提到的 “As such, it is completely untrusted, …”
Audit Backend
记日志的,对于敏感信息,日志会进行 HMAC-SHA256 哈希,这样做可以避免暴露明文但仍然能进行对照检查;另外如果启动了至少一个 audit backends 的话,对 Vault 的每一个请求都会阻塞直到其中一个日志完成记录,这样就能保证没有任何的操作不被记录下来