hashicorp vault consul workshop docker

やわらかVault

hashicorp vault consul workshop docker

最近、5kgの重しを背負って懸垂していますプリンシパルエンジニアの @linyows です。 みなさん、NHKの「筋肉は裏切らない!」(正しくは筋肉体操)見てますか? … はい。ですよね。

GMOペパボは、ロリポップ!やminneなど様々なサービス(microservices的なものではない)を運営しており、それぞれにおいて、秘密情報を管理している状態にあります。この秘密情報を独自に管理するのは、専門のスキルやノウハウとしてあらゆるコストがかかります。

そこで僕(ら)は、秘密情報管理ソフトウェアであるHashiCorp Vaultを各サービスに導入し、サービスを堅牢にするというゴールをもとに社内でVaultのWorkshopを幾度となく開催しています。

今回は、そのWorkshop「やわらかVault」のご紹介です。

やわらかVault

これは、HashiCorp Vaultを正しく理解して、実務で導入/運用をできるようにするワークショップです。 なお、なぜ秘密情報管理すべきなのかに関しては、WEB+DB PRESS Vol.104買って「Hashicorp Vaultで秘密情報管理」の記事を読んでおくとバッチリでしょう。

目次

  1. はじめに
  2. 座学
    1. Vaultとは
    2. Vaultのアーキテクチャ
    3. Vaultのセキュリティに対する設計
  3. ハンズオン
    1. Vaultの冗長構成
    2. Vaultで通信を安全にする
    3. VaultでDatabaseに素の個人情報を入れないようにする
    4. VaultでWrite権限ユーザを発行する
  4. おわりに

はじめに

本ワークショップのゴール

  • Vaultが何なのか説明できるようになる
  • Vaultの運用操作ができるようになる
  • 実務においてVaultの使い所、使うべき所がわかる

座学

Vaultを使う上で必要な基礎知識をお話しします。

Vaultとは

  • DevOpsなソフトウェアを作り出すHashiCorpの秘密情報管理のソフトウェア
  • 平たくいうと暗号化されたKey-Value Store
  • たくさんの機能とともにセキュリティに対するプラクティスが詰まっている(最小権限の原則や職務分掌など)
  • ほかのHashicorpソフトウェアと併用することで秘密情報管理を自動化できる

Vaultのアーキテクチャ

architecture

via: https://www.vaultproject.io/docs/internals/architecture.html

  • KVS(ストレージ)という括りで、認証/秘密/監査/ポリシー という仕組み
  • Seal/Unsealという仕組みがあってひとたびSealになると分散した鍵が閾値数以上つかってUnsealすることになる
  • HTTP APIを通じてあらゆる機能を操作する
  • ポリシーはファイル管理なのでgitなどでバージョン管理できる

Vaultのセキュリティに対する設計

  • Vault内部は全て暗号化され暗号化の鍵は外へ出せない
  • 暗号化の鍵利用はマスターキーにより使用でき、マスターキーは分割して保管できる
  • ポリシーによって秘密情報へのアクセス権限のスコープを小さくすること、監査ログをとること、でインシデントのインパクトを最小限にする
  • 秘密情報のTTLを短く設定しrenewという延長処理を行って運用する
  • 秘密情報の権限は子孫関係があり親をrevokeすると伝播してrevokeされる

ハンズオン

実際に手を動かし、Vaultの機能を体験してみましょう。今回作るシステムの全体図は以下の通りです。

HA構成にしたVaultのバックエンドとしてConsul Serverをたて、Consul DNSで割り当てられたVaultにアクセスするためのドメインでTLS通信するために、VaultをPKIとして機能させます。次に、WebアプリケーションからDatabaseに接続する資格情報をVaultから発行し、Webアプリケーションへ入力された個人情報をVaultを利用して暗号化/復号を行うシステムです。各秘密情報の自動化に関してはconsul-templateを使い、各コンテナ上に複数プロセス動かすのでsupervisordを使用します。

architecture-for-handon

