Let’s Encrypt でワイルドカード 証明書を得る (nsd4 利用)

Let’s Encrypt でワイルドカード証明書を得るためには DNS-01 で認証を得る必要があります。

nsd4 と dehydrated の組み合わせで一応出来たので、備忘録として記載します。なお、マスターサーバ上で実装、bind9 付属の named-checkzone を利用しているので注意して下さい。

・/etc/nsd/zones/example.com (ゾーンファイルの修正)

nsd4 では、動的にゾーンのレコードを追加したり削除したり出来ないので、実際のゾーンファイルを直接書き換えます。

関係ないところを書き換えないためにも、シリアル値を「;SERIAL_START」から「;SERIAL_END」の範囲で、DNS-01 で使う「_acme-challenge」については「;LE_DNS-01_START」から「;LE_DNS-01_END」の範囲でくくります。

$TTL 86400
example.com.		IN	SOA	ns.example.com. root.ns.example.com. (
;SERIAL_START
				2018051301	; Serial
;SERIAL_END
				86400		; Refresh 24 Hours
				7200		; Retry 2 Hours
				1209600		; Expire 2 Weeks
				7200		; Negative cache TTL 2 Hours
			)

					IN	NS	ns.example.com.

;LE_DNS-01_START
;_acme-challenge.example.com.	60	IN	TXT	"NOTHING"
;LE_DNS-01_END

NS					IN	A	192.168.1.1

・hook.sh

dehydrated 同梱の hook スクリプトを以下の様にします。

#!/usr/bin/env bash

deploy_challenge() {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
    local REGEXDOMAINNAME=`echo ${DOMAIN} | sed -e 's/\./\\\./'`
    local NOWUMASK="`umask`"
    local serial1=`named-checkzone "${DOMAIN}" "/etc/nsd/zones/${DOMAIN}" | awk '/serial/ {print $5}'`
    local serial2=`expr $serial1 + 1`
    
    umask 022
    
    sed -i -e "/;SERIAL_START/,/;SERIAL_END/ s/$serial1/$serial2/" "/etc/nsd/zones/${DOMAIN}"
    sed -i -e "/;LE_DNS-01_START/,/;LE_DNS-01_END/ s/^;_acme-challenge\.${REGEXDOMAINNAME}\..*/_acme-challenge.${DOMAIN}.\t60\tIN\tTXT\t\"${TOKENVALUE}\"/" /etc/nsd/zones/${DOMAIN}
    nsd-control reload
    
    sleep 5
    
    umask ${NOWUMASK}
}

clean_challenge() {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
    local REGEXDOMAINNAME=`echo ${DOMAIN} | sed -e 's/\./\\\./'`
    local NOWUMASK="`umask`"
    local serial1=`named-checkzone "${DOMAIN}" "/etc/nsd/zones/${DOMAIN}" | awk '/serial/ {print $5}'`
    local serial2=`expr $serial1 + 1`
    
    umask 022
    
    sed -i -e "/;SERIAL_START/,/;SERIAL_END/ s/$serial1/$serial2/" "/etc/nsd/zones/${DOMAIN}"
    sed -i -e "/;LE_DNS-01_START/,/;LE_DNS-01_END/ s/^_acme-challenge\.${REGEXDOMAINNAME}\..*/;_acme-challenge.${DOMAIN}.\t60\tIN\tTXT\t\"NOTHING\"/" /etc/nsd/zones/${DOMAIN}
    nsd-control reload
    
    sleep 5
    
    umask ${NOWUMASK}
}

deploy_cert() {
}
deploy_ocsp() {
}
unchanged_cert() {
}
invalid_challenge() {
}
request_failure() {
}
generate_csr() {
}
startup_hook() {
}
exit_hook() {
}

HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then
  "$HANDLER" "$@"
fi

dehydrated の config ファイルの修正

DNS-01 と hook.sh を使う様に指定します。その他は適切に設定して下さい。

CHALLENGETYPE="dns-01"
HOOK=./hook.sh

dehydrated の domains.txt ファイルの修正

以下の様な書式で記載します。

*.example.com > wildcard.exaple.com

証明書発行

後は、いつもどおり証明書を要求すると、DNS-01 で認証され、発行されます。

その他

domains.txt で「example.com *.example.com」の様にすると、example.com に 2 回 DNS-01 認証が発生し、上手くいきませんでした。

Apache 2.2.16 における SSL の SNI 設定

Apache 2.2.12 以降では、SSL でも Server Name Indication (SNI) という仕組みで名前ベースのバーチャルホストが設定可能です。

と言うわけで、自サーバ (Debian GNU/Linux Squeeze + apache 2.2.16) で設定をしてみました。

と言っても特別なことは不要で、普通に 80 番ポートの名前ベースのバーチャルホストと同じように設定すれば良いだけです。

・/etc/apache2/ports.conf

NameVirtualHost *:80
Listen 80
<IfModule mod_ssl.c>
    NameVirtualHost *:443
    Listen 443
</IfModule>

SSL で使用する TCP 443 ポートでも「NameVirtualHost」の指定をしてあげます。

・/etc/apache2/mods-available/ssl.conf

SSLStrictSNIVHostCheck Off

上記の指定を追加します。

「SSLStrictSNIVHostCheck Off」は、SNI 非対応環境からのアクセスの場合、一番最初のデフォルトのバーチャルホストへのアクセスを許可する設定です。これを「On」とした場合、SNI 非対応環境からは SSL サイトへの接続自体が拒否されます。

あとは、普通に /etc/apache2/sites-available 以下に通常のバーチャルホストと同じ感覚で、SSL サイトのバーチャルホストを作成していき、a2ensite で有効にします。

