letsencrypt.sh for Let's Encrypt DNS challenge
無料でSSL証明書を発行してくれるサービスのletsencryptには、DNSでドメイン認証を行う方法がありサーバ側は既に対応済みだが、公式のクライアントはまだ問題があるらしく対応していない。公式の代わりにLukas Schauer氏のletsenrypt.shがDNS認証に対応してあるので、それを使ってLetsEncrypt証明書の管理をしている。その備忘録。
hookスクリプト
letsencrypt.shのDNS認証部分については、大まかに下記のような流れになっている。
- letsencryptのサーバにCommonNameを申請
- letsencryptのサーバからDNSのテキストレコードに登録する値を取得
- 取得した値を
_acme-challenge.COMMON-NAME.
という名前でテキストレコードとしてDNSに登録 - 登録されたレコードをletsencryptが確認
- 確認ができれば証明書を発行
この処理について別途追加する必要があり、それぞれのDNS事情にあった認証を行えるようになっている。有名どころなDNSプロバイダについては、有志によってスクリプトが作成されているが、そこからリンクされているroute53のrubyスクリプトはサブドメインを渡した場合に不具合があるので、そのままでは動かなかった。
利用してるものは、もう少し手を加えているが、動くように修正したものが下記。
#!/usr/bin/env ruby
require 'aws-sdk'
require 'domain_name'
# ------------------------------------------------------------------------------
# Credentials
# ------------------------------------------------------------------------------
# pick up AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY by default from
# environment
Aws.config.update({
region: 'ap-northeast-1',
})
# ------------------------------------------------------------------------------
def setup_dns(common_name, domain, txt_challenge)
route53 = Aws::Route53::Client.new()
hosted_zone = route53.list_hosted_zones_by_name(
{dns_name: "#{domain}."}).hosted_zones[0]
changes = []
changes << {
action: "UPSERT",
resource_record_set: {
name: "_acme-challenge.#{common_name}.",
type: "TXT",
ttl: 60,
resource_records: [
value: "\"#{txt_challenge}\"",
],
},
}
resp = route53.change_resource_record_sets({
hosted_zone_id: hosted_zone.id,
change_batch: {
changes: changes,
},
})
sleep 20
end
def delete_dns(common_name, domain, txt_challenge)
route53 = Aws::Route53::Client.new()
hosted_zone = route53.list_hosted_zones_by_name(
{dns_name: "#{domain}."}).hosted_zones[0]
changes = []
changes << {
action: "DELETE",
resource_record_set: {
name: "_acme-challenge.#{common_name}.",
type: "TXT",
ttl: 60,
resource_records: [
value: "\"#{txt_challenge}\"",
],
},
}
resp = route53.change_resource_record_sets({
hosted_zone_id: hosted_zone.id,
change_batch: {
changes: changes,
},
})
sleep 10
end
if __FILE__ == $0
hook_stage = ARGV[0]
common_name = ARGV[1]
txt_challenge = ARGV[3]
domain = DomainName(common_name).domain
puts " hook_stage: #{hook_stage}"
puts " common_name: #{common_name}"
puts " domain: #{domain}"
puts "txt_challenge: #{txt_challenge}"
if hook_stage == "deploy_challenge"
setup_dns(common_name, domain, txt_challenge)
elsif hook_stage == "clean_challenge"
delete_dns(common_name, domain, txt_challenge)
end
end
rubygemのaws-sdk
最新版とdomain_name
が必要。
このスクリプトではhook_stage
がdeploy_challenge
とclean_challenge
の場合の処理しか無いが、証明書作成ができた時点にもhook_stage
にdeploy_cert
が指定され、txt_challenge
に作成された証明書のpathが指定されて、このhookスクリプトが呼ばれる。
letsencrypt.sh 利用方法
letsencrypt.sh
のインストールおよび利用方法について、簡単にまとめておく。 /etc/letsencrypt.sh
を基準とするようにしてある。
cd /etc
git clone https://github.com/lukas2511/letsencrypt.sh
cd letsencrypt.sh
同じディレクトリ上にconfig.sh
があれば、スクリプト実行時にデフォルト値として動作するようになっているので、作成しておく。 CHALLENGETYPEをdns-01、RENEW-DAYSは45日、秘密鍵は毎回再作成、hookスクリプトの指定を行っている。
CA="https://acme-v01.api.letsencrypt.org/directory"
LICENSE="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
CHALLENGETYPE="dns-01"
HOOK="/etc/letsencrypt.sh/hook.rb"
HOOK_CHAIN="no"
RENEW_DAYS="45"
ACCOUNT_KEY="/etc/letsencrypt.sh/private_key.pem"
ACCOUNT_KEY_JSON="/etc/letsencrypt.sh/private_key.json"
KEYSIZE="4096"
WELLKNOWN="/etc/letsencrypt.sh/.acme-challenges"
PRIVATE_KEY_RENEW="yes"
OPENSSL_CNF="/usr/lib/ssl/openssl.cnf"
CONTACT_EMAIL=""
LOCKFILE="/etc/letsencrypt.sh/lock"
同様にdomains.txt
が同じディレクトリにあれば、記載してあるコモンネームを利用するようになっていて、1行に1証明書で作成される。1行に複数のコモンネームを指定可能で、複数のコモンネームを1枚の証明書で作成できるようになっている。以下は実際に作ったサンプル。
d6rkaiz.com mta.d6rkaiz.com blog.d6rkaiz.com
deny.jp mta.deny.jp
実際の運用には下記のようなスクリプトを作成し、cronで毎月1回実行している。
#!/bin/bash
PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LEDIR=/etc/letsencrypt.sh
set -e
tagname="letsencrypt_renew"
${LEDIR}/letsencrypt.sh -c -x | logger -t ${tagname}
${LEDIR}/letsencrypt.sh -gc | logger -t ${tagname}
svcs="nginx postfix dovecot"
for svc in ${svcs}
do
service $svc restart | logger -t ${tagname}
done