Fire Engine

化学の修士号→消防士→ITエンジニア(2016年11月〜)

Consulを使う人が知っておくべきACLを使ったセキュリティ対策

こんにちは、つるべーです!みなさん、Consul使ってますか?
ConsulはHashiCorpが開発するツールで、サービスディスカバリやヘルスチェックなど様々な機能を有しています。Consulは、ノードやサービスの状態変化を起点として、特定の処理を発火させることができるため、『状態の把握 → 変化の検知 → 動的な制御』のような流れがConsulを使って組むことができ、動的に変化するインフラの運用管理に絶大な力を発揮するツールです。

一方で、その多岐にわたる機能のため、仕組みを完全に把握することが難しく、使い方によっては思わぬ危険性を持ってしまうことがあります。今回は、Consulのセキュリティ対策について、ACLという仕組みを中心に書いていきたいと思います。

consul image

目次

セキュリティ対策の概要

Consulのセキュリティ周りの概要を掴むには、Consulの公式ドキュメントにあるSecurity Modelのページを読むのが一番良いと思います。Consulをセキュアに使うために必要な情報がまとまっています。

そのページに以下のようなことが書かれていました。

The Consul threat model is only applicable if Consul is running in a secure configuration. Consul does not operate in a secure-by-default configuration.

意訳すると、「Consulはちゃんとセキュアな設定をしたときだけ安全に使えるよ、そしてデフォルトの設定だけでは安全ではないよ」という感じの内容が書かれています。
このことから、Consulを安全に利用するためには、仕組みをちゃんと理解して、明示的に安全な設定をする必要があるということがわかります。

同記事では、Secure Configurationとして、以下の二つが必要だと挙げています。(注:これだけで十分というわけではない)

  1. ACLを有効にする
    ACLのデフォルトポリシーをdenyにする(いわゆるホワイトリスト方式)

  2. 暗号化を有効にする
    Consul agent間の通信を暗号化するためにTLSを利用する

今回はこのうち、「1. ACLを有効にする」について書きます。

Consul HTTP APIに内在する危険性

Consulは主要なインターフェースの一つとして、RESTful HTTP APIを有しており、ノードやサービスに対するCRUD処理を行うことができます。前述のACLは、このHTTP APIに対するアクセス制限を行うためのものなので、ACLの仕組みをお話する前に、「なぜHTTP APIに対するアクセス制御が必要なのか」について書きます。

そのHTTP APIの危険性を理解するには、以下の公式ブログの記事を読むと良いです。

Protecting Consul from RCE Risk in Specific Configurations

内容としては、HTTP APIを利用したRCE (Remote Code Execution) 、すなわち遠隔からの不正なコマンド実行の危険性があるということが書かれています。これは決してConsulの脆弱性についての話でなく、Consulの設定を正しく行わないとRCEの危険性を持ってしまうという話です。

具体的には、ヘルスチェック等に使われるチェックスクリプトをHTTP API経由で登録することができるエンドポイント(/agent/check/register)を利用して、任意のスクリプトを登録・実行するといった攻撃手法があるようです。

これは、通常8500ポートでLISTENするConsul HTTP APIを外部に公開している、かつスクリプトによるチェックを有効にしている(-enable-script-checksをtrueにしている)場合に、攻撃の危険性があります。
この攻撃手法を防ぐためには、いくつかの対応方法が考えられるのですが、この問題を非常に簡単に防ぐ方法がConsul 1.3.0からリリースされたようです。

In Consul 1.3.0, released earlier this month, a member of the Consul community contributed a patch that adds a new configuration option, -enable-local-script-checks, which allows script checks to be registered only via local configuration files, thus preventing use of the HTTP API to register malicious checks.

-enable-script-checksの設定の代わりに、-enable-local-script-checksをtrueにすると、チェックスクリプトの登録をHTTP API経由からできなくなるというものです。
この設定を入れると、前述のチェックスクリプト登録による不正コマンド実行の危険性は防ぐことができます。

ただし、これは危険性の一部に過ぎないかもしれません。なぜなら、HTTP APIのエンドポイントの全てを把握するのは困難だし(私はできていない)、これから追加されないとも言えません。問題なのは、リモートからHTTP APIに対して更新系の処理を許してしまっていることにあるのではないかと思います。そのような考えを元に、チェックスクリプト登録を防ぐということに話を絞らず、HTTP APIへのアクセス制御について書いていきたいと思います。

