モチベーション

sensu-chefのSSLサーバ証明書の自動生成スクリプトがしていることを把握して,流用可能か判断する.

記事概要

Sensuという監視のためのフレームワーク使って監視システムを構築する時,ChefやPuppetなどのConfiguration Managementツールを使うことが推奨されている.僕は普段からChefを使っているためSensuが公式に用意しているcookbook:sensu-chefを利用している.このsensu-chefはSensu serverとSensu clientとの通信にSSLを使うように実装されているため証明書の用意をする必要がある.一応,sensu-chefにはSSLで通信するときに必要な証明書を作成するためのスクリプトも用意されているのだが,その中身は果たして安全なのだろうか? スクリプトの処理内容を確認して,構築予定の監視サーバでどのように証明書を準備するか判断しようと思う,

Sensuの論理アーキテクチャとSSL通信

Sensuの論理アーキテクチャは図の通り.今回整理するのは,Sensu serverとclientのMessage Queueとして利用しているRabbitMQとの通信に利用する証明書の作成スクリプトです.これは,RabbitMQのSSL Supportに書かれている内容を自動で処理するためのもの.

20150315_sensu_logical_architecture

証明書の自動生成スクリプトを読んでいく

sensu-chefのREADMEによると, >Running Sensu with SSL is recommended; this cookbook uses a data bag sensu, with an item ssl, containing the SSL certificates required. Sensu data bag items may be encrypted. This cookbook comes with a tool to generate the certificates and data bag item. If the integrity of the certificates is ever compromised, you must regenerate and redeploy them.

と書かれている.Sensu server&client間の通信はSSLを利用することが推奨されており,sensu-chefのrecipeでは証明書情報はChefのdata bagから取得するとのこと.この証明書を作成するためのプログラムがexamples/ssl配下のssl_certs.shとして用意されている.sensu-chefの証明書自動生成スクリプトはご覧の通り.

generate() {
①  mkdir -p client server sensu_ca/private sensu_ca/certs
   touch sensu_ca/index.txt
   echo 01 > sensu_ca/serial
   cd sensu_ca
   openssl req -x509 -config openssl.cnf -newkey rsa:2048 -days 1825 -out cacert.pem -outform PEM -subj /CN=SensuCA/ -nodes
   openssl x509 -in cacert.pem -out cacert.cer -outform DER
②  cd ../server
   openssl genrsa -out key.pem 2048
   openssl req -new -key key.pem -out req.pem -outform PEM -subj /CN=sensu/O=server/ -nodes
   cd ../sensu_ca
   openssl ca -config openssl.cnf -in ../server/req.pem -out ../server/cert.pem -notext -batch -extensions server_ca_extensions
   cd ../server
   openssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass:secret
③  cd ../client
   openssl genrsa -out key.pem 2048
   openssl req -new -key key.pem -out req.pem -outform PEM -subj /CN=sensu/O=client/ -nodes
   cd ../sensu_ca
   openssl ca -config openssl.cnf -in ../client/req.pem -out ../client/cert.pem -notext -batch -extensions client_ca_extensions
   cd ../client
   openssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass:secret
④  cd ../
   ./generate_databag.rb
}

# 実行結果
➤  ./ssl_certs.sh generate
①
Generating a Sensu SSL data bag item ...
Generating a 2048 bit RSA private key
....+++
....................................................................+++
writing new private key to './private/cakey.pem'
-----

②
Generating RSA private key, 2048 bit long modulus
..............................+++
......................................+++
e is 65537 (0x10001)
Using configuration from openssl.cnf
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :PRINTABLE:'sensu'
organizationName      :PRINTABLE:'server'
Certificate is to be certified until Mar 13 02:18:23 2020 GMT (1825 days)

