Play Framework + Let's Encrypt で、非 JKS 証明書形式 ( PEM / PKCS ) を使って SSL/TLS で HTTPS してみた話

Play Framework の公式ドキュメントの [HTTPS の設定の項目](https://www.playframework.com/documentation/2.5.x/ConfiguringHttps)があまりにもエスパー向けなので、いろいろ調査してみたののまとめ。Let's Encrypt と Play で https をやります。証明書更新等各種スクリプト付き。
(ただし、このスクリプトによって発生したいかなる不具合・損害についても責任は負いません。自己責任で使ってください。念のため)。

# 検証環境

- Ubuntu 16.04
- Play 2.5
- OpenJDK 1.8

# Let's Encrypt から証明書をもらう

Let's Encrypt では証明書の取得に関するいろいろを自動化するツールを提供してくれている。まずそれをインストールする。といっても git clone するだけ。

```
$ git clone https://github.com/certbot/certbot
```

あとはクローンしたディレクトリ配下の certbot-auto コマンドを基本的には使う。詳しくは[公式ページ](https://letsencrypt.jp/usage/)参照。

今回は example.com というドメインで SSL 証明書を取得する。

```
$ ./certbot-auto certonly --standalone -d example.com -d www.example.com
```

Let's Encrypt では証明書の申請ドメインにアクセスして、実際にそこで申請に用いられた certbot と通信することで、サーバー/ドメインの存在証明をしているので

- 申請ドメインが利用するつもりのサーバーに割り当てられている
- certbot が申請ドメインの割り当てられているサーバーで実行される
- certbot 用に 80 / 443 port が開放してある(iptables・ufw)
- 他のプロセスが 80 / 443 port にバインドされていない(httpd とかいたらとめておく)

ことが必要。

いろいろ指示に従って入力していけば証明書がもらえる。証明書のシンボリックリンクが ```/etc/letsencrypt/live/<domain name>/``` 以下に生成される。今回の場合は ```/etc/letsencrypt/live/example.com/``` 以下に、次の4つのファイルが生成される。

- privkey.pem
- cert.pem
- chain.pem
- fullchain.pem

見て分かる通り、PEM エンコードされた証明書が生成されている。

# PEM から Java Keystore (JKS) を生成する

証明書が入手出来たので、Play Framework にこの証明書を使ったSSL通信をさせたい。Play の公式ドキュメントに [Configuring HTTPS](https://www.playframework.com/documentation/2.5.x/ConfiguringHttps) というページがあるんだけれど PEM なんて単語は全く出てこないし、privkey や chain などいろいろなファイル名も出てこない。どうしたらいいんだ…

ポイントは Java Keystore (JKS) で、Java で証明書を用いたセキュアな通信をする場合、基本的にはこの JKS に証明書やキーを格納しなければいけないらしい。なので、PEM から JKS に変換をします。

PEM から JKS に変換するためには PEM -> PKCS -> JKS という二段階の変換をする。

## PEM -> PKCS

openssl を利用する。

```
$ openssl pkcs12 -export -passout pass:hogepiyofuga
  -in /etc/letsencrypt/live/example.com/fullchain.pem
  -inkey /etc/letsencrypt/live/example.com/privkey.pem
  -out fullchain_and_key.p12 -name play
```

(見やすさのために複数行に改行したが1行で入力する)

多分デフォルトでは ```/etc/letsencrypt/``` 以下の証明書が配置されているディレクトリ ```archive``` とか ```live``` は所有権/アクセス権が ```root:root rwx------``` になっているので、コマンド実行するユーザーからリードだけできるようにいい感じに設定しておくと良いと思われる(cron用)。セキュリティ的にはいいんだろうか…

上記コマンドが実行できると現在のディレクトリに ```fullchain_and_key.p12``` ファイルが生成されている。はず。

## PKCS -> JKS

Java keytool を使う。JDK入れてれば入ってるはず。

```
keytool -importkeystore
  -deststorepass <パスワード入れる>
  -destkeypass <！！storepassと同じパスワード入れる！！>
  -destkeystore MyDSKeyStore.jks
  -srckeystore fullchain_and_key.p12
  -srcstoretype PKCS12
  -srcstorepass hogepiyofuga
  -alias play
```

(見やすさのために複数行に改行したが1行で入力する)

ポイントは ```-deststorepass``` と ```-destkeypass``` には __必ず__ 同じ値を設定すること。これ、最初 Play のドキュメントを読んでもわからなかったんだけど、Play の SSL の設定にはパスワードを指定するキーが ```play.server.https.keyStore.password``` しかなくて、ソースを読んだらこのキーの値を storepass と keypass 両方に使っていた... ので必ず同じにしてエンコードしてください。ドキュメントに書いといてくれよ...

これでうまく行けば ```MyDSKeyStore.jks``` が生成されている。

# Play に SSL/TLS を利用するように設定をする

用意すべきものは5つ。

- Play application のリリースビルド
- プロダクション用の設定ファイル prod.conf
- サーバー起動スクリプト
- サーバー停止スクリプト
- certbot の証明書自動更新スクリプトと cron 設定

## Play application のリリースビルドとデプロイ

とりあえず Play application のリリースビルドをする。これも Play 2.4 系から変わってるみたいで、現在は [dist](https://www.playframework.com/documentation/2.5.x/Deploying#using-the-dist-task) コマンドが使われている。

dist コマンドを実行すると適当なところに ```target/universal/<project name>.zip``` が生成されるので、それをサーバーに転送して展開する。展開した配下に ```bin/<project name>``` という起動スクリプトがあるので、それを呼び出せばプロダクションモードでサーバーが起動する。

## プロダクション用の設定ファイル prod.conf

おそらく、上記の方法で起動しようとすると ```play.crypto.secret``` が設定されてないからプロダクションモードで起動できないよ！と怒られる。ので、```play.crypto.secret``` を設定する。

applicaiton.conf にはローカルのテスト用設定 / プロダクションと共用の設定だけ書いて、セキュリティにまつわるものなどはプロダクション専用の設定ファイルに記述するべきなので、サーバー側で適当なディレクトリにプロダクション用の設定ファイルを作成する。

```prod.conf
include "application" 

play.crypto.secret = "<適当に生成した secretkey>" 

http.port = "disabled" 
https.port = 443 
 
play.server.https.keyStore.path = "/path/to/your/keystore.jks" 
play.server.https.keyStore.type = "JKS" 
play.server.https.keyStore.password = "<JKS を作成するときに使ったパスワード>" 
```

1行目で、もともとの設定ファイルである ```application.conf``` を include している。これで ```application.conf``` に設定されている内容が取り込まれる。Play の conf ファイルでは [HOCON Syntax](https://www.playframework.com/documentation/2.5.x/ConfigFile#hocon-syntax) が利用されていて、既存の項目については下に記述のある方で上書きされる(順序性がある！)ので、先頭で include する。

play.crypto.secret には適当なキーを渡してください。[Generationg Appication Secret](https://www.playframework.com/documentation/2.5.x/ApplicationSecret#generating-an-application-secret) に詳しく書いてある。

今回はサーバーに SSL/TLS で通信させたいので、http を塞いで、https の well-known ポートを設定している。さらに SSL/TLS を利用するために、前の章でつくった JKS に関する設定もここでしている。

これでプロダクション用の設定ファイル完成。

## サーバー起動スクリプト

リリースビルドを展開したフォルダに対して、サーバーを起動するスクリプト。JSSE のセキュリティオプションとか、上記の prod.conf を設定ファイルとして読みこむように Play application に指示する。

```
$ sudo ./start.sh server-0.0.1/
```

みたいな感じで使う。

```bash:start.sh
#!/bin/bash

path="/tekitou/na/server/you/folder"

if [ $# == 1 ]; then
	temptarget=$1
	newtarget=${temptarget%/}
	echo $newtarget > $path/target.path
else
	echo "Usage: ./start.sh server-path/"
	exit 0
fi

target=`cat $path/target.path`

if [ -d "$path/$target/" ]; then
	echo "Start $target server."
else
	echo "$target not found."
	exit 0
fi

nohup $path/$target/bin/<project name> -Djdk.tls.ephemeralDHKeySize=2048 -Djdk.tls.rejectClientInitiatedRenegotiation=true -Dconfig.file=/path/to/your/prod.conf &
```

nohup はターミナルを閉じてもバックグラウンドプロセスを殺さないためのコマンド。詳しくはググッて。```<project name>``` のところは適宜置き換えてください。

## サーバー停止スクリプト

起動スクリプトの時点でお察しかも知れないが、Play にはグローバルな http のフロントエンドデーモンみたいなものがいなくて、すべてリリースビルドに含まれている。そしてコマンドラインからサーバーを停止できるみたいな気の利いたものはない。フロントタスクにいる時に Ctrl-c するか、kill <pid> するしかないので、停止スクリプトを自作する。

```bash:stop.sh
#!/bin/bash

path="/tekitou/na/server/you/folder"
targetpath=`cat $path/target.path`

if [ ! -f $path/$targetpath/RUNNING_PID ]; then
	echo "Cannot find RUNNING_PID"
	exit 0
fi

echo "Stop $targetpath server."

kill `cat $targetpath/RUNNING_PID`
```

デフォルトの設定だとサーバー実行中に、展開したサーバーのフォルダに ```RUNNING_PID``` というファイルが作成されるので、この内容を参照して kill コマンドを送るだけ。

## Let's Encrypt の証明書自動更新スクリプトと cron 設定

ここまでやれば、上記の start / stop スクリプトで Play で https 通信ができることを確認できる。が、Let's Encrypt の証明書は寿命がなんと90日しかないので、証明書の更新をわりと頻繁にしなければならない。忘れたら証明書期限切れの悲劇になるので自動化したい。

### 更新した証明書から JKS へのインポートの自動化

まず、Let's Encrypt で証明書を更新出来ても Play は JKS 形式でないと困るので、PEM -> JKS の変換を自動化する。

```bash:create_jks.sh
#!/bin/sh

callpath=`pwd`
relativepath=`dirname $0`
passpath="<適当なパスワードを書いたファイルのパス>"
pass=`cat $passpath`
pempath="/etc/letsencrypt/live/<ドメイン名>"
pkcsname="chain_and_key.p12"
keyalias="play"
jksname="keystore.jks"

cd $relativepath

if [ -f $pkcsname ]; then
	rm $pkcsname
fi

if [ -f $jksname ]; then
	rm $jksname
fi

openssl pkcs12 -export -passout pass:$pass -in $pempath/fullchain.pem -inkey $pempath/privkey.pem -out $pkcsname -name $keyalias
keytool -importkeystore -deststorepass $pass -destkeypass $pass -destkeystore $jksname -srckeystore $pkcsname -srcstoretype PKCS12 -srcstorepass $pass -alias $keyalias

chmod 660 $pkcsname
chmod 660 $jksname
chown biacco42:root $pkcsname
chown biacco42:root $jksname

cd $callpath
```

基本的には、すでに説明した PEM -> JKS 変換の手続きを書いて、利便性のためにアクセス権等を設定してるだけ。

### Let's Encrypt の証明書の更新

基本的には ```certbot-auto renew``` でおっけーなんだけど、自動化しようと思うといくつか問題がある。

1. certbot は Let's Encrypt サーバーと対話して証明書を発行するので、対話に用いるポート(443)をほかのサービスが利用していてはいけない
1. certbot は root 権限を要求する(1024番以下のポートを利用する Privilege Permission が必要)

#### 1. certbot のために Play を停止、再起動する

まずは 1. から。当然のことながら、実行中の Play application は 443 ポートを利用しているので、certbot を動かすときには一時的に停止していなければならない。のでスクリプトを書く。

```bash:renew.sh
#!/bin/sh

home="<home>"
path="$home/server"
targetpath=`cat $path/target.path`
pid=`cat $path/$targetpath/RUNNING_PID`

$path/stop.sh
wait $pid

$home/tools/certbot/certbot-auto renew --standalone --quiet

$path/cert/create_jks.sh
$path/start.sh $targetpath/
```

ディレクトリ構造は下記を仮定しているので、適宜変更してください。

```
<HOME>
├ tools
│ └ certbot
└ server
  ├ start.sh
  ├ stop.sh
  └ cert
    └ create_jks.sh
```

本当は、certbot-auto renew のオプションに ```--pre-hook```、```--post-hook``` というのが[あるんだけれど](http://letsencrypt.readthedocs.io/en/latest/using.html#renewal)、パス指定したり、やることが多かったりでスクリプトにまとめてしまった。あまりよくないかもしれない。

#### cron を sudo で実行する

次に 2.。これは crontab での登録を sudo で行うことで、そのユーザーで cron タスクが実行されるので、OK。と簡単にはいかない。

当然のことながら、通常通り sudo でコマンド実行しようとすると、sudo を呼び出したユーザーに対してパスワード要求が発生するので、自動実行するときに困る。また、sudo は tty (ターミナル) からのみ利用できるよう初期設定ではされているので、cron でコマンド実行しようとすると怒られる。詳しくは [バックアップcronでsudoを使う](http://tech-blog.aloha-inc.jp/tech/22) あたりを見てください。

ということで、```sudo visodo``` あたりで設定を変更する。

```
Defaults:<cron を登録するユーザー>      !requiretty
<cron を登録するユーザー> ALL=(ALL)     NOPASSWD: /path/to/renew.sh
```

これで、cron 登録ユーザーは tty を通さず sudo できて、renew.sh スクリプトの実行に際して __だけ__ は sudo でのパスワード要求が不要になる。

この状態で、やっと cron にタスクを登録できる。certbot-auto は証明書の期限を確認して、残り期限が 30 日以下の場合のみ証明書を更新してくれるので、適当な間隔で呼んだらいいだろう。

```
$ sudo crontab -e
```

```
00 5 1,15 * * /path/to/renew.sh
```

というわけで、Let's Encrypt の証明書自動更新ができるようになった。テスト実行として renew.sh の certbot-auto renew に ```--force-renew``` オプションを付けて実行とかしてみると、挙動確認ができると思う。

# まとめ

長くなってしまいましたが、これで Play + Let's Encrypt で https できるようになりました！

よかったね。