ご注意

  • 環境変数を使う前提で進みますので、複数のターミナルを使うと環境変数がなくてエラーというのに気をつけてください
  • 手元のアプリとポートが被っているとコンテナが起動しない場合があるので確認しながら進めてください

Step.1 – 準備

まず、dockerコンテナを使ってのハンズオンなので、以下のdockerイメージをpullしておきます。

$ docker pull linyows/consul:1.2-agent
$ docker pull linyows/consul:1.2-server
$ docker pull linyows/consul:1.2-node

次に、VaultとConsulをclientとして使うのでhomebrewからインストールしておきます。また、Vault CLIを補完出来るようにしておくと便利です。

$ brew install vault consul
$ vault -autocomplete-install

そして、使用するファイル群をgit cloneします。

$ git clone https://github.com/linyows/vault-yawaraka.git
$ cd vault-yawaraka

Step.2 – Vaultの冗長構成

  • ストレージバックエンドにConsulを使用
  • HA構成のVault同士はストレージを通じてコミュニケーション
  • VIPをシェアする必要ないしstandbyにrequestを投げてもmasterにredirectされる
  • HA構成にするには各VaultをUnsealにしておく必要

consul serverコンテナを起動する

今回作るコンテナはconsul clusterとして起動するので、まず最初にConsul serverを起動する

$ docker run -p 8500:8500 --name consul -h consul -d linyows/consul:1.2-server
$ open http://localhost:8500/ui

vaultコンテナを起動する

イメージをビルドしてVaultを2台起動する

$ cd ./vault
$ docker build -t linyows/vault .
$ ip=$(docker inspect -f "{{.NetworkSettings.IPAddress}}" consul)
$ docker run -p 8200:8200 --dns=127.0.0.1 --dns=8.8.8.8 --add-host=consul:$ip --name vault-1 -h vault-1 \
  -e VAULT_REDIRECT_ADDR=https://vault.service.consul:8200 -d linyows/vault
$ docker run -p 8201:8200 --dns=127.0.0.1 --dns=8.8.8.8 --add-host=consul:$ip --name vault-2 -h vault-2 \
  -e VAULT_REDIRECT_ADDR=https://vault.service.consul:8200 -d linyows/vault

# この状態をconsulから確認してみる
$ export CONSUL_HTTP_ADDR=localhost:8500
$ consul members
Node     Address          Status  Type    Build  Protocol  DC   Segment
consul   172.17.0.2:8301  alive   server  1.2.0  2         dc1  <all>
vault-1  172.17.0.3:8301  alive   client  1.2.0  2         dc1  <default>
vault-2  172.17.0.4:8301  alive   client  1.2.0  2         dc1  <default>

vaultの初期化と設定を行う

# localhostで信頼できるのでverify skip
$ export VAULT_SKIP_VERIFY=true
$ export VAULT_ADDR=https://localhost:8200

# 初期化で大事な情報が出力されます
$ vault operator init | tee keys
Unseal Key 1: SNvELrGymFguSf9rb4L9mpF0/0Yg+sBBjwzE54Ig+xmo
Unseal Key 2: s6fDWUERp032ffcyt7/nOIl4HxC2WLRBa8YMe9rV2zuO
Unseal Key 3: X+GMJ9yUO/foxRFWXtWFN2MMfEqQ4kZpfg3pi7tP0l+u
Unseal Key 4: IX6UEDy+0ksp5GtKIGutcbcLT5os3+0jsmc5Tappcuy9
Unseal Key 5: H8uWa1oNQUgax3skFM7WxSZHNEoMXjKXWsWMHQOqqpiL

Initial Root Token: 3f20a2c3-dfbf-2abf-022d-7e1cb887781b

# 閾値までunseal
$ vault operator unseal

# 状態確認 => active
$ vault status
Key             Value
---             -----
Seal Type       shamir
Sealed          false
Total Shares    5
Threshold       3
Version         0.10.3
Cluster Name    vault-cluster-81b8b861
Cluster ID      499c5059-23d5-04d3-2f92-6977d154e212
HA Enabled      true
HA Cluster      https://vault.service.consul:8201
HA Mode         active

# unsealされていないとクラスタにjoinしないので2台目も設定
$ export VAULT_ADDR=https://localhost:8201

