HashiCorp Boundaryを使った本番リソースへのアクセス制御を考える

[Ateam Group Manager ＆ Specialist Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ateam)の16日目は株式会社エイチーム引越し侍のインフラエンジニア、[@dd511805](https://qiita.com/dd511805)が担当します。

毎年クリスマスにはサーバーに障害が発生しても自己修復する機能が備わりますようにとお願いをしています。残念ながらそんな都合のよい願いは聞き届けられることなく、それは[kから始まって間に8文字のアルファベットがあってsで終わるプロダクト](https://kubernetes.io/ja/)で実現せよという天の声が聞こえるようになりました。
インフラレベルの障害であればそれでいいのですが、アプリケーションのリリースを伴う不具合ではホストにあるログやDBにあるトランザクションデータを確認したい場合が出てきます。
大抵の場合、プロダクション環境へのリソースへのアクセスは制限されていますが、機能を開発したエンジニアに一時的にアクセスを許可したいというケースも出てきます。
そういったときに接続をセキュアに管理できそうなHashiCorp Boundaryについて触ってみました。

# 今までのサーバーへログインする手段
Boundayのサイトから従来のワークフローを拝借して下記に載せていますが、サーバーへログインするまでに以下のようなフローが必要でした。
 >![Traditional Workflow](https://www.datocms-assets.com/2885/1602530950-boundary-blog-3-edited.png?fit=max&fm=png&q=80&w=2500 "Traditional Workflow")
>[(Citation: Announcing HashiCorp Boundary)](https://www.hashicorp.com/blog/hashicorp-boundary)

1. VPNに接続
2. 踏み台となるSSHサーバーへ接続
3. IPベースのファイヤーウォールを通過
4. アプリケーションでユーザー認証を行う

このフローにはいくつかの難しい点が存在します。たとえば、1.ではオンボーディングをスケールさせることは難しいです。(コロナ禍の現状ではリモートで働いているためVPNに接続可能であるというケースもあるかもしれません)
2.ではユーザーがネットワークにアクセスするため攻撃にさらされる機会が多くなります。
3.では主にIPベースのファイヤーウォールの設定を行いますが、これはクラウド上でのサーバーの振る舞いとマッチしません。
4.ではユーザーはアクセスするホストの資格情報を知っている必要があります。

# HahiCorp Boundaryを使った場合

Boundaryを使った場合については以下のようになります。

 >![Boundary Workflow](https://learn.hashicorp.com/img/boundary/boundary-workflow.png "Boundary Workflow")
>[(Citation: Introduction to Boundary)](https://learn.hashicorp.com/tutorials/boundary/getting-started-intro?in=boundary/getting-started)

1. IDプロバイダーで認証
2. ロールと論理的なサービスに基づいた承認
3. カタログからホストやアプリケーションを選択
4. アプリケーションの資格情報をユーザーに公開せずに接続

1.のステップはIdプロバイダーにリンクしたことによりオンボーディングが簡単になります。例えば退職者が出た場合にはIdプロバイダーからユーザーを削除するだけで済み、それはおそらく退職処理のワークフローに存在します。
2.のステップはユーザーに対してより柔軟なアクセス権限の制御が出来るようになります。踏み台サーバーへのSSH接続ではそこでユーザーがSSH可能かどうかまでは簡単に制御できますが、その先のホストの権限に関しては制御できませんでした。
4.のステップではユーザーに資格情報を渡すことなくターゲットのホストやアプリケーションにアクセスできるようになります。

Boundaryを使うことによって、今まではホストやアプリケーションでユーザーに対するアクセス制御
を行う必要がありましたが、その前にあるBoundaryで行うことができるようになりました。

# Boundaryのインストール
概要が何となくわかったところで、Boundaryのインストールを行っていきます。各環境ごとのインストール方法は[Getting Started](https://learn.hashicorp.com/tutorials/boundary/getting-started-install?in=boundary/getting-started)に紹介されています。

```
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install boundary
```

ubuntuの環境で試しましたが、Getting Startedの説明通りに動いたので、特に特筆すべきことはありません。
最後にboundaryコマンドをうって、インストールが完了されているか確認します。

```
$ boundary
Usage: boundary <command> [args]

Commands:
    accounts           Manage Boundary accounts
    auth-methods       Manage Boundary auth methods
...
```

# Boundaryの開発用サーバーの起動

前項でboundaryコマンドを確認したとおり、boundary devコマンドで開発用のサーバーを起動できるので、開発用サーバーを起動して動作を見ていきます。

```
$ boundary dev
==> Boundary server configuration:

       [Controller] AEAD Key Bytes: 4PLX4uOboddtUw2sHCWgmyBr5Kagzy6csmssnJht+Ro=
         [Recovery] AEAD Key Bytes: QdGzvGJhLhHRw/imeqBMtWt2Xqibyfl/iRn7OxEyAIk=
      [Worker-Auth] AEAD Key Bytes: pDYVjt4ArZzL3mmZ13m0D8EWtR+lYknHvbf6oh0ZMFk=
              [Recovery] AEAD Type: aes-gcm
                  [Root] AEAD Type: aes-gcm
           [Worker-Auth] AEAD Type: aes-gcm
                               Cgo: disabled
    Controller Public Cluster Addr: 127.0.0.1:9201
            Dev Database Container: boring_cannon
                  Dev Database Url: postgres://postgres:password@localhost:32769?sslmode=disable
          Generated Auth Method Id: ampw_1234567890
  Generated Auth Method Login Name: admin
    Generated Auth Method Password: password
         Generated Host Catalog Id: hcst_1234567890
                 Generated Host Id: hst_1234567890
             Generated Host Set Id: hsst_1234567890
            Generated Org Scope Id: o_1234567890
        Generated Project Scope Id: p_1234567890
               Generated Target Id: ttcp_1234567890
                        Listener 1: tcp (addr: "127.0.0.1:9200", max_request_duration: "1m30s", purpose: "api")
                        Listener 2: tcp (addr: "127.0.0.1:9201", max_request_duration: "1m30s", purpose: "cluster")
                        Listener 3: tcp (addr: "127.0.0.1:9202", max_request_duration: "1m30s", purpose: "proxy")
                         Log Level: info
                             Mlock: supported: true, enabled: false
                           Version: Boundary v0.1.2
                       Version Sha: d8020842ae8b6c742b94538baada313d7eb52809
                Worker Public Addr: 127.0.0.1:9202

==> Boundary server started! Log data will stream in below:

2020-12-08T13:55:50.849Z [INFO]  worker: connected to controller: address=127.0.0.1:9201
2020-12-08T13:55:50.852Z [INFO]  controller: worker successfully authed: name=dev-worker
```
サーバーが起動すると以下のメッセージが表示されます。http://127.0.0.1:9201にアクセスしてGenerated Auth Method Login NameとGenerated Auth Method Password
の値を入力するとログインできます。
ちなみにboundary devで起動した状態でdocker psしてみるとpostgresのコンテナだけが起動していることが分かります。volumeをマウントせずに起動している状態なので、停止すると今まで設定した内容が消えてしまうことには注意が必要です。消えてもらったほうが便利なこともありますが。

```
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                     NAMES
f771b3c656ba        postgres:12         "docker-entrypoint.s…"   3 minutes ago       Up 3 minutes        0.0.0.0:32769->5432/tcp   boring_cannon
```

もしローカルではなく検証用のサーバーで動作を確認しているときは-api-listen-addressの引数に0.0.0.0を入れることで128.0.0.1以外のURLでも
boundaryにアクセスできるようになります。

```
boundary dev -api-listen-address=0.0.0.0 -cluster-listen-address=0.0.0.0 -proxy-listen-address=0.0.0.0 -worker-public-address=10.
3.8.11
```

ログイン画面まで見ることができました。次はBoundaryの管理画面の機能を見ていきます。
![boundary_screenshot1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60761/3e2b2942-c3b7-1094-4c2b-245a21cebde4.png)

# Boundaryの管理画面の機能
まず最初に目につくのはOrgの項目です。
![boundary_screenshot3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60761/e144601b-55a4-3df1-da0c-252ce2a05ff6.png)
OrgはProjectとIAMリソースを整理するために使用されるスコープです。
![boundary_screenshot4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60761/bbe1de88-4de3-cfdd-145d-ef622fa9f995.png)
ProjectはTargetとHostCatalogを整理するために使用されるスコープです。
![boundary_screenshot5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60761/8e21a71d-c291-8f34-cb42-e5f23a9a3206.png)
そしてHost CatalogはHostとHostSetsを含むリソースになります。
![boundary_screenshot6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60761/9bdefaa8-b26a-0b54-c56c-76524f7435a4.png)
HostはBoundaryから到達可能なネットワークアドレスを持つリソース、HostSetsはアクセス制御の目的で同等とみなされるホストのコレクションを表すリソースです。

画面上からどのようなリソースがあるのか見てきましたが、実際のリソースの関係性は以下のようになります。
>![boundary_domainmodel](https://www.boundaryproject.io/img/boundary-domain-model-overview.jpg)
>[(Citation: Overview)](https://www.boundaryproject.io/docs/concepts/domain-model)

# Boundaryを使って作成済みのホストに接続する
Boundaryをdevモードで起動すると自分自身のホストが登録されているので、その情報を使って自分自身のサーバーにSSHをしてみます。
まずはCLIでboundaryの認証を行います。

```
$ export BOUNDARY_ADDR=http://3.112.66.20:9200
$ boundary authenticate password -auth-method-id=ampw_1234567890 -login-name=admin -password=password -keyring-type=none -fo
rmat=json
Error opening keyring: Specified keyring backend not available
Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.
{"id":"at_WznzLAtOfA","scope":{"id":"global","type":"global"},"token":"at_WznzLAtOfA_s1YPpFiPHyrqmQzUgWoMaFH4sBartUYjGoNvb3B9C7ss65C1oT8DAkDUBv4yP1cx9jP2nV5BiUnpLEasfTWhPX3QULsKwjCcCsDdYd2FpCCXWetz8D1isMN9KaCXxAh","user_id":"u_1234567890","auth_method_id":"ampw_1234567890","account_id":"apw_86L44alteB","created_time":"2020-12-09T13:17:24.079332Z","updated_time":"2020-12-09T13:17:24.079332Z","approximate_last_used_time":"2020-12-09T13:17:24.079332Z","expiration_time":"2020-12-16T13:17:24Z"}
```

上記のような結果が返ってきました。expiration_timeはこのトークンの有効期限を示しているものと思われますが7日間ほどの期間があるようでした。AWSのassume roleとアクセスキー
の有効期限は最長で1日程度なので、それに比べると長い印象を受けました。管理画面から有効期限を変える方法があるのか探してみましたが、見つかりませんでした。
このコマンドで得られたトークンを環境変数 BOUNDARY_TOKENに格納する必要があるので、以下のようにして環境変数に格納します。

```
$ boundary authenticate password -auth-method-id=ampw_1234567890 -login-name=admin -password=password -keyring-type=none -format=json  | jq -r ".token" > boundary_token.txt
$ export BOUNDARY_TOKEN=$(cat boundary_token.txt)
```

準備が整ったので、boundaryを使ってssh接続を試みます。当然ですが、自分自身にssh接続をするわけなので、boundaryを介す前にssh localhostなどで
ssh接続できるかどうかは確認しておきます。
boundaryを使ってSSH接続を行うには以下コマンドを実行します
target-idとhost-idは管理画面上のTargetsとHost Catalogから取得した値を使います。

```
$ boundary connect ssh -target-id ttcp_1234567890 -host-id hst_1234567890
```

これでboundaryを用いたSSH接続を行うことができます。

![boundary_screenshot2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60761/6318404c-70e2-0d3f-f395-6dd7746d45be.png)

SSH接続をした情報は管理画面のSessionsで確認することができ、ここではどのユーザーがいつセッションを開始したかを把握することが出来ます。
右端のActionsのCancelボタンを押すと、利用中のセッションを強制的に切断することができ、ターミナル上では以下のメッセージが出て切断されたことが確認できます。

```
ubuntu@ip-10-3-8-11:~ 
$ Connection to 127.0.0.1 closed by remote host.
Connection to 127.0.0.1 closed.
```

boundaryは現在、ssh,postgres,rdpのラッパーが組み込まれています。例えばpostgresに場合はport番号5432を指定したTargetを指定して、
postgresqlが起動しているインスタンスをHostとして登録してTargetとHost間をHostSetsで繋げたところ
以下のような記述をすることで接続することが出来ました。

```
$ boundary connect postgres -target-id ttcp_53aMW8rtHZ -host-id=hst_fsqE6zrbCy -- -U ec2_user -d postgres
psql (10.15 (Ubuntu 10.15-0ubuntu0.18.04.1), server 9.3.9)
Type "help" for help.

postgres=>
```

boundaryに組み込まれていないライブラリを使いたい場合はboundary connect -execを利用することで、
好きなクライアントライブラリを利用することができます。
例えばmysqlに接続したい場合は以下のようになります。

```
$ boundary connect -target-id ttcp_kU7ckgb70J -exec mysql -- -h mysql.host -u admin -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 498
Server version: 5.6.10 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
```
target-idを指定してexecオプションで使用するコマンドを指定、--の後にコマンドで使用する引数を入れることでmysqlに接続できるようになりました。

この仕組みを活用すると~/.ssh/configに以下のような記述をすることで、boundaryを使ったSSH接続をすることが出来るようになります。

```
Host boundary-web
    Hostname localhost
    ProxyCommand sh -c "boundary connect -target-id ttcp_1234567890 -exec nc -- {{boundary.ip}} {{boundary.port}}"
```
# BoundaryをTerraformで構成する
クライアント側での使い方は何となくわかったので、次はBoundaryでの構成の管理方法について見ていきます。
[Terraform](https://www.terraform.io/)はBoundaryと同じくHashiCorpが開発したIaCのためのツールですが、Boundaryの構成もTerraformで行うことができます。
ユーザーを作成する。ユーザーにグループを割り当てる。ホストマップにホストを追加するなど、UIから手作業で行えることは便利ですが、誰がどこのグループに存在するかなどを管理するのは人数が増えてくると困難になります。Terraformを使うことによってアクセス権のあるべき姿を定義することができるのは大きなメリットがあると感じます。
早速[Getting Started](https://learn.hashicorp.com/tutorials/boundary/getting-started-config?in=boundary/getting-started)にあるmain.tfを実行して結果を見てみます。

```
ubuntu@ip-10-3-8-11:~/boundary-test 
$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # boundary_account.users_acct["Jeff"] will be created
...
...
...
boundary_target.backend_servers_ssh: Creating...
boundary_target.backend_servers_service: Creating...
boundary_role.organization_admin: Creation complete after 4s [id=r_heoH1yRuG8]
boundary_target.backend_servers_service: Creation complete after 2s [id=ttcp_pO52QkwFbR]
boundary_target.backend_servers_ssh: Creation complete after 2s [id=ttcp_3uswNmsz7Y]
boundary_role.organization_readonly: Creation complete after 3s [id=r_QCSkRaHzCz]

Apply complete! Resources: 28 added, 0 changed, 0 destroyed.
```
という結果になって28個のリソースが作成されていることがわかります。
管理画面から新たに作成した組織のユーザーを見てみるとTerraformで作成した9名のユーザーが追加されていることが確認できます。
![boundary_screenshot7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60761/937650d9-3157-dc19-b29d-cc695619cc67.png)

[Getting Started](https://learn.hashicorp.com/tutorials/boundary/getting-started-config?in=boundary/getting-started)はここで終わりになっているのですが、これだけではTerraformで作成したユーザーではCLIで操作を行うことはできません。

```
$ boundary authenticate password -auth-method-id=ampw_cXIe93axU9 -login-name=jim -password=password -keyring-type=none -format=json
 | jq -r ".token" > boundary_token.txt
Error opening keyring: Specified keyring backend not available
Token must be provided via BOUNDARY_TOKEN env var or -token flag. Reading the token can also be disabled via -keyring-type=none.
$ export BOUNDARY_TOKEN=$(cat boundary_token.txt)
$ boundary users read -id=u_nlptCHusjJ
Error from controller when performing read on user: 
Error information:
  Code:
  Message:             Forbidden
  Status:
```
これはUserとAccountが紐づいていないことが原因なので、以下のようにUserの定義を変更してAccountとUserを紐づけます。

```
$ git diff
diff --git a/main.tf b/main.tf
index 72ad01f..affeec2 100644
--- a/main.tf
+++ b/main.tf
@@ -69,6 +69,7 @@ resource "boundary_user" "users" {
   for_each    = var.users
   name        = each.key
   description = "User resource for ${each.key}"
+  account_ids = [boundary_account.users_acct[each.key].id]
   scope_id    = boundary_scope.corp.id
 }
```
これを行うことでCLIでの操作が可能になります

```
$ boundary users read -id=u_uXqx4mhw4s

User information:
  Created Time:        Sun, 13 Dec 2020 15:50:02 UTC
  Description:         User resource for Jim
  ID:                  u_uXqx4mhw4s
  Name:                Jim
  Updated Time:        Sun, 13 Dec 2020 15:50:03 UTC
  Version:             2

  Scope:
    ID:                o_Ico4y2244E
    Name:              Corp One
    Parent Scope ID:   global
    Type:              org

  Accounts:
    ID:                apw_kmNlLgbkLo
    Scope ID:          o_Ico4y2244E
```

最後にCorp One orgにpostgresqlにアクセスできるoperaterのユーザーを作ってみます

```terraform:operater.tf
# Userのリスト定義
variable "operater_users" {
  type    = set(string)
  default = [
    "Nick"
  ]
}
# Accountの作成
resource "boundary_account" "operaters_acct" {
  for_each       = var.operater_users
  name           = each.key
  description    = "Operater User account for ${each.key}"
  type           = "password"
  login_name     = lower(each.key)
  password       = "password"
  auth_method_id = boundary_auth_method.password.id
}
# Userの作成とAccountの紐付け
resource "boundary_user" "operater_users" {
  for_each    = var.operater_users
  name        = each.key
  description = "User resource for ${each.key}"
  account_ids = [boundary_account.operaters_acct[each.key].id]
  scope_id    = boundary_scope.corp.id
}
# Groupの作成とUser、Orgの紐付け
resource "boundary_group" "operater" {
  name        = "Operater"
  description = "Organization group for operater users"
  member_ids  = [for user in boundary_user.operater_users : user.id]
  scope_id    = boundary_scope.corp.id
}
# Roleの作成とGroupの紐付け、Grantの割り当て、Grant Scopeの設定、Orgの紐付け
# actions=authorize-sessionがターゲットを利用して接続するための権限
resource "boundary_role" "operater" {
  name        = "Operater"
  description = "Operater role"
  principal_ids = [boundary_group.operater.id]
  grant_strings = ["id=${boundary_target.postgres_connection.id};actions=authorize-session"]
  scope_id    = boundary_scope.corp.id
  grant_scope_id = boundary_scope.core_infra.id
}
# Host Catalogの作成、Orgの紐付け
resource "boundary_host_catalog" "postgres" {
  name        = "postgress"
  description = "Postgres host catalog"
  type        = "static"
  scope_id    = boundary_scope.core_infra.id
}
# Hostの作成、Host Catalogの紐付け
resource "boundary_host" "postgres" {
  type            = "static"
  name            = "postgres_host1"
  description     = "Postgres host 1"
  address         = "10.3.4.27"
  host_catalog_id = boundary_host_catalog.postgres.id
}
# Host Setの作成、Hostの紐付け、Host Catalogの紐付け
resource "boundary_host_set" "postgres" {
  type            = "static"
  name            = "postgres_hostset"
  description     = "Host set for Postgress"
  host_catalog_id = boundary_host_catalog.postgres.id
  host_ids        = [boundary_host.postgres.id]
}
# Targetの作成、Host Setの紐付け
resource "boundary_target" "postgres_connection" {
  type         = "tcp"
  name         = "Postgres"
  description  = "Postgres target"
  scope_id     = boundary_scope.core_infra.id
  default_port = "5432"

  host_set_ids = [
    boundary_host_set.postgres.id
  ]
}
```
これでoperaterユーザーグループに所属する人物だけが、targetを通してpostgresqlにアクセスできるようになるはずです。
roleにはgrant_scopeという設定があり、定義したロールがどのスコープで有効になるかを指定する必要があります。Users、Groups、Roles、Auth Methodsに関する権限を設定する場合はOrgをgrant scope idに設定する必要がになります。一方Targets、Host Catalogs、Host、HostSetsはProjectをgrand scope idになります。
これで定義としてはあっているはず...ですがgrantにtarget-idを指定した場合、大文字を含むidでもすべて小文字で構成されたidに
対するgrantが作成されてしまうため、現在ではまだgrantをidで絞ることはできません。リソースに対するアクセス制御を行う場合はProjectを別にして、Roleに割り当てるGrant Scope Idを別のものにする必要があります。
これでユーザーに割り当てられたグループ毎にアクセスできるリソースを制御できるということが分かったと思います。

# まとめ

HashiCorp Boundaryはまだ0.1.0と未成熟なプロダクトです。ただ動的なアクセス制御とその管理を行うプロダクトとしてはまだ未来に希望が持てます。現状ではBoundary自身が管理するパスワードでしか
認証が行えませんが、Github,Azure,AWS,Okta等のIDプロバイダーでの認証が行えるようになれば、もっと便利なプロダクトになるのではないかと思いました。