ぶていのログでぶログ

思い出したが吉日

NAT環境下でLet's Encrypt!の証明書を発行する

Let's Encrypt! めっちゃ便利ですよね! 気軽にSSLが使えるようになったし、HTTP2も導入しやすくなったと思います。

今まで実はLet's Encrypt! 使ったことなかったのですが、最近使う機会があって使ってみました。 ですが、なんと NAT環境下(Vagrantとか)ではエラーになってしまったのです。。。 orz

どうやらACMEプロトコルの仕様で、ACEMサーバから認証用のファイルをGETしに来るようです。 当然、NAT環境下ではそれ相応の設定しないかぎりアクセスできないので認証エラーとなってしまうのです。。

しかし、ACMEプロトコルの認証方式にはいくつか種類があるようです。 前述した方法はHTTPを用いた認証(http-01)で、これ以外に DNSを用いた認証(dns-01) があります。 今回はこのDNSを用いた認証(dns-01)を使ってLet's Encrypt!の証明書を発行してみました。

DNS認証方式(dns-01)とは

DNS認証方式と聞くと敷居が高く感じますが、設定自体はかなりシンプルです。 具体的には、ACMEサーバに認証リクエストを送ると、認証用のコードが返ってきます。 これを認証したいドメインサブドメインのTXTレコードに設定するだけです。 サブドメイン名は決まっていて _acme-challenge になります。

例えば、exmaple.comであれば認証用のドメイン_acme-challenge.exmaple.com になります。

(2016/02/23現在)公式クライアントがまだ対応していない

実はこのdns-01ですが、Let's Encrypt!のACMEサーバでは対応しているものの公式クライアントが対応していません。 PRは作られているのでそのうちマージされると思われます。

github.com

まぁ、これを待つのがいいのですが、今すぐに証明書がほしいわけです。 ではどうするか? curlを叩いて…ってのも良かったのですが、dns-01に対応したスクリプトを公開されている方がいたのでこちらを使うことにしました。

github.com

フックスクリプト

このスクリプトでは、認証処理前/後、証明書発行後の処理をフックすることができhttp-01では必須ではないのですが、dns-01では必須となっています。 フックスクリプトはサンプルが用意されているのでこれを流用して作成します。 今回認証処理前に、DNS登録を行う必要があります。

CloudFrontやRoute53などへ登録を行なうサンプルのフックスクリプトはいくつか公開されています。 https://github.com/lukas2511/letsencrypt.sh/wiki/Examples-for-DNS-01-hooks

しかし、今回は自前のDNSサーバへの登録を行うことにしました。 登録部分をスクリプト化すればいいのですが、単発の作業であったのでフックスクリプトでは入力待ち状態にしてその間に裏で職人による温かみのあるDNS登録作業をするようにしましたw 以下、今回つくったスクリプトです。

#!/bin/bash

function deploy_challenge {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

    echo "++ deploy_challenge DOMAIN:$DOMAIN TOKEN_FILENAME:$TOKEN_FILENAME TOKEN_VALUE:$TOKEN_VALUE"
    echo "Please Enterkey"
    read
    # This hook is called once for every domain that needs to be
    # validated, including any alternative names you may have listed.
    #
    # Parameters:
    # - DOMAIN
    #   The domain name (CN or subject alternative name) being
    #   validated.
    # - TOKEN_FILENAME
    #   The name of the file containing the token to be served for HTTP
    #   validation. Should be served by your web server as
    #   /.well-known/acme-challenge/${TOKEN_FILENAME}.
    # - TOKEN_VALUE
    #   The token value that needs to be served for validation. For DNS
    #   validation, this is what you want to put in the _acme-challenge
    #   TXT record. For HTTP validation it is the value that is expected
    #   be found in the $TOKEN_FILENAME file.
}

function clean_challenge {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

    echo "++ clean_challenge DOMAIN:$DOMAIN TOKEN_FILENAME:$TOKEN_FILENAME TOKEN_VALUE:$TOKEN_VALUE"
    echo "Please Enterkey"
    read
    # This hook is called after attempting to validate each domain,
    # whether or not validation was successful. Here you can delete
    # files or DNS records that are no longer needed.
    #
    # The parameters are the same as for deploy_challenge.
}

function deploy_cert {
    local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" CHAINFILE="${4}"

    echo "++ deploy_cert DOMAIN:$DOMAIN TOKEN_FILENAME:$TOKEN_FILENAME TOKEN_VALUE:$TOKEN_VALUE"
    echo "Please Enterkey"
    read
    # This hook is called once for each certificate that has been
    # produced. Here you might, for instance, copy your new certificates
    # to service-specific locations and reload the service.
    #
    # Parameters:
    # - DOMAIN
    #   The primary domain name, i.e. the certificate common
    #   name (CN).
    # - KEYFILE
    #   The path of the file containing the private key.
    # - CERTFILE
    #   The path of the file containing the signed certificate.
    # - CHAINFILE
    #   The path of the file containing the full certificate chain.
}

HANDLER=$1; shift; $HANDLER $@

実行例

以下、実際に実行した結果を載せておきます。 deploy_challenge の入力待ちでは裏でDNS登録を行い、clean_challenge の入力待ちでは裏でDNS登録の削除行っています(が、ここでは省略します)。

[vagrant@example letsencrypt.sh]$ ./letsencrypt.sh -c -d <ドメイン> -t dns-01 -k $(readlink -f hook.sh)
#
# !! WARNING !! No main config file found, using default config!
#
Processing <ドメイン>
 + Signing domains...
 + Generating private key...
 + Generating signing request...
 + Requesting challenge for <ドメイン>...
++ deploy_challenge DOMAIN:<ドメイン> TOKEN_FILENAME:***** TOKEN_VALUE:****
Please Enterkey

 + Responding to challenge for <ドメイン>...
++ clean_challenge DOMAIN:<ドメイン> TOKEN_FILENAME:***** TOKEN_VALUE:****
Please Enterkey

 + Challenge is valid!
 + Requesting certificate...
 + Checking certificate...
 + Done!
 + Creating fullchain.pem...
++ deploy_cert DOMAIN:<ドメイン> TOKEN_FILENAME: TOKEN_VALUE:
Please Enterkey

 + Done!

おわりに

DNS認証を使うことでNAT環境下でもLet's Encrypt!の証明書を発行することができました。 今現在は、非公式クライアントを使う必要がありますが、前述したPRがマージされれば公式クライアントだけでDNS認証方式が扱えると思います。