最近、5kgの重しを背負って懸垂していますプリンシパルエンジニアの @linyows です。 みなさん、NHKの「筋肉は裏切らない!」(正しくは筋肉体操)見てますか? … はい。ですよね。
GMOペパボは、ロリポップ!やminneなど様々なサービス(microservices的なものではない)を運営しており、それぞれにおいて、秘密情報を管理している状態にあります。この秘密情報を独自に管理するのは、専門のスキルやノウハウとしてあらゆるコストがかかります。
そこで僕(ら)は、秘密情報管理ソフトウェアであるHashiCorp Vaultを各サービスに導入し、サービスを堅牢にするというゴールをもとに社内でVaultのWorkshopを幾度となく開催しています。
今回は、そのWorkshop「やわらかVault」のご紹介です。
やわらかVault
これは、HashiCorp Vaultを正しく理解して、実務で導入/運用をできるようにするワークショップです。 なお、なぜ秘密情報管理すべきなのかに関しては、WEB+DB PRESS Vol.104を 買って「Hashicorp Vaultで秘密情報管理」の記事を読んでおくとバッチリでしょう。
目次
- はじめに
- 座学
- Vaultとは
- Vaultのアーキテクチャ
- Vaultのセキュリティに対する設計
- ハンズオン
- Vaultの冗長構成
- Vaultで通信を安全にする
- VaultでDatabaseに素の個人情報を入れないようにする
- VaultでWrite権限ユーザを発行する
- おわりに
はじめに
本ワークショップのゴール
- Vaultが何なのか説明できるようになる
- Vaultの
運用操作ができるようになる - 実務においてVaultの使い所、使うべき所がわかる
座学
Vaultを使う上で必要な基礎知識をお話しします。
Vaultとは
- DevOpsなソフトウェアを作り出すHashiCorpの秘密情報管理のソフトウェア
- 平たくいうと暗号化されたKey-Value Store
- たくさんの機能とともにセキュリティに対するプラクティスが詰まっている(最小権限の原則や職務分掌など)
- ほかのHashicorpソフトウェアと併用することで秘密情報管理を自動化できる
Vaultのアーキテクチャ
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を使用します。
ご注意
- 環境変数を使う前提で進みますので、複数のターミナルを使うと環境変数がなくてエラーというのに気をつけてください
- 手元のアプリとポートが被っているとコンテナが起動しない場合があるので確認しながら進めてください
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キーを設定し、設定したキーに対して暗号化/復号する仕組み
- 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ソフトウェア」と題しましてお話しいたします。既に定員に達してはおりますが、当日キャンセルが出る可能性もございますので興味があればご参加ください。