Consul ACLの使い方

Consul ACL (Consul 0.4.X)の使い方。
詳細まで正しいかは不明だが、解ったことから書いていく。

概要
----

* トークン単位で値に対する操作を制限する機能。
* 各トークンはID、名前、どこの値にどのようにアクセスできるかのルールセットを持つ。
* 値へのアクセス時にトークンIDを指定し（指定しなければデフォルトトークン）、認可を行う。

公式ドキュメントは[こちら](http://www.consul.io/docs/internals/acl.html "ACL")。

対象API
-------

[ドキュメント](http://www.consul.io/docs/internals/acl.html "ドキュメント")に、
> As of Consul 0.4, it is only possible to specify policies for the KV store.

とあるとおり、0.4時点ではKVSだけアクセス制限できる。
※ACLのAPIにはmaster tokenでないとアクセスできない。

Configで書けること
-----------------

ACLはHTTP APIで登録する。Configファイルにルールを書いておいて、起動したらそれが有効になる、という使い方ではない模様。

### Configファイルに書く値

* acl_datacenter

これで指定したdatacenterがACLでアクセス制限する対象になる。この指定があるとACLが有効になるが、指定せずにACLのAPIにアクセスするとエラーが発生する。

```bash
$ curl http://localhost:8500/v1/acl/list?token=master_token_test
ACL support disabled
```

* acl_default_policy

allowかdenyを設定する。ACLの持っているruleに対して適用されるもので、allowの場合はblacklist、denyの場合はwhitelist方式での制限を行う。
デフォルトはallow。

* acl_master_token

Typeがmanagementのトークン。これを指定すると、全ての値にwriteレベルでアクセスでき、ACLの操作ができる。
※対してTypeがclientの場合、ACLの操作はできない。

* acl_token

HTTP APIで値にアクセスする際、?token=を指定しない場合はこのトークンが利用される。
デフォルトはanonymous。
※トークンを指定すれば、このデフォルトをリクエスト単位で上書きできる、ということ。

### 設定ファイルの例

/etc/consul.d/default.json

```json
{
  "datacenter": "testdc",
  "data_dir": "/var/lib/consul",
  "server": true,
  "bootstrap": true,
  "acl_datacenter": "testdc",
  "acl_default_policy": "deny",
  "acl_master_token": "master_token_test",
  "acl_token": "anonymous"
}
```

上記の設定で起動した場合にデフォルトで存在するACLを取得してみると、以下のようになる。

```bash
$ curl http://localhost:8500/v1/acl/list
Permission denied

$ curl http://localhost:8500/v1/acl/list?token=anonymous
Permission denied

$ curl http://localhost:8500/v1/acl/list?token=master_token_error
ACL not found

$ curl http://localhost:8500/v1/acl/list?token=master_token_test | jq .
[
  {
    "Rules": "",
    "Type": "client",
    "Name": "Anonymous Token",
    "ID": "anonymous",
    "ModifyIndex": 2,
    "CreateIndex": 2
  },
  {
    "Rules": "",
    "Type": "management",
    "Name": "Master Token",
    "ID": "master_token_test",
    "ModifyIndex": 3,
    "CreateIndex": 3
  }
]
```

ACLの登録
--------

ACLはHTTP APIで登録する。
Rulesは [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl/ "HashiCorp Configuration Language (HCL)") を文字列で記述する。 ~~jsonで書くとエラーが発生する。~~ jsonで書いても大丈夫。
policyにはdeny、read、writeを書くことができ、writeはreadの権限も包含する(readwriteは存在せず、書き込めるが読み込めない、ということは無い)。

例：

```bash
$ cat /tmp/acl.txt
{
  "Name": "test_token",
  "Type": "client",
  "Rules": "
    key \"testread/\" {
      policy = \"read\"
    }
    key \"testwrite/\" {
      policy = \"write\"
    }
  "
}

$ curl -X PUT http://localhost:8500/v1/acl/create?token=master_token_test -d @/tmp/acl.txt
{"ID":"53b6df4c-7f77-c816-750d-b5eabf5ca582"}

$ curl http://localhost:8500/v1/acl/list?token=master_token_test | jq .
[
  {
    "Rules": "    key \"testread/\" {      policy = \"read\"    }    key \"testwrite/\" {      policy = \"write\"    }  ",
    "Type": "client",
    "Name": "test_token",
    "ID": "53b6df4c-7f77-c816-750d-b5eabf5ca582",
    "ModifyIndex": 26,
    "CreateIndex": 26
  },
  {
    "Rules": "",
    "Type": "client",
    "Name": "Anonymous Token",
    "ID": "anonymous",
    "ModifyIndex": 2,
    "CreateIndex": 2
  },
  {
    "Rules": "",
    "Type": "management",
    "Name": "Master Token",
    "ID": "master_token_test",
    "ModifyIndex": 3,
    "CreateIndex": 3
  }
]
```

createすると、作成されたACLのIDが戻ってくるので、それを使う形になる。

ACLを使ってアクセス
-----------------

### readableに対するアクセス

* readableに対してGETを行い、値が無い場合

```bash
$ # no value
$ curl -i http://localhost:8500/v1/kv/testread/value?token=master_token_test
HTTP/1.1 404 Not Found
X-Consul-Index: 44
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:46:22 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i http://localhost:8500/v1/kv/testread/value?token=anonymous
HTTP/1.1 404 Not Found
X-Consul-Index: 44
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:46:30 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i http://localhost:8500/v1/kv/testread/value
HTTP/1.1 404 Not Found
X-Consul-Index: 44
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:46:40 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i http://localhost:8500/v1/kv/testread/value?token=53b6df4c-7f77-c816-750d-b5eabf5ca582
HTTP/1.1 404 Not Found
X-Consul-Index: 44
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:46:54 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8
```

* readableに対してPUTを行う場合

``` bash
$ curl -i -X PUT http://localhost:8500/v1/kv/testread/value -d 200
HTTP/1.1 403 Forbidden
Date: Thu, 06 Nov 2014 06:47:41 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

Permission denied

$ curl -i -X PUT http://localhost:8500/v1/kv/testread/value?token=anonymous -d 200
HTTP/1.1 403 Forbidden
Date: Thu, 06 Nov 2014 06:47:48 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

Permission denied

$ curl -i -X PUT http://localhost:8500/v1/kv/testread/value?token=53b6df4c-7f77-c816-750d-b5eabf5ca582 -d 200
HTTP/1.1 403 Forbidden
Date: Thu, 06 Nov 2014 06:47:59 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

Permission denied

$ curl -i -X PUT http://localhost:8500/v1/kv/testread/value?token=master_token_test -d 200
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 06 Nov 2014 06:48:31 GMT
Content-Length: 4

true
```

* readableに対してGETを行い、値がある場合

```bash
$ curl -i http://localhost:8500/v1/kv/testread/value?token=master_token_test
HTTP/1.1 200 OK
Content-Type: application/json
X-Consul-Index: 48
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:48:58 GMT
Content-Length: 99

[{"CreateIndex":48,"ModifyIndex":48,"LockIndex":0,"Key":"testread/value","Flags":0,"Value":"MjAw"}]

$ curl -i http://localhost:8500/v1/kv/testread/value?token=anonymous
HTTP/1.1 404 Not Found
X-Consul-Index: 48
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:49:26 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i http://localhost:8500/v1/kv/testread/value
HTTP/1.1 404 Not Found
X-Consul-Index: 48
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:49:34 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i http://localhost:8500/v1/kv/testread/value?token=53b6df4c-7f77-c816-750d-b5eabf5ca582
HTTP/1.1 200 OK
Content-Type: application/json
X-Consul-Index: 48
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 06:49:42 GMT
Content-Length: 99

[{"CreateIndex":48,"ModifyIndex":48,"LockIndex":0,"Key":"testread/value","Flags":0,"Value":"MjAw"}]
```

master_tokenは何でもできる。
また、readが許可されていないトークンでGETを行った場合、403ではなく404になるらしいので、エラーハンドリングは注意が必要かもしれない。

### writableに対するアクセス

master_tokenは上でも似たような実験を行ったため、省略。

* writableに対して書き込み

```bash
$ curl -i -X PUT http://localhost:8500/v1/kv/testwrite/value -d 300
HTTP/1.1 403 Forbidden
Date: Thu, 06 Nov 2014 07:00:47 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

Permission denied

$ curl -i -X PUT http://localhost:8500/v1/kv/testwrite/value?token=anonymous -d 300
HTTP/1.1 403 Forbidden
Date: Thu, 06 Nov 2014 07:00:54 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

Permission denied

$ curl -i -X PUT http://localhost:8500/v1/kv/testwrite/value?token=53b6df4c-7f77-c816-750d-b5eabf5ca582 -d 300
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 06 Nov 2014 07:01:03 GMT
Content-Length: 4

true
```

* writableに対して読み込み

```bash
$ curl -i http://localhost:8500/v1/kv/testwrite/value
HTTP/1.1 404 Not Found
X-Consul-Index: 62
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 07:03:09 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i http://localhost:8500/v1/kv/testwrite/value?token=anonymous
HTTP/1.1 404 Not Found
X-Consul-Index: 62
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 07:03:17 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

$ curl -i http://localhost:8500/v1/kv/testwrite/value?token=53b6df4c-7f77-c816-750d-b5eabf5ca582
HTTP/1.1 200 OK
Content-Type: application/json
X-Consul-Index: 62
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 07:03:23 GMT
Content-Length: 100

[{"CreateIndex":62,"ModifyIndex":62,"LockIndex":0,"Key":"testwrite/value","Flags":0,"Value":"MzAw"}]
```

KVS以外のAPIへのアクセス
--------------------------

先頭で記載したとおり、0.4時点ではKVS以外へのアクセスは制限されない。

```bash
$ curl -i -X GET http://localhost:8500/v1/catalog/services?token=53b6df4c-7f77-c816-750d-b5eabf5ca582
HTTP/1.1 200 OK
Content-Type: application/json
X-Consul-Index: 6
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 07:19:51 GMT
Content-Length: 13

{"consul":[]}

$ curl -i -X PUT http://localhost:8500/v1/event/fire/testevent?token=53b6df4c-7f77-c816-750d-b5eabf5ca582
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 06 Nov 2014 07:20:16 GMT
Content-Length: 151

{"ID":"1aa92970-7d1a-d4ae-2334-d8fca7002a26","Name":"testevent","Payload":null,"NodeFilter":"","ServiceFilter":"","TagFilter":"","Versi
on":1,"LTime":0}

$ curl -i -X GET http://localhost:8500/v1/event/list?token=53b6df4c-7f77-c816-750d-b5eabf5ca582
HTTP/1.1 200 OK
Content-Type: application/json
X-Consul-Index: 4151740018762382984
Date: Thu, 06 Nov 2014 07:20:24 GMT
Content-Length: 153

[{"ID":"1aa92970-7d1a-d4ae-2334-d8fca7002a26","Name":"testevent","Payload":null,"NodeFilter":"","ServiceFilter":"","TagFilter":"","Vers
ion":1,"LTime":2}]
```

ただし、ACLのAPIだけは、masterのみアクセス可能。

```bash
$ curl -i -X GET http://localhost:8500/v1/acl/list
HTTP/1.1 403 Forbidden
X-Consul-Index: 0
X-Consul-Knownleader: false
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 07:12:18 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

Permission denied

$ curl -i -X GET http://localhost:8500/v1/acl/list?token=53b6df4c-7f77-c816-750d-b5eabf5ca582
HTTP/1.1 403 Forbidden
X-Consul-Index: 0
X-Consul-Knownleader: false
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 07:12:22 GMT
Content-Length: 17
Content-Type: text/plain; charset=utf-8

Permission denied

$ curl -i -X GET http://localhost:8500/v1/acl/list?token=master_token_test
HTTP/1.1 200 OK
Content-Type: application/json
X-Consul-Index: 26
X-Consul-Knownleader: true
X-Consul-Lastcontact: 0
Date: Thu, 06 Nov 2014 07:12:26 GMT
Content-Length: 449

[{"CreateIndex":26,"ModifyIndex":26,"ID":"53b6df4c-7f77-c816-750d-b5eabf5ca582","Name":"test_token","Type":"client","Rules":"    key \"testread/\" {      policy = \"read\"    }    key \"testwrite/\" {      policy = \"write\"    }  "},{"CreateIndex":2,"ModifyIndex":2,"ID":"anonymous","Name":"Anonymous Token","Type":"client","Rules":""},{"CreateIndex":3,"ModifyIndex":3,"ID":"master_token_test","Name":"Master Token","Type":"management","Rules":""}]
```

細かい動き
----------

### クラスタに参加するノードの設定に書かれたacl_master_tokenがすべて異なる場合、どれが有効になるのか？

acl_master_tokenは、Leaderのものが有効になる。
厳密には、Leaderのacl_master_tokenをIDとしたACLがクラスタに追加される。

### Leaderがダウンしたらどうなるのか？

* 新しく選出されたLeaderの設定に書かれていたacl_master_tokenがダウンした元Leaderのacl_master_tokenと異なる場合

新しく選出されたLeaderの設定に書かれていたacl_master_tokenをIDとしたACLが追加される。
ダウンした元Leaderのacl_master_tokenのACLはそのまま残るので、master tokenが複数ある状態になる。
（ダウン時も、ダウンした元Leaderの復旧時も、特に削除しにいったりはしない）

* 新しく選出されたLeaderの設定に書かれていたacl_master_tokenがダウンした元Leaderのacl_master_tokenと同一の場合

ACLは増えもせず減りもせず。引き続き同じacl_master_tokenのACLを利用できる。


今のところ、ここまで。