VaultとConsulを連携する (2) Consulをsecret backendとして使う

# Vaultのsecret backendとしてのConsul

[先の記事](http://qiita.com/yunano/items/bfaac7868ce4fb946ee5)ではVaultのstorage backendにConsulを利用する方法を記述した。
こちらの記事では[Consulをsecret backendとする方法](http://vaultproject.io/docs/secrets/consul/index.html)を記述する。

なお、storage backendとsecret backendとには特に何の連携もないので、secret backendとしてConsulを使ったからといってstorage backendもConsulにしないといけないとか、またはその逆にstorage backendにConsulを使っているからといってsecret backendにもConsulを使わなければならないとか、なんてことはまったくない。

この記事はVaultのバージョンは0.1.2、Consulのバージョンは0.5.1で書き始めた。

## Consul側の準備

Consulをsecret backendとした場合、VaultがConsul ACLの一時トークンの発行と無効化を行うことが可能になる。
ただしConsul側で設定しないとACL機能は有効化されていないため、まずこれを有効化する必要がある。

Consul側でACL機能を有効化するためには、Consulサーバの設定ファイルでacl_datacenter項にデータセンタ名を設定する。
データセンタ名には[接続されているデータセンタ](https://www.consul.io/docs/guides/datacenters.html)のうちどれかを指定するが、自分以外のどことも接続されていなければ自分のデータセンタ名を指定する。
自分のデータセンタ名はdatacenter項に設定した値であり、省略時は"dc1"である。
ちなみに、実際にACL機能が有効になるのはConsulサーバのうち、acl_datacenter項が設定されたノードがleaderになった時、のようだ。

Consulの設定ファイルはここでは /etc/consul.d/agent.json とする。

```json:/etc/consul.d/agent.json
{
  (前略)
  "acl_datacenter": "dc1",
  "acl_default_policy": "deny",
  "acl_master_token": "root",
  (後略)
}
```

今回acl_default_policy項とacl_master_token項も設定している。

acl_default_policy項は許可も禁止もされていない操作が行われた場合に許可するか禁止するかを設定する項目であり、"deny"だと禁止、"allow"だと許可する。
デフォルト値は"allow"であるが、ここはACL機能をより理解するためのチャレンジとして"deny"にしてみた。

acl_master_token項はこのConsulサーバのマスタトークン、つまり何でもできるACLトークンの名前を指定する。
このACLトークンは存在しなければConsulサーバの起動時に作成されるので、あらかじめ手動で作成するなどの操作は必要ない。

Consulクライアント側ではACL機能を有効化するために設定する項目はないが、トークンの発行を行うノードではacl_datacenter項にConsulサーバと同じ設定値を指定する必要がある。
今回の目的はVaultによるConsul ACLの一時トークンの発行と無効化なので、Vaultのsecret backendの接続先Consulノード（説明は後述）がConsulサーバではなくConsulクライアントである場合、その接続先Consulクライアントの設定ファイルにもacl_datacenter項を追加してやる必要がある。

```json:/etc/consul.d/agent.json
{
  (前略)
  "acl_datacenter": "dc1",
  (後略)
}
```

ACLはキーバリューストアの読み書きとサービスの登録に影響する。
[先の記事](http://qiita.com/yunano/items/bfaac7868ce4fb946ee5)ではVaultサーバでvaultという名前のサービスと、自分のホスト名のサービスを登録していた。
acl_default_policy項が"deny"だとこれらのサービス登録も阻害されるので、これらサービスの登録を許可するACLトークンを作らなければならない。
（これは[先の記事](http://qiita.com/yunano/items/bfaac7868ce4fb946ee5)のHTTPS接続でHA構成とするためのセットアップ手順に従った場合のものである。そんなことしていないならサービスの登録など特に必要ない）

Vaultサーバのうち、cc6というホスト名のノードのためのACLトークンは次のコマンドで発行する。
`token=root`の箇所は、`token=(Consulサーバのacl_master_token項で設定した文字列)`である。
別のホスト用にはホスト名（2箇所）を変更して実行する。

```
# curl -X PUT http://localhost:8500/v1/acl/create?token=root -d '{ "Name": "cc6", "Rules": "key \"_rexec/\" { policy = \"write\" } service \"vault\" { policy = \"write\" } service \"cc6\" { policy = \"write\" }"}'
```

上記、Rulesの中が新しく作るACLトークンのポリシーであり、HCLで記述する。
上記コマンドのままでは読みづらいので読みやすいようにすると次のようなものを与えている。

```
key "_rexec/" { policy = "write" }
service "vault" { policy = "write" }
service "cc6" { policy = "write" }
```

このHCLを使った指定の仕方は[このドキュメント](https://consul.io/docs/internals/acl.html)に説明がある。


ここではvaultというサービスとcc6（ホスト名）というサービスの登録を許可している。
もうひとつの「key "_rexec/"」は、本記事とは特に関係はないが`consul exec`が使用するキーバリューストアのキーへの読み書きを許可するものである。これがないと`consul exec`が使用できない。

さて、上記のcurlコマンドを実行すると`{"ID":"47006405-4248-de71-9874-8588fc2cc176"}`のようにIDが返ってくる。
この値がACLトークンとなる。

ここで得られたACLトークンはVaultサーバがあるノードのConsul設定ファイルのacl_token項に設定する。
acl_token項はそのノードのConsulがConsulクライアントとして振舞う場合に、どのACLトークンを使用するかを設定する項目である。

```json:/etc/consul.d/agent.json
{
  (前略)
  "acl_token": "47006405-4248-de71-9874-8588fc2cc176",
  (後略)
}
```

ここまで、Consulの設定ファイルを変更した際はConsulの再起動を行うこと。
[ドキュメント](http://www.consul.io/docs/agent/options.html)によると`consul reload`ではこれらの設定は反映されないようだ。
しかしConsulの再起動を行った後にさらに`consul reload`してやっとサービス設定が反映されたこともあるのでその辺り良く分からない。

## Vault側の準備

Vaultサーバはstorage backendにConsulを使用している場合、Consulのキーバリューストアにvaultというディレクトリ（あるいは、backend "consul"のpath項に設定したディレクトリ）を作って利用している。
Consulサーバのacl_default_policy項が"deny"だとVaultからこのvaultディレクトリが読み書きできなくなるので、これを許可するACLトークンを作る必要がある。

```
# curl -X PUT http://localhost:8500/v1/acl/create?token=root -d '{ "Name": "vault", "Rules": "key \"vault/\" { policy = \"write\" }"}'
```

発行されたACLトークンはVault設定ファイルのbackend "consul"のtoken項に設定する。
このACLトークンはVaultサーバ共通で同じものが使用できる（どのVaultサーバでも同じポリシーを適用すれば良いから）。

```/etc/vault.d/server.hcl
backend "consul" {
  advertise_addr = "https://cc6.service.consul:8200"
  token = "bfd965ef-1568-7790-7ebe-8e1d60b3ba4e"
}

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_cert_file = "/etc/pki/tls/self/service.crt"
  tls_key_file = "/etc/pki/tls/self/service.key"
}
```

設定したらVaultサーバを再起動する。

現在Vaultにマウントされているsecret backendは`vault mounts`で一覧を見ることができる。

```
# vault mounts
Path     Type     Description
secret/  generic  generic secret storage
sys/     system   system endpoints used for control, policy and debugging
```

secret/とsys/は初期からマウントされているsecret backendである。

Consulをsecret backendとしてマウントするためには次を実行する。

```
# vault mount consul
```

もう一度`vault mounts`を実行するとconsul/が増えている。

```
# vault mounts
Path     Type     Description
consul/  consul
secret/  generic  generic secret storage
sys/     system   system endpoints used for control, policy and debugging
```

ACL一時トークンを発行するConsulのアドレスと発行に使用するACLトークンは consul/config/access に`vault write`することにより指定できる。
今のところACLトークンを発行できるACLトークンはroot（Consulサーバのacl_master_token項で設定したマスタトークン）しか見つけていないので、tokenにはrootを指定している。

```
# vault write consul/config/access address=127.0.0.1:8500 token=root
```

## ACL一時トークンの発行

ここまででVault側の準備もできた。
ここからは実際に発行するための操作になる。

まず発行する一時トークンのポリシーを定義する。
これはHCLで書いたACLポリシーをbase64エンコードして、`vault write consul/roles/(ロール名)`のpolicyに指定してやれば良い。
ロール名は任意である。ここではキーバリューストアのすべてのキーの読み書きができるロールrwと、すべてのキーの読みだけできるロールroを作ってみる。

```
# echo 'key "" { policy = "write" }' | base64 | vault write consul/roles/rw policy=-
# echo 'key "" { policy = "read" }' | base64 | vault write consul/roles/ro policy=-
```

一時トークンの発行は`vault read consul/creds/(ロール名)`で行う。`vault read consul/roles/(ロール名)`ではない。
まずrwロールの方から一時トークンを発行してみる。

```
# vault read consul/creds/rw
Key             Value
lease_id        consul/creds/rw/3f5f5cd1-9e70-8dc8-c286-bba7dff1ace8
lease_duration  3600
lease_renewable true
token           d7f04ee2-1e03-9e1e-74cc-b13660699f81
```

この表示されたtoken、"d7f04ee2-1e03-9e1e-74cc-b13660699f81"が一時トークンである。
`vault read consul/creds/rw`を実行するたびに違うトークンが発行される。

Consulサーバのacl_default_policy項が"deny"なので、トークンなしでConsulのキーバリューストアに書き込もうとしてもエラーになる。

```
# curl -X PUT -d 'aaa' http://127.0.0.1:8500/v1/kv/test1
rpc error: Permission denied
```

しかし先程発行されたトークンを使用すれば書き込みを行うことができる。

```
# curl -X PUT -d 'aaa' http://127.0.0.1:8500/v1/kv/test1?token=d7f04ee2-1e03-9e1e-74cc-b13660699f81
true
```

読み込みもトークンなしでは何も返ってこない。

```
# curl -s http://127.0.0.1:8500/v1/kv/test1
```

一時トークンを使用すれば読み込みできる。
jqはCentOS 6や7だとEPELにある。

```
# curl -s http://127.0.0.1:8500/v1/kv/test1?token=d7f04ee2-1e03-9e1e-74cc-b13660699f81 | jq -r .[0].Value | base64 -d
aaa
```

roロールの方からも一時トークンを発行してみる。

```
# vault read consul/creds/ro
Key             Value
lease_id        consul/creds/ro/b366c317-bb86-7454-14ad-e2cccae52cd7
lease_duration  3600
lease_renewable true
token           27bb968d-1cc3-bccd-1a6c-654e0be02371
```

このトークンでは書き込みはできないが、読み込みは可能である。

```
# curl -X PUT -d 'bbb' http://127.0.0.1:8500/v1/kv/test1?token=27bb968d-1cc3-bccd-1a6c-654e0be02371
rpc error: Permission denied

# curl -s http://127.0.0.1:8500/v1/kv/test1?token=27bb968d-1cc3-bccd-1a6c-654e0be02371 | jq -r .[0].Value | base64 -d
aaa
```

これら一時トークンの寿命はlease_durationに書かれている秒数、つまり3600秒（1時間）である。
本来なら`vault renew (lease_id) (秒数)`で指定した秒数だけ寿命を延ばすことができるはずだがどういうわけか今のところinternalエラーになる（これまで1回だけエラーにならなかったことがあるが、通るとは思ってなくて適当に実行したので本当に成功したのかはよくわからなかった）。
また、今はlease_durationの値を変更することもできない。

寿命が来る前に一時トークンを無効化するには次のようにする。
revokeの後に指定するのは発行時に表示されたlease_idである。

```
# vault revoke consul/creds/ro/b366c317-bb86-7454-14ad-e2cccae52cd7
```

無効化した後に同じトークンを使って読み込みを行おうとしてももはや今度はエラーになる。

```
# curl -s http://127.0.0.1:8500/v1/kv/test1?token=27bb968d-1cc3-bccd-1a6c-654e0be02371
rpc error: ACL not found
```

あるいは-prefix=trueオプションで指定した値から始まるlease_idを持つ一時トークンをすべて無効化できる。
例えばrwロールで発行した一時トークンをすべて無効化する場合は次のようにする。

```
# vault revoke -prefix=true consul/creds/rw/
```

# read,write,renew,revokeについての考察

以上で本来書くべき内容は終了だが、書いてみてvault read/write/renew/revokeについて考えたことをメモ程度に残しておく。

まず`vault read`と`vault write`は一般的に想像されるread/writeとは違う。
オブジェクト指向（というかクラスベース）から言葉を借りれば、`vault write`がクラスの定義、`vault read`はそのクラスのインスタンスを作成するようなものである。

`vault renew`は`vault read`で作ったものの寿命を延ばすコマンドであり、`vault revoke`は`vault read`で作ったものを消し去るコマンドである。どちらも`vault write`で作ったものを操作するわけではない。
ゆえにConsulをsecret backendにする場合のように、`vault read`によって何かが動的に作られ、かつそれが何かと連携していない限り`vault renew`や`vault revoke`は意味をなさない。

従ってVaultに初期から用意されているgenericなsecret backendであるsecret/のキーに対して`vault renew`や`vault revoke`が効果がないように見えるのはこのためである。
generic secret backendは単純に文字列を暗号化して保持しておくために使用されるもの、と理解できる。
しかし[generic secret backendのドキュメント](http://vaultproject.io/docs/secrets/generic/index.html)ではlease_durationを設定する例が書かれているんだよな。ここまでの理解が正しければ、lease_durationなど意味がないはずなのだが。