ただし、デフォルトとして使用するバーチャルホストは最初に読み込まれる必要があるので、例えば、https://www.example.com/ (設定ファイルは www.example.com-ssl) と https://www.example.net/ (設定ファイルは www.example.net-ssl) があり、SNI 非対応環境からのアクセスは https://www.example.com/ と限定する場合、下記のようにシンボリックリンクのファイル名を変更します。

# cd /etc/apache2/sites-enabled/
# mv www.example.com-ssl 000-www.example.com-ssl
# /etc/init.d/apache2 restart

これで、SNI 対応環境からはそれぞれ該当の SSL サイトへ名前ベースのバーチャルホストでアクセスできるようになります。

なお、SNI 非対応環境からは https://www.example.com/ では証明書エラーが発生せず通常にアクセス可能、https://www.example.net/ では証明書エラーが発生するようになります。

ちなみに、Mac OS X 10.6 の Safari 5.1.7 や、Windows XP の Firefox 13.0.1 では正常にそれぞれの SSL サイトへのアクセスが出来ました。

謎なのが、Windows XP 上の Internet Explorer 8 。この組み合わせでは SNI に対応しないのは知っていたのですが、デフォルト以外の SSL サイトへアクセスした場合、証明書のエラーは出るものの、実は参照しているドキュメントルートは正しいサイトのものであるという・・・。

Apache2 における共有 SSL 機能の実装

Apache2 において、よくレンタルサーバ等で利用されている共有 SSL 機能を簡単に実装できることがわかったので、そのメモです。(説明は良いからすっとばして設定方法へ進む)

そもそも共有 SSL の存在意義ですが、現時点での SSL の実装では、URL も暗号化されて来るため、ウェブサーバ側では実際に暗号を複合してみないと、どの URL にアクセスしてきているのかわかりません。

つまり、単一 IP アドレスしか持たないサーバでは、名前ベースのバーチャルホストで複数の SSL サイトを実現することは出来ません。

何とかして、単一ウェブサーバで複数ドメインの SSL を実装する方法としては、

  • SSL コモンネーム毎に、IP アドレスを割り当てる。
  • SSL コモンネーム毎に、ポート番号を変更する。
  • SAN を含む SSL 証明書 (いわゆるマルチドメイン SSL 証明書) を使う。
  • SNI を使用する。

が考えられます。

最初の方法は SSL 対応すべきコモンネームが必要な個数分の IP アドレスが必要で、現時点での IPv4 の枯渇からして厳しいし、中間の方法は、2 個目のコモンネームから標準的では無いポート番号でのアクセスになるため、環境によってはアクセスできない可能性がある等の理由で現実的ではありません。

SAN を含む SSL 証明書を使うの方法は一見上手くいけそうですが、前提としてブラウザ側が SAN に対応していなければならないし、しかもその単一のバーチャルホストの定義で、証明書に含まれている分の複数のコモンネームでアクセスしてもエラーにならない、と言うことだけあって、名前ベースのバーチャルホストが出来るわけでは無いので、どのみち目的は果たせません。(という認識なのですが、間違っていたらコメントで突っ込みをいただけると嬉しいです)

本命は、SNI を使用する方法なのですが、これも、ブラウザ側の対応と、サーバ側では Apache 2.2.12 以降が必要なので、みんなが SNI 対応ブラウザを使うまで待つしか無いです。

で、前置きが非常に長くなりましたが、SNI 対応クライアント環境の普及や、IPv4 でのアクセスが主流の現時点においての妥協策としては、共有 SSL の実装が無難と考えられます。しかも、先にも書いたとおり、その方法もそんなに難しくないです。(環境は、例によって Debian GNU/Linux 6.0 + Apache 2.2.16 です)

なお、以下のような共有 SSL の設定を想定します。既にバーチャルホストは設定されているものとします。

http://www.example.com/ の共有 SSL での表示は、https://ssl.example.jp/www.example.com/
http://www.example.net/ の共有 SSL での表示は、https://ssl.example.jp/www.example.net/

  • /etc/apache2/mods-available/proxy.conf ファイルを下記のように設定します。

    ProxyRequests Off
    ProxyVia On #こちらは必須では無い。

    基本的に Debian GNU/Linux 6.0 の apache2 においてはデフォルトのままで OK 。

  • 以下のコマンドを入力して 必要な proxy モジュールを有効にします。

    # a2enmod proxy proxy_http
  • https://ssl.example.jp/ を定義しているバーチャルホスト設定部分で下記を追加します。

    ProxyPass /www.example.com/ http://www.example.com/
    ProxyPass /www.example.net/ http://www.example.net/

これで、Apache2 を再起動することで、https://www.example.jp/www.example.com/ にアクセスすると、サーバ内部で http://www.example.com/ へアクセスが行われるため、上記ドメインで、http://www.example.com/ と同一のコンテンツ内容が表示されます。また、www.example.net についても同様です。

注意点としては下記があります。

  • アクセス元がサーバ自身となる。(しかし アクセス元が自サーバであった場合に、HTTP_X_FORWARDED_FOR 変数を参照することで、アクセス元はわかる)
  • 自分自身のサーバ内で完結すること。(他のサーバをアクセス先にすると、リバースプロキシを設定したサーバと他のサーバ感の間の通信は HTTP となり暗号化されない)
  • URL の階層が非 SSL でのアクセスの場合と SSL のそれとでずれる。(これは、ワイルドカード SSL と mod_rewrite を使えば解決できそうだが、携帯電話からのアクセスに対応できないと思われる)

上記中、特に、自分自身のサーバ内で、非 SSL 部分となる通信を完結することは重要です。