③
Write out database with 1 new entries
Data Base Updated
Generating RSA private key, 2048 bit long modulus
......+++
.......................+++
e is 65537 (0x10001)
Using configuration from openssl.cnf
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :PRINTABLE:'sensu'
organizationName      :PRINTABLE:'client'
Certificate is to be certified until Mar 13 02:18:23 2020 GMT (1825 days)

④
Write out database with 1 new entries
Data Base Updated
Data bag item created: ssl.json

これを見ると,大きく分けて以下の4つの処理を行っている事がわかる.

  • ① sensu_caの秘密鍵と自己証明書の作成
  • ② sensu serverの秘密鍵とSSLサーバ証明書の作成
  • ③ sensu clientの秘密鍵とSSLサーバ証明書の作成
  • ④ 秘密鍵,証明書情報を含んだsensu.jsonの作成

処理内容まで理解しなくていいや,って人も多いかもしれないけどセキュリティの観点から証明書の自動生成で何が起きているのかを知っておくのは重要なこと.処理内容で問題がないのであればそのまま使えるし,問題が有るなら自分なりに何か他のプログラムを作成しようと思う.

SSLで使う証明書の作成手順のおさらい

処理内容を読んで行く前にSSLサーバ証明書が作成される流れを復習しよう.SSLサーバ証明書は通信の暗号化と通信相手の身元の証明の2つの役割を持つ.このSSLサーバ証明書の作成から配布の一般的な作成手順は以下の通り.

  1. 証明書依頼者が依頼者の公開鍵を含んだ証明書要求書(CSR)の作成する
  2. 認証局(CA)がCSRを受け取り,認証局の秘密鍵で証明書要求に署名する
  3. 証明書依頼者が署名された証明書要求を受け取りそれをSSLサーバ証明書とする
  4. クライアントとSSLで通信するときはサーバ証明書を配布する

次は処理内容を見ていく.

① sensu_caの秘密鍵と自己証明書の作成

まずはsensu_caの自己証明書作成の処理を見ていく.以下ではsensu_caの秘密鍵(examples/private/cakey.pem)と証明書(examples/cacert.pem)の作成をする.

$openssl req -x509 -config openssl.cnf -newkey rsa:2048 -days 1825 -out cacert.pem -outform PEM -subj /CN=SensuCA/ -nodes

openssl reqの使い方はOpenSSL: Documents, req(1)に詳しく書かれている.ドキュメントより,openssl reqで使われているオプションはそれぞれ以下の意味を持つことがわかる.

  • openssl req -new:CSRの作成
  • -x509: X.509形式でCSRを作成する
  • -config:引数で指定したopenssl.cnfファイルを利用する
  • -newkey: 新しい秘密鍵と署名要求を作成する
  • -days: X.509形式のCSRの有効期限のを指定する
  • -out: CSR,CRTの出力先
  • -ourtform: 出力形式を指定する.
  • -subj: CSR作成時に入力する情報を引数で渡す
  • -nodes:秘密鍵を暗号化しない(ノードではなくNo DES)

通常openssl req -newはCSRの作成に使われるし,SSLの証明書作成の手順で確認した通り証明書は認証局にCSRを提出して承認した結果もらえるものです.しかし,ここではsensu_caという名前の自己認証局を作成して以下の処理をしています.

  1. sensu_caの秘密鍵を作成
  2. sensu_caのCSRを作成
  3. sensu_caの秘密鍵でCSRに署名して自己証明書の作成

一見実行しているコマンドは秘密鍵の指定やCSRの作成に関する記述が無いので不足しているように思えますが,openssl.cnfというファイルにあらゆる情報を含ませています.例えば,秘密鍵の保存先はprivate_key = $dir/private/cakey.pemと指定しています.他にも,openssl.cnfに記載されている情報を利用して自己証明書の作成をしてたりします.openssl.cnfの書き方は本家のページに記載されている.また,openssl.cnf 日本語解説版を用意してくださった方がいるのでこちらも参考になる.sensu-chefで用意されているopenssl.cnfは以下の通り.

[ ca ]
default_ca = sensu_ca # デフォルトCAセクションの指定

