TLS証明書の発行・デプロイについて

こんにちは、インフラストラクチャー部セキュリティグループの三戸 (@mittyorz) です。 クックパッドでは全てのサービスをHTTPSにて提供しています。 今回はHTTPSの使用にあたって必要となるTLS証明書について、申請や発行、管理やサーバへのデプロイなどの運用について書きたいと思います。

TLS終端

クックパッドでは、サービスとユーザーとの通信経路は全てTLSにより暗号化されていますが、通信内容を暗号化するためのTLS終端処理はELBあるいはCloudFrontで行っています。 ELB、CloudFrontともにAWS Certificate Manager(ACM)を用いて証明書を管理*1することが出来ますが、社内向けで外部に公開していないサービスやステージング環境についてはELB背後のリバースプロキシで終端処理をしているものも多く、これらについては証明書ファイルを直接EC2インスタンスへ配置する必要があります。

なお、クックパッドのHTTPS化については Web サービスの完全 HTTPS 化 を御覧ください。

証明書の種類

TLS証明書には、ドメインの所有者について認証局が実在照会を厳格に行ったのちに発行される、Extended Validation(EV)証明書があります。 EV証明書を用いることで、ブラウザのアドレスバーにはそのドメインの所有者の情報が表示され、ユーザーにとって意図したサイトに接続しているかどうかがわかりやすくなります。 クックパッドでは、PCあるいはスマートフォン向けブラウザからユーザーが直接アクセスするページについては、原則EV証明書を設定するようにしています。

なお、EV証明書ではない証明書には、ドメインの所有者であることを確認して発行されるDomain Validation(DV)証明書と、所有者の実在照会まで行うOrganization Validation(OV)証明書が存在します。 OV証明書とEV証明書はいずれも実在照会が行われますが、CA/Browser Forumによって定められたガイドライン*2に従って発行されたものだけがEV証明書となります。

証明書の発行

新規サービスの立ち上げなどで新しいドメインを使用する場合、まずはACMを用いてDV証明書を発行し、APIエンドポイント用のドメインなどを除いて順次EV証明書を配置しています。 以前はドメインごとにEV証明書を一つ一つ購入していたためコストも無視できなかったのですが、後述するマルチドメイン証明書を用いることで年100ドルほどで追加購入できるようになりました。 また、常にEV証明書を設定するというわけでもなく、URLの変更などで使用しなくなりリダイレクトのみ行うドメインについてはEV証明書をやめてACMの証明書に切り替える、ということも行っています。

EV証明書の発行はACMでは行えないため、ACMで用いる場合別途認証局から購入しインポートする必要があります。 またACMから秘密鍵を取り出すことも出来ないため、EC2インスタンスで直接TLS終端している場合も同様に購入しています。

認証局の選定

クックパッドでは、現在はDigiCertから証明書を購入しています。 使いやすいWebコンソールが存在していることや、WebコンソールへのログインがSAMLによるシングルサインオンに対応していることが選定理由ですが、 後述するSANに対応したEV証明書の発行に対応していることやAPIが用意されていることもポイントとして挙げられるかと思います。 また、脆弱性の発生時など特に迅速な対応が必要な場合でも、認証局から直接のサポートが受けられるというのもあります。

証明書のデプロイ

Classic Load Balancer(CLB)の設定にはkelbimを、ECSと組み合わせて用いるApplication Load Balancer(ALB)の設定にはHakoを用いており、 それぞれACMに用意した証明書をARNを使って指定することが出来るようになっています。 CLBは主に社内向けのステージング環境や、Hako化がまだなされていないサービスにおいて使用されています。 最近リリースされたサービスは基本的にHakoを用いてデプロイ出来るようになっているので、以下のようなフローで証明書の設定を行っています。

  1. 証明書の発行の依頼がサービス開発チームからSREチームに来る
  2. EV証明書が必要と判断された場合は認証局へ発行を申請する
  3. ACMへ証明書をインポート、もしくはACMで証明書を発行する
  4. 証明書のARNをサービス開発チームに通知し、Hakoの定義ファイルに記載する
  5. Hakoを用いてデプロイ。ALBに証明書が設定される

Hako自体の説明はここではいたしませんが、定義ファイルでの証明書の指定の仕方はサンプルなどが参考になるでしょう*3

他、設定ファイルや証明書をサーバに直接配置する必要がある場合は、証明書や中間証明書はGitリポジトリに含めておき、itamaeを用いてデプロイしています。秘密鍵はそのままリポジトリに入れるのではなく、変数を用いてデプロイ時に展開されるようになっています。

証明書の有効期限の監視

