はじめに
Caddyで、特定のドメイン(www.example.com
)についてのDV証明書をLet's Encryptから取得するのはほぼ設定なしで簡単にできますが、ワイルドカードなドメイン(*.example.com
)の証明書の取得はやったことがなかったので、今回チャレンジしてみました。
前提
- Route53で管理されているゾーン(
example.com
)のドメインのワイルドカード証明書(*.example.com
)を取得することとします。 - TerraformにてAWS上でのプロビジョニングを記述するものとします。
- Caddyのバージョンは
2.75
を用いています。
ワイルドカード証明書取得のチャレンジ要件
- ワイルドカード証明書の取得にあたっては、ドメイン所有の証明方法としてHTTP-01チャレンジではなくDNS-01チャレンジを用いる必要があります。
- DNS-01チャレンジはHTTP-01チャレンジと違い、WebサーバがHTTP越しに外部からアクセス可能でなくても構わないですが、一方で チャレンジ中に要求されたDNSレコードを作成できる必要があります。
- DNSレコードの作成については、自動か人力かは問われません。
- 今回の場合であれば、CaddyにRoute53の操作をしてもらう必要があります。
Route53 (AWS) 側の準備 〜 IAMユーザとそのアクセスキーの作成
下記でDNS-01認証の際にだけ使うIAMユーザを作成し、そのアクセスキーを作成しておきます。
(あとでCaddyの設定ファイルに作成したアクセスキーを記述します。)
data "aws_route53_zone" "my_zone" {
name = "example.com."
}
resource "aws_iam_user" "acme_client_user" {
name = "acme_client_user"
}
resource "aws_iam_user_policy" "acme_client_user" {
name = "acme_client_user"
user = aws_iam_user.acme_client_user.name
policy = jsonencode({
Version : "2012-10-17",
Statement : [
{
Action : [
"route53:Get*",
"route53:List*",
"route53:ChangeResourceRecordSets"
],
Effect : "Allow",
Resource : [data.aws_route53_zone.my_zone.arn]
},
{
Action : ["route53:ListHostedZonesByName"],
Effect : "Allow",
Resource : "*"
}
]
})
}
# アクセスキーはマネージメントコンソールで発行してください
Caddyの準備 〜 Route53プラグインの組み込み
パッケージ版のCaddyを入れている人には大変悲しいお知らせですが、 素のCaddyはそのままだとRoute53とやり取りすることができません。 (別のDNSプロバイダであっても同じです)
なので、Route53とやり取りするためのプラグインを組み込んだCaddyをビルドする必要があります。
カスタムビルドのためにGoのコンパイラなどを設定してビルド等するのも非常に面倒なので、いっそのことDockerでCaddyをビルド定義ごと扱うと楽になれます。
具体的には下記のDocker Compose定義を用いればいいでしょう。
services:
caddy_server:
image: my-custom-built-caddy
# 立ち上げる際に ./caddy_image/Dockerfile でビルドする
build: ./caddy_image
restart: always
# ローカルホストにそのままプロキシさせたいのでホストネットワークモード
network_mode: host
volumes:
# Caddyfile を置いているディレクトリ (後々のリロード操作のためにディレクトリをマウント)
- ./caddy_conf:/etc/caddy:ro
# 証明書やACME情報等の保存先
- ./caddy_data:/data
FROM caddy:2.7.5-builder AS builder
# Route53とやりとりするための拡張を組み込む
RUN xcaddy build \
--with github.com/caddy-dns/route53@v1.4.0
FROM caddy:2.7.5
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
{
email admin@example.com
}
*.example.com {
tls {
dns route53 {
region us-east-1
access_key_id 発行したアクセスキーID
secret_access_key 発行したシークレットアクセスキー
}
}
}
上記の設定さえ行えば後は起動するだけです!
$ docker compose up -d
# => https://any-string-goes-here.example.com 😉
ところで、Route53で書き換えるゾーンIDの指定がCaddyfileに見当たらないけれど、どういう仕組みになっているの?
上記のCaddyfileでは、Route53についてアクセスキーIDとシークレットアクセスキーしか設定していませんが、よくよく考えるとこれだけではAWS上ではドメインの設定を書き換えることができません。書き換える対象のレコードがどのゾーンにいるかわからないからです。
# 人間が見れば *.example.com のゾーンは example.com にありそうだと予測がつくが、
# サブドメインのワイルドカードなどであれば誰にもゾーンがどこかわからない
*.example.com {
tls {
dns route53 {
access_key_id 発行したアクセスキーID
secret_access_key 発行したシークレットアクセスキー
# ここにゾーンの指定があってもよさそうだが、無い
}
}
}
ではCaddyは具体的にはどうやってゾーンを特定しているのか? というと、チャレンジ中にCaddy上から example.com
についてDNS問い合わせし、SOAレコードを見てレコードを追加するゾーンを特定しているようです。
(参考: solvers.go, dnsutil.go)
// Present creates the DNS TXT record for the given ACME challenge.
func (s *DNS01Solver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
zone, err := findZoneByFQDN(dnsName, recursiveNameservers(s.Resolvers))
if err != nil {
return fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err)
}
rec := libdns.Record{
Type: "TXT",
Name: libdns.RelativeName(dnsName+".", zone),
Value: keyAuth,
TTL: s.TTL,
}
// findZoneByFQDN determines the zone apex for the given fqdn by recursing
// up the domain labels until the nameserver returns a SOA record in the
// answer section.
func findZoneByFQDN(fqdn string, nameservers []string) (string, error) {
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
soa, err := lookupSoaByFqdn(fqdn, nameservers)
if err != nil {
return "", err
}
return soa.zone, nil
}
面白いですね!(ストレートに設定ファイルにゾーンIDを指定させる仕様でもよかったのでは???とも思いますが)
まとめ
DNS(Route53)・APIキー(AWSアクセスキー)・DNS拡張を組み込んだCaddyの3つさえ準備できれば簡単にワイルドカード証明書を取得できることがわかりました。
みなさんもぜひワイルドカード証明書を取得してみてください。