文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

GAEで独自ドメインにLet's encryptで発行したSNI形式のSSL証明書を適用する

手順が結構あるうえ(無料では)自動化できないので、手順残しておきます。
f:id:treeapps:20160521191008p:plain

Google App Engineですが、独自ドメインを使わなければ標準でSSLに対応しています。

独自ドメインを使ってSSL証明書を適用する場合、色々と煩雑な作業が必要になるため、メモっておこうと思います。

今回は、独自メインは既に購入済みで、Google Cloud PlatformのGAEメニューでカスタムドメインを登録し、SSLを適用したいドメインの所有権を確認済みの状態とします。

目標

GAEに適用した独自ドメインに対して、Let's encryptでSNI形式のSSL証明書を発行し、適用する。

完全無料で行うこと。

実行環境

OS mac
GAEランタイム java

Let's encryptでSSL証明書を発行する

GAEはSNI形式のSSL証明書に対応しているのですが、SNI形式で更に無料のSSL証明書となるとLet's encryptくらいしか無いので、Let's encryptの証明書を取得します。

certbotのインストール

ユーザーガイド - Let's Encrypt 総合ポータル
公式サイトに書いてあるように、git cloneしてプログラムを取得します。

git clone https://github.com/certbot/certbot
cd certbot

certbotでSSL証明書を生成する

cloneしたら早速以下のコマンドから証明書取得ウィザードを起動します。実行するにはroot権限が必要になります。

sudo ./certbot-auto -a manual certonly

コマンドを入力すると、ターミナル上にグラフィカルな入力インターフェースが現れ、SSL証明書を発行したいドメインの入力を求められるので、入力します。今回は「http://www.tree-maps.com」にSSLを適用したいので、「www.tree-maps.com」を入力し、EnterキーでOKします。
f:id:treeapps:20160911025143p:plain

確認されるので、EnterでOKします。
f:id:treeapps:20160911025430p:plain

すると、以下のような画面表示になり、「Enterキー押してね」と入力待ち状態になりますが、ここではEnterを押さずに先にアプリケーション側の変更をする必要があります。
f:id:treeapps:20160911025629p:plain
ここで求めらているのは、Let's encrypt側が証明書を発行するのに、対象アプリケーションが本物かどうか確認するため、あなたのアプリケーション側で「http://www.tree-maps.com/.well-known/acme-challenge/xxxxxx」というURLにアクセスした時に、セキュリティトークン値を返してね、それが準備できたらEnter押してね、という事です。

アプリケーション側でセキュリティトークンを返せるようにする

今回はGAE/JのServletでアプリを開発しているので、Servletの設定例となります。

