POSTS2017

Vault 介绍

vault

之前一篇文章介绍了 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

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_keyvault_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 的每一个请求都会阻塞直到其中一个日志完成记录,这样就能保证没有任何的操作不被记录下来