[ sensu_ca ]
dir = . # CAのホームディレクトリの指定
certificate = $dir/cacert.pem  # CAの証明書
database = $dir/index.txt # シリアルと発行した証明書のインデックス
new_certs_dir = $dir/certs # 新しく自分が発行した証明書を置く
private_key = $dir/private/cakey.pem # CAの秘密鍵
serial = $dir/serial # 次に発行する証明書に付けられるシリアル

default_crl_days = 7 # CRLの収集間隔
default_days = 1825 # 証明書のデフォルトの有効期限
default_md = sha1 # メッセージダイジェスト(ハッシュ関数)の指定

policy = sensu_ca_policy # 使用するポリシーの指定
x509_extensions = certificate_extensions # 	x509のv3拡張の定義セクションを指定

[ sensu_ca_policy ]
commonName = supplied # 一般名(CN):空欄不可
stateOrProvinceName = optional # 州名,県名(S):任意
countryName = optional # 国名(C):任意
emailAddress = optional # email:任意
organizationName = optional # 団体名(OU)
organizationalUnitName = optional(OU):任意

[ certificate_extensions ]
basicConstraints = CA:false #[CAの証明書を発行できる場合はtrue,サーバ,クライアント証明書のみを発行する場合はfalseを指定する](http://goo.gl/yTefRE)

[ req ] # 証明書発行要求(CSR)に関するデフォルト値設定
default_bits = 2048 # 公開鍵の鍵長
default_keyfile = ./private/cakey.pem # -inで鍵を指定しない時の秘密鍵
default_md = sha1 # メッセージダイジェストの指定
prompt = yes # 
distinguished_name = root_ca_distinguished_name # 識別名(DS)の情報を指定するセクションを定義
x509_extensions = root_ca_extensions # x509のv3拡張の定義セクションを指定

[ root_ca_distinguished_name ]
commonName = sensu # SSL接続するサイトのURL(FQDN)

[ root_ca_extensions ]
basicConstraints = CA:true # CAなのでtrueを指定
keyUsage = keyCertSign, cRLSign #X.509で定義されている証明書の使い道について (KeyCertSign:鍵の署名,cRLSing:証明書失効リストの検証)

[ client_ca_extensions ]
basicConstraints = CA:false
keyUsage = digitalSignature # 鍵をデジタル署名に利用する
extendedKeyUsage = 1.3.6.1.5.5.7.3.2 # clientAuth (1.3.6.1.5.5.7.3.2) -- TLS Web client authenticationのOIDを指定

[ server_ca_extensions ]
basicConstraints = CA:false
keyUsage = keyEncipherment # 鍵を鍵配布に使用
extendedKeyUsage = 1.3.6.1.5.5.7.3.1 # serverAuth (1.3.6.1.5.5.7.3.1) -- TLS Web server authenticationのOIDを指定

openssl.cnfはいくつかのセクションから構成されているが,通常一番最初のセクションが読まれて,その中で他のセクションが読み込まれます.つまり,今回で言えば,[ca]セクションが読まれた後,[ca]セクションの中で[sensu_ca]セクションが呼び出されます.このように繰り返しセクションが読み込まれ,最終的には,[ certificate_extensions ]まで読み込まれます.また,Openssl.conf Walkthruによると[req]セクションを設けることでopenssl reqコマンドを実行した時に使用する値を定義することができるとのこと.なので,[ certificate_extensions ]まで読み込んだら,[req]セクションに飛んでファイルの読み込みをすることになる.

sensu_caでは以下のコマンドも実行している.

openssl x509 -in cacert.pem -out cacert.cer -outform DER

これは,作成した証明書をバイナリー(DER)フォーマットしているのだが,このフォーマットされた証明書はrecipe内では使われていないです.WindowsサーバでRabbitMQを建てる人とかは必要なのかな?とりあえず,LinuxサーバではDERフォーマットのファイルは要らないです.