WEB-INF/web.xml
<servlet>
    <servlet-name>letsencrypt</servlet-name>
    <servlet-class>tree.servlet.LetsencryptServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>letsencrypt</servlet-name>
    <url-pattern>/.well-known/acme-challenge/*</url-pattern>
</servlet-mapping>
LetsencryptServlet

f:id:treeapps:20160911030412p:plain
コンソール上に「このURLにアクセスしたら、この値を返せ」という指示があるので、それを実現するServletを書きます。

この例では、以下のURLにアクセスすると、
http://www.hoge.com/.well-known/acme-challenge/QVQf5E92p76wmp3qQ5KhDM0toAK8jP7zNXTGmr3RJUI
以下のセキュリティトークン値を返す必要があります。
QVQf5E92p76wmp3qQ5KhDM0toAK8jP7zNXTGmr3RJUI.2imcf49mOVFHrqSATpIkuyZvzlg1JGKo6BbypbmfLFc

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LetsencryptServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    public static final Map<String, String> challenges = new HashMap<String, String>();

    static {
        challenges.put("QVQf5E92p76wmp3qQ5KhDM0toAK8jP7zNXTGmr3RJUI", "QVQf5E92p76wmp3qQ5KhDM0toAK8jP7zNXTGmr3RJUI.2imcf49mOVFHrqSATpIkuyZvzlg1JGKo6BbypbmfLFc");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (!req.getRequestURI().startsWith("/.well-known/acme-challenge/")) {
            resp.sendError(404);
            return;
        }
        String id = req.getRequestURI().substring("/.well-known/acme-challenge/".length());
        if (!challenges.containsKey(id)) {
            resp.sendError(404);
            return;
        }
        resp.setContentType("text/plain");
        resp.getOutputStream().print(challenges.get(id));
    }
}

アプリをデプロイしてアクセスして見る

Servletが作成できたらデプロイし、実際以下にアクセスします。
http://www.tree-maps.com/.well-known/acme-challenge/QVQf5E92p76wmp3qQ5KhDM0toAK8jP7zNXTGmr3RJUI
以下のように返ればOKです。
f:id:treeapps:20160911031027p:plain

SSL証明書を発行する

アプリ側の準備ができたらターミナルに戻り、Enterキーをクリックすると、Let's encryptが上記URLにアクセスし、発行したセキュリティキー・バリューが正しいかを確認し、問題無ければ生成が完了します。

認証が通ると、以下のログが表示されてコンソールは終了します。

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/www.tree-maps.com/fullchain.pem. Your cert
   will expire on 2016-12-09. To obtain a new or tweaked version of
   this certificate in the future, simply run certbot-auto again. To
   non-interactively renew *all* of your certificates, run
   "certbot-auto renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

ここに「/etc/letsencrypt/live/www.tree-maps.com/fullchain.pem」に2016/12/09までの期限の公開鍵作ったぞ、とあるのでこれを使います。

GAEに生成した証明書を登録する

https://console.cloud.google.com/home/dashboard からGoogle Cloud Platformを開き、ハンバーガーメニューから「App Engine」メニューをクリックします。
f:id:treeapps:20160911021505p:plain

左サイドメニューの「設定」をクリックし、「SSL 証明書」タブを選択し、「新しい証明書をアップロード」ボタンをクリックします。
f:id:treeapps:20160911031959p:plain

それぞれ以下のファイルの中身を貼り付けます。
f:id:treeapps:20160911032418p:plain
それぞれの中身は以下のように確認します。

PEM でエンコードされた X.509 公開鍵証明書の確認

Let's encryptのcertbotで「www.tree-maps.com」というドメインの証明書を生成した場合、以下に公開鍵が生成されます。
/etc/letsencrypt/live/www.tree-maps.com/fullchain.pem
なので、

sudo cat /etc/letsencrypt/live/www.tree-maps.com/fullchain.pem

で中身を表示してコピーし、「PEM でエンコードされた X.509 公開鍵証明書の確認」のtextareaに貼り付けます。

復号化された PEM でエンコードされた RSA 秘密鍵

秘密鍵は以下に生成されています。
/etc/letsencrypt/live/www.tree-maps.com/privkey.pem
しかしこのまま秘密鍵の中身を貼り付けても「無効」と言われてしまうので、以下のようにRSAエンコードする必要があります。

sudo openssl rsa -in /etc/letsencrypt/live/www.tree-maps.com/privkey.pem

ここで表示された結果を「復号化された PEM でエンコードされた RSA 秘密鍵」のtextareaに貼り付けます。


両方入力できたら「アップロード」ボタンをクリックしてアップロードを完了します。

証明書をドメインに適用

最初戸惑ったのですが、この状態だとまだドメインに対してSSLが適用されていません。

以下のように、「SSL 証明書」タブに戻ると、アップロードした証明書が追加されています。ここで「名前」列の部分がリンクになっているのでクリックします。(名前部分がリンクになっている事が非常に解りづらく、この手順を飛ばしがちです)
f:id:treeapps:20160911033038p:plain

すると、証明書を紐付けるドメイン選択できるので、目的のドメインのcheckboxにチェックを付け、保存ボタンをクリックします。
f:id:treeapps:20160911033228p:plain

これでようやくGAEの独自ドメインにLet's encryptで発行したSNI形式のSSL証明書を適用する事ができます。

雑感

Let's encryptの証明書の期限は3ヶ月なので、3ヶ月毎にこの作業をする必要があるので自動化したいところですが、今回行った事を無料で行う事って現状できません。(できませんよね?)

別途GCEインスタンスを生成したりcloud shellを使えばできるようですが、無料で簡潔する事はできません。

ここら辺が自動化できると楽なのですけどね。