# おなじく閾値までunseal
$ vault operator unseal

# 状態確認 => standby
$ vault status
Key                    Value
---                    -----
Seal Type              shamir
Sealed                 false
Total Shares           5
Threshold              3
Version                0.10.3
Cluster Name           vault-cluster-81b8b861
Cluster ID             499c5059-23d5-04d3-2f92-6977d154e212
HA Enabled             true
HA Cluster             https://vault.service.consul:8201
HA Mode                standby
Active Node Address    https://vault.service.consul:8200

# ハンズオンでは便宜上root tokenを使います `~/.vault-token`
$ vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                3f20a2c3-dfbf-2abf-022d-7e1cb887781b
token_accessor       be43dba5-7b41-c861-4e92-79606c3c816a
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

おかわり 🍚

Vaultコンテナを全部削除し、再作成したらどうなるか? 🤔

$ docker stop vault-1 && docker rm vault-1
$ docker stop vault-2 && docker rm vault-2

Step.3 – Vaultで通信を安全にする

  • サービスの大小あるが内部通信は必ずある
  • 暗号化はしても通信先が正しいかどうかは疎かにされがち
  • VaultをRoot CAまたはIntermediate CAとして機能させる
  • Let's Encryptでもよいが、各設定が可能なVaultを自動化するほうが便利

PKI secretの設定

# Root CAを有効にする(pkiは自分でdescriptionを設定しないとsecrets listでn/aと表示される)
$ vault secrets enable -description="root ca" pki

# TTLの最大値を設定しておく
$ vault secrets tune -max-lease-ttl=87600h pki/

# CAのcertificateとprivate keyを作成する
$ vault write pki/root/generate/internal common_name=service.consul ttl=8760h

# 証明証発行とCRLのパスを変更
$ vault write pki/config/urls issuing_certificates="https://vault.service.consul:8200/v1/pki/ca" \
  crl_distribution_points="https://vault.service.consul:8200/v1/pki/crl"

# 証明証を発行するロールを作成する
$ vault write pki/roles/service-dot-consul allowed_domains=service.consul allow_subdomains=true max_ttl=72h

# 証明証を発行
$ vault write pki/issue/service-dot-consul common_name=vault.service.consul

consul-templateを利用する

# 名前解決しつつ、証明書のエラーが出ることを確認する
$ docker exec -it vault-1 curl https://vault.service.consul:8200/v1/

# supervisordの状態確認
$ docker exec -it vault-1 supervisorctl status
consul                           RUNNING   pid 10, uptime 0:07:44
consul-template                  STOPPED   Not started
vault                            RUNNING   pid 9, uptime 0:07:44

# consul-templateが使うtokenを設定するためにlogin
$ docker exec -it vault-1 vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                052f8bdc-738b-5032-4074-7443e206694f
token_accessor       8528fe1a-0a04-b8c1-93b0-b4314992d9ba
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

# consul-templateを起動する
$ docker exec -it vault-1 supervisorctl start consul-template

# 状態確認
$ docker exec -it vault-1 supervisorctl status
consul                           RUNNING   pid 10, uptime 0:13:17
consul-template                  RUNNING   pid 101, uptime 0:04:00
vault                            RUNNING   pid 9, uptime 0:13:17

# 名前解決しつつ、証明書のエラーが出ないかを確認する
$ docker exec -it vault-1 curl https://vault.service.consul:8200/v1/
{"errors":[]}

vault-2も同様の手順を行う

Step.4 – VaultでDatabaseのユーザを発行する

  • databaseシークレットは様々なdatabaseの資格情報をVaultが動的に管理する
  • ユーザやアプリケーションはTTLつきの資格情報をVaultから取得する
  • 資格情報cd の動的な管理により、資格情報のハードコードがなくなる

イメージをビルドして起動

$ cd ../mysql
$ docker build -t linyows/mysql .
$ docker run -e MYSQL_DATABASE=express -p 13306:3306 --dns=127.0.0.1 --dns=8.8.8.8 \
  --add-host=consul:$ip --name mysql -h mysql -d linyows/mysql