HTTP APIへのセキュリティ対策

1. HTTP APIを外部に公開しない

まず、一番最初にチェックした方が良い設定は、HTTP APIを外部に公開しているかどうかです。Consulが起動している環境が手元にあるのであればnetstatを叩くと即座にわかります。

/ # netstat -ant | grep 8500
tcp        0      0 :::8500                 :::*                    LISTEN

上は、外部に公開している場合の例です。これはConsulの設定の中に、"client_addr": "0.0.0.0"と書いているためです。もしConsulの設定の中でclient_addrを明示的に指定していないのであれば、デフォルトの値として127.0.0.1が採用されるため、HTTP APIが利用する8500ポートは外部から利用できなくなります。
client_addrを明示的に指定しない場合は以下

/ # netstat -ant | grep 8500
tcp        0      0 127.0.0.1:8500          0.0.0.0:*               LISTEN

どうしても外部からHTTP APIを使いたい場合を除いて、デフォルトのローカルホストのみのLISTENにしておいた方が良いでしょう。これにより外部からHTTP APIの参照・更新系の処理を実行することを防げるため、多くの危険性を未然に防ぐことができます。

2. ACLを使ってアクセス権限を制御する

すでに何度か登場しているACLですが、これを使うと、さらに細かくリソース(node、serviceなど)ごとにread・write権限を設定できます。
また、細かいアクセス制限ができるということに加えて、私がACLを使うメリットの一つだと感じたのは、ローカルホストに対するAPI実行にも制限をかけられることです。
1の方法で、APIを外部に公開しなければ、リモートからAPIを実行されることは防げますが、ローカルホストからの実行に対しては、全権限を有してしまっています。これによる危険性は、攻撃者にSSHログインを許してしまった場合はもちろんですが(SSHログインを許した場合の被害はConsulのAPIを実行されるどころじゃないと思うが)、Consulが動いているサーバ上にホストされているWebアプリケーションに何かしら脆弱性があり、そこを経由してローカルホストへのコマンド実行されるケースが想定されると思います。このような場合に備えて、ローカルホストに対するAPIの実行権限も絞っておくべきではないかと考えます。

Consul ACL

Consul ACLを理解するには下記の公式のドキュメントを読むのが一番の近道だと思います。

https://www.consul.io/docs/guides/acl.html

こちらのドキュメントにはACLの概念や仕様だけでなく、ハンズオン形式で試して行けるようになっているため、大変有用です。
ここではドキュメントから私がポイントだと思った部分をかいつまんで説明していきます。

ACLAccess Control Listという名の通り、APIやデータに対するアクセスをコントロール仕組みです。ACLには2つの主要なコンポーネントがあります。

1. ACL Policies

ACL Policies(以下、単にPolicyと呼ぶ)とは、それぞれのリソースに対するパーミッションの許可または拒否ルールの集合です。
ここでいうリソースとは、ドキュメントにあるリソース一覧のとおり、Agent APIを操作するagentリソースや、KV Store APIを操作するkeyリソースなど様々です。
これらにそれぞれ対して、read・write権限を設定することができます。(さらにprefixごとに権限を設定することもできる)

それぞれのリソースに対するルールを集めたものは、HCLの形式で以下のように書くことができ、

acl = "read"
key_prefix "_rexec" {
  policy = "write"
}
・・・

これがPolicyの設定ファイルになります。

2. ACL Tokens

ACL Tokens(以下、単にTokenと呼ぶ)は、Consul agentへのリクエストに対して、呼び出し側がそのアクションを実行する権限を持っているかを判断するために使用されます。
このtokenに前述のpolicyを割り当てることで、リクエストに対する実行権限を制御します。
ACLを有効にすると以下の2つのtokenがデフォルトで設定されます。

  • Master Token
    全権限を保持しているGlobal ManagementというPolicyがアタッチされている。そのため、Master TokenのSecret IDを使うとなんでも実行できる。

  • Anonymous Token
    tokenをつけずにリクエストを送った場合は、Anonymous Tokenが利用される。すなわちtokenなどつけずに何も意識せず、consul membersなどを叩いた時はAnonymous Tokenが利用される。このAnonymous Tokenには自分でPolicyをアタッチすることができる。

ハンズオン

それでは実際にConsul ACLを触ってみましょう。こちらにdocker-composeを利用した検証環境を用意しています。

github.com