TLS証明書には有効期限が存在します。 有効期限が切れる前に新しい証明書に更新する必要がありますが、有効期限は1年以上となっていることが多く「忘れた頃に有効期限が来る」ということが起きます。 認証局によっては、例えば30日前などにメールで通知してくれるところがありますし、ACMの場合は2017年の11月からDNSレコードによりドメインの所有者検証を行い自動更新することが出来ます*4。 EC2インスタンスで直接TLS終端している場合、どのインスタンスで証明書が使用されているのか把握しておく必要がありますが、クックパッドではZabbixを用いて監視しています。 また、一部のドメインについてはStatusCakeも併用しています。

社外のインターネット回線からアクセスした場合とオフィスからアクセスした場合とでエンドポイントが違っていて*5、設定されている証明書が異なるため監視漏れで危うく有効期限切れするところだったということもありました。 また、見落としがちなのがオフィスからのみアクセスできるサーバやアプライアンス製品で、特にワイルドカード証明書は思わぬサブドメインで使われていることもあるので、 Route 53からレコードを取得し、登録されているサブドメインも含め全てのドメインに対してチェックするということも行っています。

EV証明書発行のための実在証明

実在証明と書くとなんだか凄そうですが、手順としてはそれほど複雑ではなく、ざっくりと以下のようなことを行いました。

  1. 組織名(Organization)として商号を登録する
    • この部分がサイトにアクセスした際にアドレスバーに表示されます。
    • あわせて、本社所在地などの情報も登録します。
  2. 組織名と所在地が掲載された公的文書を提出する
  3. 担当者の在籍状況について、電話などで確認が行われる

2 について認証局が日本法人であれば登記簿謄本を提出することで証明出来たのですが、DigiCertはアメリカ合衆国の法人なため、アメリカ合衆国において発行されたものが必要となります*6。 今回はアメリカ証券取引委員会に登録された文章を見つけることが出来たため、比較的すんなりと会社の実在証明を行うことが出来ました。

一方 3 については、公的文書には代表電話番号のみ記載されていたためその番号での対応が必要となり、インフラストラクチャー部の直通番号へ入電を期待していたため何度か掛け直してもらうなど混乱もありました。 詳しい手順は前述のガイドラインにも掲載されていますが、受容可能な手順として法的に有効な文書に記載されている住所、電話番号、メールアドレスなどを用いて担当者の確認を行うこととされているため、 担当者直通など任意の電話番号に掛けてもらうにはその番号が記載された公的文書が必要となり注意が必要です。

フィーチャーフォン対応

国内のフィーチャーフォンがターゲットとなっているモバれぴ*7については特段の配慮が必要になりました。

証明書の認証パスにおいて、ルート証明書は本来その名の通り根本に存在し他のどの証明書にも依存せずに信頼される必要があるため、 OSに付属して提供されたり、ブラウザとあわせてインストールされるなど予め信頼されるようになっています。 しかし、フィーチャーフォンでは出荷後のアップデートなどで新しく証明書を追加することが出来ないことが多く、 プリインストールされているルート証明書自体も種類が少ないということがよくあります。 したがって、古いルート証明書しかサポートしていないフィーチャーフォンにおいては、証明書を切り替えてしまうと認証されずエラーとなる可能性があります。

この問題は、サポートされていないルート証明書を別のサポートされているルート証明書で署名する、クロスルート証明書という仕組みで回避することが出来ます。

DigiCertが発行しているルート証明書は多くの環境でサポートされていますが、フィーチャーフォン向けのBaltimoreのルート証明書によって更に署名されており、 この場合具体的には次のような認証パスになります。

  1. CN=Baltimore CyberTrust Root
  2. CN=DigiCert High Assurance EV Root
  3. CN=DigiCert SHA2 Extended Validation Server CA
  4. CN=m.cookpad.com

フィーチャーフォン以外の殆どの環境では2がルート証明書、 3が中間証明書、4がサーバ証明書になりますが、このケースだと2、3が中間証明書であると言えます。 したがって、ACMに証明書を登録する場合は、以下のように登録することになります。

  • Certificate body に、4の証明書
  • Certificate private key に、4の秘密鍵
  • Certificate chainに に、3の証明書へ2の証明書を結合したもの

実際に openssl コマンドを用いて認証パスを表示すると以下のようになります。

$ openssl s_client -connect m.cookpad.com:443 -quiet
depth=3 C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
verify return:1
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance EV Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 Extended Validation Server CA
verify return:1
depth=0 businessCategory = Private Organization, jurisdictionC = JP, serialNumber = 0104-01-071872, C = JP, ST = Tokyo, L = Shibuya-ku, O = COOKPAD Inc., OU = Infrastructure Division, CN = m.cookpad.com
verify return:1