# 状態確認
$ consul members
Node     Address          Status  Type    Build  Protocol  DC   Segment
consul   172.17.0.2:8301  alive   server  1.2.0  2         dc1  <all>
mysql    172.17.0.5:8301  alive   client  1.2.0  2         dc1  <default>
vault-1  172.17.0.3:8301  alive   client  1.2.0  2         dc1  <default>
vault-2  172.17.0.4:8301  alive   client  1.2.0  2         dc1  <default>

database secretを有効にする

$ vault secrets enable database
Success! Enabled the database secrets engine at: database/

# プラグインの指定とDatabaseへの接続情報を設定する
$ vault write database/config/mysql-database plugin_name=mysql-database-plugin \
  connection_url="{{username}}:{{password}}@tcp(mysql.node.consul:3306)/" allowed_roles="express" username="root" password="secret"

# ロールの作成とロールのTTLや発行するコマンドの設定
$ vault write database/roles/express db_name=mysql-database default_ttl="1h" max_ttl="24h" \
  creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT ALL ON *.* TO '{{name}}'@'%';"
Success! Data written to: database/roles/express

# ユーザ発行
$ vault read database/creds/express
Key                Value
---                -----
lease_id           database/creds/express/d2f89c12-1df5-66d1-d62e-a562ed79fbef
lease_duration     1h
lease_renewable    true
password           A1a-2T4GrUg0sAZREKrV
username           v-root-express-eAyhwKO10Ma0ceDl3

# 発行された資格情報から接続確認
$ mysql -h 127.0.0.1 -P 13306 -u v-root-express-eAyhwKO10Ma0ceDl3 -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.5.5-10.1.32-MariaDB MariaDB Server

Copyright (c) 2000, 2017, 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>

Step.5 – VaultでAppからDatabaseに素の個人情報を入れないようにする

transit

  • Transitシークレットとは、transitキーを設定し、設定したキーに対して暗号化/復号する仕組み
  • HMAC暗号
  • 同じキーでもcontext(別アプリケーションの復号するシーンが違う場合)で暗号用の鍵を派生鍵として利用できる
  • Rails用のGemはHashiCorpが出している
  • NodeのSequelize版を作った

イメージをビルドして起動

$ cd ../express
# CA公開鍵をイメージに含める
$ curl -H "X-Vault-Token: $(cat ~/.vault-token)" -s -X PUT -k -d '{"common_name":"vault.service.consul"}' \
  https://127.0.0.1:8200/v1/pki/issue/service-dot-consul | jq  -r '.data.issuing_ca' > ca.crt

# ビルド
$ docker build -t linyows/express .

# トークンを環境変数に設定する
$ docker run -e VAULT_TOKEN=$(cat ~/.vault-token) -e VAULT_ADDR=https://vault.service.consul:8200 \
  -e DB_HOST=mysql.node.consul -e DB_PORT=3306 \
  -p 3000:3000 --dns=127.0.0.1 --dns=8.8.8.8 --add-host=consul:$ip --name express -h express -d linyows/express

# ビルド時にcaの公開鍵を配置しているので、Vaultサーバを認証できる確認
$ docker exec -it express curl https://vault.service.consul:8200/v1/
{"errors":[]}

# 状態確認
$ consul members
Node     Address          Status  Type    Build  Protocol  DC   Segment
consul   172.17.0.2:8301  alive   server  1.2.0  2         dc1  <all>
express  172.17.0.6:8301  alive   client  1.2.0  2         dc1  <default>
mysql    172.17.0.5:8301  alive   client  1.2.0  2         dc1  <default>
vault-1  172.17.0.3:8301  alive   client  1.2.0  2         dc1  <default>
vault-2  172.17.0.4:8301  alive   client  1.2.0  2         dc1  <default>

transit secretを設定

# 有効化
$ vault secrets enable transit
Success! Enabled the transit secrets engine at: transit/

# 暗号化を一意にするには以下の2つのオプションが必要になる
$ vault write -f transit/keys/express_users_email convergent_encryption=true derived=true
Success! Data written to: transit/keys/express_users_email