Tips:unable to write ‘random state’の表示が出る

$openssl req -x509 -config openssl.cnf -newkey rsa:2048 -days 1825 -out cacert.pem -outform PEM -subj /CN=SensuCA/ -nodesを実行した結果以下のメッセージが表示される時がある.

Generating a 2048 bit RSA private key
.............................................................................+++
...+++
unable to write 'random state'
writing new private key to './private/cakey.pem'

この場合は,ホームディレクトリ配下の.rndファイルを削除する.これは,秘密鍵の元になる乱数を与えるためのデータ.(参考:Using openssl what does “unable to write ‘random state’” mean? [closed])

② sensu serverの秘密鍵とSSLサーバ証明書の作成

続いて,sense serverの証明書作成手順を見ていく.まずはsensu serverの秘密鍵(examples/server/key.pem)とCSR(examples/server/req.pem)を作っている.

openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -out req.pem -outform PEM -subj /CN=sensu/O=server/ -nodes
  • -batch: インタラクティブモードにしないオプション
  • -extensions: openssl.cnfで引数と同じ名前のセクションを読み込む

続いて,作成したCSRをsensu_caで認証してsensu serverの証明書を作成する.

cd ../sensu_ca
openssl ca -config openssl.cnf -in ../server/req.pem -out ../server/cert.pem -notext -batch -extensions server_ca_extensions
cd ../server
openssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass:secret

examples/server/cert.pemが作成した証明書になる.作成した,examples/server/key.pemexamples/server/cert.pemは中身をRabbitMQのSSL通信関連の設定に引き渡せるように後述のssl.jsonに格納する.また,sense serverの秘密鍵と証明書をまとめたPKCS12ファイルを作成しているが,これも秘密鍵と証明書を1つのファイルにまとめて後で取り出せるようにしているだけ.なので,pass::secretと部分は必要に応じて変えること.

③ sensu clientの秘密鍵とSSLサーバ証明書の作成

sense clientのためのスクリプト処理はserverと全く同じなので読み替えて下さい.

④ 秘密鍵,証明書情報を含んだssl.jsonの作成

最後の処理では,①&②&③で作成した情報を含めるssl.jsonの作成している.generate_databag.rbの内容を読むとssl.jsonの作成方法がわかる.

#!/usr/bin/env ruby

require 'rubygems'
require 'json'

def process_pem(filename)
  output = ""
  File.open(filename).each_line do |line|
    output << line
  end
  output
end

content = {
  :id => "ssl",
  :server => {
    :key => process_pem("server/key.pem"),
    :cert => process_pem("server/cert.pem"),
    :cacert => process_pem("sensu_ca/cacert.pem")
  },
  :client => {
    :key => process_pem("client/key.pem"),
    :cert => process_pem("client/cert.pem")
  }
}

File.open("ssl.json","w") do |data_bag_item|
  data_bag_item.puts JSON.pretty_generate(content)
end

puts "Data bag item created: ssl.json"

ここで作られた,ssl.jsonを/path/to/sensu-recipe/data_bags/sensu/sensu.jsonに格納すればsensu-chefのcookbookの適用準備が完了する.

Tips:knife data bag create sensuを実行しないのはなぜ?

sensu-chefの公式手順だと③が終わったら以下を実行する.

cd examples/ssl
./ssl_certs.sh generate
knife data bag create sensu
knife data bag from file sensu ssl.json

これにより,暗号化されたssl.jsonがdata bagで利用できるようになるのだが,knife soloで運用すること前提にしている人は恐らくこんな感じに失敗するだろう.

➤  knife data bag create sensu
ERROR: Your private key could not be loaded from /etc/chef/client.pem
Check your configuration file and ensure that your private key is readable

