踊る犬.netブログ (旧)

Herokuで自動更新アリでLet’s encryptを導入する方法

追記 (2017/03/24)

Herokuがついに公式でLet’s Encryptに対応したようです。

こちらの手順に則ったほうがいいでしょう。

さよならStartSSL

ある日StartSSLのダッシュボードにログインした私。
なんか不穏な空気を醸すメッセージが赤々と表示されている事に気づく。

  1. Mozilla and Google decided to distrust all StartCom root certificates as of 21st of October, this situation will have an impact in the upcoming release of Firefox and Chrome in January. Apple’s decision announced on Nov 30th of distrusting all StartCom root certificates as of 1st of December will have an impact in their upcoming security update.
  2. Any subscribers that paid the validation fee after Oct. 21st can get full refund by request.
  3. StartCom will provide an interim solution soon and will replace all the issued certificates with issuance date on or after Oct 21st in case of requested. Meanwhile StartCom is updating all systems and will generate new root CAs as requested by Mozilla to regain the trust in these browsers.

あっるぇ〜?
花粉が目に入ってよく読めないよ〜?
一体何が起こったって言うんだよ。

なるほど、不正やっちゃったと。そらダメだわ。もう更新できないと。

なつかしいな。StartSSLにはよくお世話になりましたよ。
5年くらい前、Class 2の証明書を取得した時にIdentity Verificationが必要だってんで、イスラエルから国際電話がかかってきたんですよ。
もう英語何言ってんのかサッパリ分からなくて、パスポートに載ってる自分の情報を片っ端から読み上げたっけ。
結局、本籍を確認したかったみたい。
「オーケーオーケー、せんきゅーバーイ!!」って言って切ったんだけど、あの日のお姉さんは今も元気でやってるかな(哀愁)

こんにちはLet’s Encrypt

StartSSL無き今、無料で取得できる認証局はLet’s Encryptのみです(たぶん)。
公益目的で作られた団体がやっているので、StartSSLみたいな安売りとは根本から違います。
下手にマイナーなやっすい証明書を買うより、こっちを導入した方が断然安心感があると言えます。
あとイスラエルから国際電話がかかっくる心配も当然ありません!!

Herokuだと更新しづらい

ただしLet’s Encryptの証明書は有効期限が3ヶ月と短いため、頻繁に更新する必要があるのが難点です。
自動更新スクリプトが公式で提供されているので、これも一度やり方を把握すれば簡単なんですけどね。
このスクリプトは、サーバがまさしく持ち主の物であることを証明するための処理を行います。

ところが、Herokuだとこの認証処理がやりづらい。
Herokuから提供されている方法は、従来のように1年に一回手動で更新するようなフローが前提だからです。
このフローに則って取得・更新する方法はネットで検索すればいろいろ出てきます。

が!!
めんどい!!
絶対更新忘れるでしょ!!
自動更新させてょ!!

と喚いていたら、ありました。

sabayonで自動化する

やっと本題です。
sabayon(サバヨン?) は、SSL証明書の取得と更新を自動化してくれる神のようなheroku用ツール。いや、神。

下準備

この辺の記事を参考にして、あなたのappのSSLを有効にします。

sabayonをherokuにデプロイ

$ git clone https://github.com/dmathieu/sabayon.git
$ cd sabayon
$ heroku create letsencrypt-app-for-<name>
$ git push heroku

ツールの名前は何でもいいです。
デプロイするとherokuが勝手にweb dynoを有効にしやがりますが、これは無効にしておいて大丈夫です。

sabayonを設定

いくつかの環境変数を設定してあげる必要があります。

oAuth API tokenを取得

sabayonがあなたのアプリを自動で設定するために、herokuが提供するoAuthの仕組みを利用します。
そしてsabayonにアクセス許可を与えるためのAPI tokenが必要です。
これは、heroku toolbeltを使って簡単に取得できます。

> heroku plugins:install heroku-cli-oauth  # たぶん既にインストールされていると言われる
> heroku authorizations:create -d "<description-you-want>"
Created OAuth authorization.
  ID:          <heroku-client-id>
  Description: <description-you-want>
  Scope:       global
  Token:       <heroku-token>

この <heroku -token> を、HEROKU_TOKEN に指定してあげましょう。

アプリに認証リクエストのハンドラを追加する

ここからはアプリの実装依存の話なので、各々で対応する必要があります。

簡単に言うと、あなたのアプリに http://<domain>/.well-known/acme-challenge/<acmetoken&gt; というURLで認証局からアクセスされるので、これを正しくハンドル出来るようにします。
こうやって持ち主であることを証明します。

ここにRuby on RailsとかExpressでの実装方法が書いているので、よくあるスタックを使っている人はコピペでOK。
koa.jsを使っている場合は以下を参考にしてください。

