10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Caddy + Route53 + Let's Encryptでワイルドカード証明書が有効なWebサーバを立てる

Last updated at Posted at 2023-11-28

はじめに

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の設定ファイルに作成したアクセスキーを記述します。)

acme_user.tf
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定義を用いればいいでしょう。

compose.yml
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
caddy_image/Dockerfile
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
caddy_conf/Caddyfile
{
    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上ではドメインの設定を書き換えることができません。書き換える対象のレコードがどのゾーンにいるかわからないからです。

Caddyfile
# 人間が見れば *.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)

solvers.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,
	}
dnsutil.go
// 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を指定させる仕様でもよかったのでは???とも思いますが)

v1.5.0 (PR) にてゾーンIDを指定できるようになったようです

まとめ

DNS(Route53)・APIキー(AWSアクセスキー)・DNS拡張を組み込んだCaddyの3つさえ準備できれば簡単にワイルドカード証明書を取得できることがわかりました。

みなさんもぜひワイルドカード証明書を取得してみてください。

参考

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?