検証にはConsul agentのバージョン1.4.0を利用しています。 上のコードをcloneしてリポジトリのトップディレクトリで下記のコマンドを叩くと、ConsulのServerモード1台、Clientモードが1台立ち上がります。

$ docker-compose up -d
Starting consul-acl-playground_consul-server_1 ... done
Starting consul-acl-playground_consul-agent_1  ... done

立ち上げたコンテナの中に入るには、

$ docker exec -it consul-acl-playground_consul-server_1 /bin/ash

コンテナ内のConsul agentのログを見るには、

$ docker logs -f consul-acl-playground_consul-server_1

といった感じのコマンドを実行します。 Consulの設定ファイル(/etc/consul.d/default.json)に以下のように設定して、ACLは有効にしてあります。

"acl": {
  "enabled": true
  ,"default_policy": "deny"
}

"default_policy": "deny"にすることで、ホワイトリスト方式でルールを追加できるので、denyにしておく方が良いです。 それでは立ち上げたコンテナの中に入って、ACLを設定を行っていきます。

ACLのブートストラップ

ServerモードのサーバでACLの初期化を行います。

/ # consul acl bootstrap
AccessorID:   8bd3c315-9155-57d7-a22f-451665f71154
SecretID:     4ec60a89-abaa-fda9-46c1-e6e174094a97
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-02 06:44:09.0256574 +0000 UTC
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

出力されたSecretIDはMaster tokenのものであるため、どこかに記録しておいたほうが良いでしょう。
Master tokenを使うと、現在発行されているACLのリストを確認できます。

/ # consul acl token list -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
AccessorID:   8bd3c315-9155-57d7-a22f-451665f71154
Description:  Bootstrap Token (Global Management)
Local:        false
Create Time:  2018-12-02 06:44:09.0256574 +0000 UTC
Legacy:       false
Policies:
   00000000-0000-0000-0000-000000000001 - global-management

AccessorID:   00000000-0000-0000-0000-000000000002
Description:  Anonymous Token
Local:        false
Create Time:  2018-12-02 06:33:20.2808862 +0000 UTC
Legacy:       false
Policies:

このようにBootstrap Token(これはMaster tokenのこと)とAnonymous Tokenが発行されていることがわかります。
また、Anonymous TokenにはデフォルトではPolicyがアタッチされていないこともわかります。
したがって、tokenをつけずにconsul membersと叩いても結果が返ってきません。

/ # consul members

Master tokenを使うと当然結果が返ってきます。

/ # consul members -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
Node             Address           Status  Type    Build  Protocol  DC       Segment
consul-server-1  192.168.0.2:8301  alive   server  1.4.0  2         test-dc  <all>
consul-agent-1   192.168.0.3:8301  alive   client  1.4.0  2         test-dc  <default>

そこでまず、Anonymous Tokenに全てのリソースのread権限を与えてみましょう。

Anonymous Tokenに権限を付与

Serverモードのサーバで以下を実行していきます。
Policyは以下のように設定しています。

/ # cat /etc/consul.d/anonymous-policy.hcl
acl = "read"
agent_prefix "" {
    policy = "read"
}
event_prefix "" {
    policy = "read"
}
key_prefix "" {
    policy = "read"
}
keyring = "read"
node_prefix "" {
    policy = "read"
}
operator = "read"
query_prefix "" {
    policy = "read"
}
service_prefix "" {
    policy = "read"
    intentions = "read"
}
session_prefix "" {
    policy = "read"
}

このファイルを元にPolicyを作成します。

/ # consul acl policy create  -name "anonymous-token" -description "Anonymous Token Policy" -rules @/etc/consul.d/anonymous-policy.hcl -token=4ec60a
89-abaa-fda9-46c1-e6e174094a97
ID:           b6f332a9-9f83-2622-2fab-506f85c1e5d8
Name:         anonymous-token
Description:  Anonymous Token Policy
Datacenters:
Rules:
acl = "read"
agent_prefix "" {
    policy = "read"
}
(以下省略)

作成したPolixyをAnonymous Tokenにアタッチします。

/ # consul acl token update -id 00000000-0000-0000-0000-000000000002 --merge-policies -description "Anonymous Token - Read Only" -policy-name anonym
ous-token -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
Token updated successfully.
AccessorID:   00000000-0000-0000-0000-000000000002
SecretID:     anonymous
Description:  Anonymous Token - Read Only
Local:        false
Create Time:  2018-12-02 06:33:20.2808862 +0000 UTC
Policies:
   b6f332a9-9f83-2622-2fab-506f85c1e5d8 - anonymous-token