これは,僕がchef serverではなくchef soloを使っているからです.今のところ,僕はchef soloを使った運用をしたいのでknife data bag create senseコマンドは失敗する. お気づきの通り,秘密鍵の情報が平文で設定ファイルに記載されているのはかなりまずい.chef soloとencrypted data bagの同時利用の方法もこの辺とか,この辺とか,こことかを参考に組み込む必要がある.ということで,暗号化したssl.jsonを使う場合は以下のようにencrypted_data_bagを使う.

乱数を使って鍵を生成する.

openssl rand -base64 512 > data_bag_key

.chef/knife.rbのencrypted_data_bag_secretのコメントアウトを外す.

➤  cat .chef/knife.rb
cookbook_path    ["cookbooks", "site-cookbooks"]
node_path        "nodes"
role_path        "roles"
environment_path "environments"
data_bag_path    "data_bags"
encrypted_data_bag_secret "data_bag_key"

knife[:berkshelf_path] = "cookbooks"

ローカルモードで実行するようにオプションに-zをつける.

➤  knife data bag create test -z
Created data_bag[test]


# ちなみにzを付けないと失敗します.
➤  knife data bag create test2
ERROR: Your private key could not be loaded from /etc/chef/client.pem
Check your configuration file and ensure that your private key is readable

以下のコマンドでssl.jsonを暗号化する.

knife data bag from file sense examples/ssl/ssl.json -z
➤  cat data_bags/sensu/ssl.json
{
  "id": "ssl",
  "server": {
    "encrypted_data": "暗号化されたデータ",
    "iv": "9a75VfBAXlkBmuQITubgxA==\n",
    "version": 1,
    "cipher": "aes-256-cbc"
  },
  "client": {
    "encrypted_data": "暗号化されたデータ",
    "iv": "k7aM+4cvWTW5TpUkY7zyFQ==\n",
    "version": 1,
    "cipher": "aes-256-cbc"
  }
}

recipe内での暗号化されたデータの扱いはsensu-chef/libraries/sensu_helpers.rbrecipes/defaultrecipes/rabbitmq.rbにかかれているので読むこと.

整理&分析

長いメモ書きのような文章を綴ってしまったが今回の内容を整理すると以下のとおり.

  • sensu-chefのexamplesに含まれるssl_certs.shを実行すると以下が作成される
    • sensu_caの証明書と秘密鍵
    • sensu serverの証明書と秘密鍵
    • sensu clientの証明書と秘密鍵さ作成される
    • 上記の証明書と秘密鍵の情報を含んだsensu.json(chefのdata bagで利用する)
  • 以下のコマンドの結果作ったファイルはrecipeで使われていない(ssl_certs.shから削除しても問題ないコマンドだと思う)
    • sense_caのopenssl x509 -in cacert.pem -out cacert.cer -outform DER
  • 以下のコマンドでは,server,clientの証明書と秘密鍵を1つのファイルにパスワードつきで格納する.機密情報の管理の観点で行っているだけである.
    • sense serverのopenssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass:secret
    • sense clientのopenssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass:secret
  • ssl_certs.shは流用可能!
    • 作成される署名や秘密鍵はユニークになるため

無事処理内容が理解できた.ただ気になるのが,全てのSensu clientで全く同じ証明書と秘密鍵を使う実装になっているということ.SSLのクライアント証明書は分けるのが普通だともうけど. この辺はRabbitMQ側の理解をする必要があるし,また別の機会に調べてみようと思う.

まとめ

今回,sensu-chefで提供されている証明書作成のスクリプトの流れを整理した.結果,用意されているスクリプトで作成される秘密鍵や証明書はユニークなので流用していいことがわかった.一方で,クライアントの証明書が全て同じになってしまう点は問題かなと思う.恐らく,RabbitMQの実装にも関係があるので今度はそっちも見てみたい.それにしても,SSLは深い.SSLについては知っているようでまだまだ知らないことも沢山あるので,時間がを作って調査しようと思う.

参考

SSLの仕組みについて

OpenSSLの使い方

openssl.cnfの書き方

RabbitMQ

その他