マルチドメイン証明書

TLS証明書にはSubject Alternative Names(SAN)という属性をもたせることができ、Common Nameとは別の任意のドメインを追加することが出来ます。 この機能により、一つの証明書で例えば example.comexample.jp のように複数の独立したドメインに対応することが可能になります。 ただし、どんなときでもまとめてしまえば良いという訳でもなく、HTTP/2でサービスを提供している場合はコネクションの再利用に注意する必要があります。 例えば、SANに *.example.com が設定された証明書を用いて配信しているサーバがあったとして、このサーバAは a.example.com のコンテンツは配信しているものの b.example.com のコンテンツは配信しておらず、別のサーバBで配信しているとします。 クライアントが a.example.com のコンテンツを取得するためにサーバAとのコネクションを確立したあと、サーバBに存在する b.example.com の取得についてもサーバAとのコネクションを再利用してしまい、 サーバAからエラーが返されてから*8サーバBに改めてコネクションを確立するため、かえってレイテンシが増えてしまいます。 これはHTTP/1.1でよく見られた、画像やCSS、javascriptファイルを別のドメインから提供することでページ全体のレイテンシを低減している場合*9に起こりやすいと言えるでしょう。

この問題については HTTP/2 のコネクション再利用について確認してみる - ブログのしゅーくりーむ に詳しく解説されています。

証明書の発行のためCSRファイルを作成する際、opensslコマンドを用いることが多いと思いますが、マルチドメイン証明書のCSRファイルについてはSANの指定が引数で指定することが出来ません。 設定ファイルのopenssl.cnfに直接記入する必要がありますが、いちいち書き換えるのも面倒なので以下のようなスクリプトで作成しています。

#!/bin/bash

subject="/C=JP/ST=Tokyo/L=Shibuya-ku/O=Cookpad Inc./OU=Infrastructure Division/"


# CN and SAN list
common_name=$1
if [ -z $common_name ]; then
      read -p 'Common name? : ' common_name
fi

sans_file=${common_name}.txt
if [ ! -f "$sans_file" ]; then
    echo $0: "$sans_file" does not exist
    exit 1
fi

# find out where the openssl.cnf is
conf=`openssl version -a | grep OPENSSLDIR | cut -d '"' -f2`
conf=$conf/openssl.cnf

# compose SAN section
sansection=$(cat <(
    echo -n "subjectAltName='DNS:"
    cat $sans_file | perl -pe "chomp if eof" | perl -pe "s/\r?\n$/,DNS:/g"
    echo "'"
))

# display CN and SANs
echo CN: $common_name
echo $sansection

# make csr/key
openssl req -new\
            -newkey rsa:2048\
            -nodes -out ${common_name}-san.csr\
            -keyout ${common_name}-san.key\
            -sha256\
            -subj "${subject}CN=$common_name" \
            -reqexts SAN \
            -config <(cat $conf \
                <(printf "\n[SAN]\n$sansection"))

作成したCSRファイルは例えば以下のようになります。

$ openssl req -text -noout -in cookpad.com-san.csr
Certificate Request:
    Data:
        Version: 0 (0x0)
        Subject: C=JP, ST=Tokyo, L=Shibuya-ku, O=Cookpad Inc., OU=Infrastructure Division, CN=cookpad.com

(中略)

        Requested Extensions:
            X509v3 Subject Alternative Name:
                DNS:info.cookpad.com, DNS:payment.cookpad.com

終わりに

クックパッドでのTLS証明書の運用について紹介しました。 HTTPS化されるインターネットサービスはどんどん増えており、証明書の発行も昔と比べてずっと容易になってきています。 一方で実際に作業してみると、コード管理されていないサーバが見つかって手作業で証明書ファイルを配置したり、認証局と電話でやり取りしたりといったこともありました。 監視対象への追加や、証明書の自動更新などまだ出来ていない部分も多く、これからも改善した点について紹介させていただきたいと思います。

*1:ACMが2016年1月にリリースされるまではIAMを使用していました。

*2:https://cabforum.org/extended-validation/

*3:サンプルでは証明書をIAMで指定していますが、ACMでも同様に指定できます

*4:https://aws.amazon.com/certificate-manager/faqs/#dns_validation

*5:特にステージング環境でよくあるケース

*6:DigiCertの場合 https://www.digicert.com/ssl-certificate-purchase-validation.htm に受容可能な文章について記載されています。

*7:https://m.cookpad.com/

*8:421 Misdirected Request

*9:いわゆるドメインシャーディング

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/