これで、tokenなしのコマンド実行(Anonymous Token)でreadのコマンドは叩くことができます。以下のようにconsul membersの結果も返ってくるようになりました。

/ # consul members
Node             Address           Status  Type    Build  Protocol  DC       Segment
consul-server-1  192.168.0.2:8301  alive   server  1.4.0  2         test-dc  <all>
consul-agent-1   192.168.0.3:8301  alive   client  1.4.0  2         test-dc  <default>

さぁここまでの設定でMaster tokenのSecret IDを知らないものはローカルホストからの実行でさえ、参照系の処理しかできなくなり、だいぶセキュアになったと思います。
これで完璧かな?と思ったのですが、Consul agentのログをみて見ると、以下のようにエラーとワーニングが出続けています。(下はClientモード側のログ)

2018/12/02 07:00:39 [WARN] agent: Coordinate update blocked by ACLs
2018/12/02 07:01:00 [ERR] consul: "Catalog.Register" RPC failed to server 192.168.0.2:8300: rpc error making call: Permission denied

これはなぜかというと、通常Consul agent同士が協調的に動作するためには、それぞれのnodeが自分自身のnodeの情報を更新する必要があり、現在の設定だと、そのためのTokenおよび権限がないためです。これをAgent Tokenとして付与してやる必要があります。
ドキュメントにもAgent Tokenの役割として、

The acl.tokens.agent is a special token that is used for an agent's internal operations.

と記載されています。次はこれを作っていきます。

Agent Tokenの作成

先に発行したMaster Tokenを使ってAgent Tokenを発行しています。
まず、Agent Token用のPolicyを作成します。これらの操作もServerモードのサーバで行います。

/ # consul acl policy create  -name "agent-token" -description "Agent Token Policy" -rules @/etc/consul.d/agent-policy.hcl -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
ID:           1e812f8f-e72d-43f3-3178-04683bc482f8
Name:         agent-token
Description:  Agent Token Policy
Datacenters:
Rules:
acl = "read"
agent_prefix "" {
    policy = "read"
}

ここでポイントなのが、Agent Token用のPolicyには最低でもnodeリソースに対するwrite権限を与えている必要があります。
上で作成したポリシーをアタッチしたAgent Tokenを発行します。

/ # consul acl token create -description "Agent Token" -policy-name "agent-token" -token=4ec60a89-abaa-fda9-46c1-e6e174094a97
AccessorID:   f604f2f2-ce21-8a64-e9ba-1ef877999742
SecretID:     65736278-e97b-6a43-9893-62f9a8574140
Description:  Agent Token
Local:        false
Create Time:  2018-12-02 07:11:32.8620954 +0000 UTC
Policies:
   1e812f8f-e72d-43f3-3178-04683bc482f8 - agent-token

この時、自動でSecretIDが払い出されます。
この時点からAgent Tokenを使ってアクセスできることがわかります。

/ # consul members -token=65736278-e97b-6a43-9893-62f9a8574140
Node             Address           Status  Type    Build  Protocol  DC       Segment
consul-server-1  192.168.0.2:8301  alive   server  1.4.0  2         test-dc  <all>
consul-agent-1   192.168.0.3:8301  alive   client  1.4.0  2         test-dc  <default>

このAgent tokenのSecret IDを下記のように設定ファイルに埋め込んで、(Serverモード・Clientモードの両方のdefault.json

"acl": {
  "enabled": true
  ,"default_policy": "deny"
  ,"tokens" : {
    "agent" : "65736278-e97b-6a43-9893-62f9a8574140"
  }
}

Consul agentをrestartすると(Dockerコンテナごとrestart)

$ docker restart consul-acl-playground_consul-server_1
consul-acl-playground_consul-server_1
$ docker restart consul-acl-playground_consul-agent_1
consul-acl-playground_consul-agent_1

エラーやワーニングが出なくなり、Consulが正常に動いていることがわかります。
以上の設定で、Tokenを知らない限り、APIに対する更新系の処理は実行できなくなりました。

さいごに

Consulは非常に便利な反面、仕様の理解が難しく、私もなかなか活用しきれてないのが現状です。ただ、非常に将来性を感じるツールですし、他のツールと組み合わせて力を発揮できるようなシーンも考えられるため、これから真剣に触れて学んでいこうと思います!正しく設定を理解してセキュアに使っていきましょう!