consul-templateによって得られたDatabase接続情報をもとにexpressを起動

$ docker exec -it express supervisorctl start consul-template
consul-template: started

$ docker exec -it express supervisorctl status
consul                           RUNNING   pid 9, uptime 0:03:27
consul-template                  RUNNING   pid 48, uptime 0:00:21
express                          STOPPED   Not started

# consul-templateにより作成されたDatabase接続情報を確認
$ docker exec -it express cat .env
DB_USER='v-root-express-yJYCFHG8lHIrQUjW1'
DB_PASS='A1a-68LCi7xme2qpijfY'

$ docker exec -it express supervisorctl start express
express: started

$ docker exec -it express supervisorctl status
consul                           RUNNING   pid 9, uptime 0:06:11
consul-template                  RUNNING   pid 51, uptime 0:02:01
express                          RUNNING   pid 99, uptime 0:00:08

# webで暗号化/復号を確認してみましょう
$ open http://localhost:3000/

# 実際に暗号化したもののみが保存されているか確認しよう
# mysql -u root -h localhost -P 13306 -p express
> select * from users;

Step.6 – GitHub認証

  • 認証する対象が違うケースがあるので、ユーザやアプリケーション用など分けられている
  • GUIを通して認証し、認証ユーザのpolicyをさわってみよう
# 有効化
$ vault auth enable github

# GitHubの認証に使う組織を指定
$ vault write auth/github/config organization=yourorg
# GitHub EnterpriseであればAPI urlを設定
$ vault write auth/github/config organization=yourorg base_url=https://ghe.yourdomain.com/api/v3/

# 既存ポリシーの確認
$ vault read sys/policy
Key         Value
---         -----
keys        [default root]
policies    [default root]

# ポリシーを作成
cat <<EOF > dev.hcl
path "pki/*" {
  capabilities = [ "create", "read", "update", "delete", "list" ]
}

path "auth/token/*" {
  capabilities = [ "create", "read", "update", "delete", "list" ]
}

path "sys/policy/*" {
  capabilities = [ "create", "read", "update", "delete", "list" ]
}

path "sys/auth/*" {
  capabilities = [ "create", "read", "update", "delete", "sudo" ]
}

#path "transit/*" {
#  capabilities = [ "create", "read", "update", "delete", "list" ]
#}
EOF

# ポリシーを登録する
vault policy write dev dev.hcl

# チームにポリシーを割り当てる
$ vault write auth/github/map/teams/yourteam value=dev

# GitHubのpersonal access tokenを発行する(admin:orgに権限付与)
$ open https://github.com/settings/tokens

# vault uiのログインでGitHub tokenを使ってログインしてみる(ローカルなので証明書のエラーは無視)
$ open https://localhost:8200/ui/

# policyのtransitのコメントアウトを戻しVaultに再読み込みしたらGUIはどう表示されるか

おわりに

ハンズオンなので、便宜上rootトークンを使い回しましたが、実際の導入では、各トークンをpolicyと紐づけて発行して利用することになります。また、導入するシステムに応じて、鍵やトークンの受け渡しをまず最初にどうするか、一連のルーチンをどう自動化するかを設計することが重要になります。

Vaultは秘密情報管理という特性上、比較的とっつきにくいソフトウェアだと思います。しかし、これを使いこなすことによってシステムがより堅牢になるのは間違いないので、脆弱なところ、導入しやすいところなどから取り組んでいきましょう。

これらのハンズオンをもっと簡略化したdocker-compose版も用意していますので、みなさんも是非試してはいかがでしょうか?


宣伝: 第3回 HashiCorpジャパンMeetup に登壇

9月11日 19時から渋谷ヒカリエにおいて、HashiCorpジャパンMeetupが開催されます。

タイトルは「DevOpsを支える今話題のHashiCorpツール群について」で、私 @linyows も登壇して「コンテナ基盤を支えるHashiCorpソフトウェア」と題しましてお話しいたします。既に定員に達してはおりますが、当日キャンセルが出る可能性もございますので興味があればご参加ください。