router.get('/.well-known/acme-challenge/:acmeToken', async (ctx) => {
  const { acmeToken } = ctx.params
  let acmeKey = null

  if (process.env.ACME_KEY && process.env.ACME_TOKEN) {
    if (acmeToken === process.env.ACME_TOKEN) {
      acmeKey = process.env.ACME_KEY;
    }
  }

  for (let key in process.env) {
    if (key.startsWith('ACME_TOKEN_')) {
      const num = key.split('ACME_TOKEN_')[1];
      if (acmeToken === process.env['ACME_TOKEN_' + num]) {
        acmeKey = process.env['ACME_KEY_' + num];
      }
    }
  }

  if (acmeKey !== null) {
    ctx.body = acmeKey
  } else {
    ctx.status = 404
  }
})

ACME_KEYとかACME_TOKENという環境変数はsabayonが自動で設定してくれます。
追加したらデプロイしましょう。

手動でsabayonを実行する

さっそく証明書を取得してみましょう。
以下のコマンドを実行します。

$ heroku run bin/sabayon -a letsencrypt-app-for-<name>

上手く行けば以下のようなログが出力されます。

2016/07/21 14:02:50 cert.create email='<name>@example.com' domains='[codetriage.com www.codetriage.com]'
2016/07/21 14:02:51 [INFO] acme: Registering account for <name>@example.com
2016/07/21 14:02:51 [INFO][codetriage.com, www.codetriage.com] acme: Obtaining bundled SAN certificate
2016/07/21 14:02:51 [INFO][codetriage.com] acme: Could not find solver for: dns-01
2016/07/21 14:02:51 [INFO][codetriage.com] acme: Could not find solver for: tls-sni-01
2016/07/21 14:02:51 [INFO][codetriage.com] acme: Trying to solve HTTP-01
2016/07/21 14:02:51 cert.validate
2016/07/21 14:03:12 cert.validated
2016/07/21 14:03:15 [INFO][codetriage.com] The server validated our request
2016/07/21 14:03:15 [INFO][www.codetriage.com] acme: Could not find solver for: dns-01
2016/07/21 14:03:15 [INFO][www.codetriage.com] acme: Trying to solve HTTP-01
2016/07/21 14:03:15 cert.validate
2016/07/21 14:03:36 cert.validated
2016/07/21 14:03:40 [INFO][www.codetriage.com] The server validated our request
2016/07/21 14:03:40 [INFO][codetriage.com, www.codetriage.com] acme: Validations succeeded; requesting certificates
2016/07/21 14:03:41 [INFO] acme: Requesting issuer cert from https://acme-v01.api.letsencrypt.org/acme/issuer-cert
2016/07/21 14:03:41 [INFO][codetriage.com] Server responded with a certificate.
2016/07/21 14:03:41 cert.created
2016/07/21 14:03:41 cert.added

もし以下のようなエラーを得たら、ちゃんとハンドラがうまく動作しているか確認してください:

ERROR: Challenge is invalid! http://sub.domain.eu/.well-known/acme-challenge/HPdGXEC2XEMFfbgpDxo49MNBFSmzYREn2i1U1lsEBDg

さあ、これで証明書が取得できて、あなたのアプリに設定されました!

DNSの設定をSSL用に変更する

herokuではHTTP用とHTTPS用のサーバが異なるので、合わせてやる必要があります。
おそらく今あなたのドメイン設定は CNAMEyour-app-name.herokuapp.comに向けられているはずです。
SSL用の設定ではSNIが正しく動作するようにCNAMEyour-app-name.com.herokudns.comとする必要があります。
設定がプロパゲートされるまで待ちましょう。

うまく行けば、 https://your-domain-name/ でアクセスできるはずです。Cheers!!

自動更新用ジョブを設定する

最後に、有効期限が迫ったら自動で更新してくれる設定をしましょう。
上記で bin/sabayon を手動で実行しましたが、これを毎日実行するようにするだけです。
sabayonは期限が一ヶ月以内の時だけ新しく証明書を取得するように計らってくれます。よくできてるゥ!!

以下のようにスケジューラーを追加します。

$ heroku addons:create scheduler:standard

ブラウザでherokuのダッシュボードを開いて、スケジューラの設定画面を開いて下さい。
そして下記画像のように設定します。

これで、毎日sabayonが実行されるようになりました。
めでたし。

無料SSLわっしょい!!!

トラブルシューティング

acme: Error 400 - urn:acme:error:connection - Could not connect to <domain -name>

DNS設定がおかしい

上記のようなエラーが出たら、あなたのDNS設定を見なおして下さい。
おそらくあなたは既にSSLを導入していて、Let’s Encryptに乗り換えようとしていますよね。
SNIサーバはポート80の接続を受け付けませんので、CNAMEyour-app-name.herokuapp.com にしてやる必要があります。

アプリが立ち上がるのが遅すぎる

もうひとつの失敗理由は、アプリがまだ立ち上がっていないのに認証局がリクエストを飛ばしてしまった場合です。
ちゃんと起動し切るまで待ってもらう必要があります。

sabayonの環境変数のRESTART_WAIT_TIME60とか、長めに設定してください。