無償で利用できるTLS(SSL)証明書として当たり前のように用いられるようになったLet’s Encryptですが、 組織内部で利用するサーバなどでこの証明書を用いたい場合、一般的に用いられるHTTP-01チャレンジでは インターネット側からWebサーバへのアクセスを受け入れる必要がある上、ドメインごとにチャレンジ手続きが必要で面倒です。

そこで今回は、Let’s Encryptで複数のサブドメインの証明書をひとつにまとめられるワイルドカード証明書を入手する環境を構築します。

Let’s Enctyptでワイルドカード証明書を入手したい時は、DNSサーバを用意しDNS-01チャレンジに対応する必要があります。

必要なもの

最低限下記のものが必要です。

  • てきとうなLinuxサーバ
    • Ubuntu Server(22.04)を想定
    • ホスト名はletsencrypt.negix.org
    • DNSサーバとしてnsd、Let’s Encryptのクライアントとしてcertbotをインストール
    • 外部からUDP/TCP53を受け付ける
  • グローバルIPv4アドレス
    • 固定のもの
  • ドメイン名
    • 自身がNSレコードを設定できるもの
    • 今回はnegix.orghachune.netを用いる
    • value-domainで取得し、value-domainの標準DNSサーバを利用

今回のように証明書を取得・管理するだけであれば、サーバはConoHaさくらのVPSの512MB~1GBプランで大丈夫です。 (ついでにWebサーバを立てるぐらいのことはできます。)

letsencrypt.negix.orgのサーバ上で動かすDNSサーバとして、 nsdをインストールします。 証明書の取得には、毎度おなじみcertbotを用います。 いずれもubuntu-22.04の標準パッケージのもので大丈夫です。 また、レコードの確認にdrillコマンドを用いるため、ldnsutilsもインストールしておくとよいでしょう。

取得する証明書

下記の証明書をそれぞれ取得します。

  • negix.org*.negix.orgの証明書をひとつ
  • hachune.net*.hachune.netの証明書をひとつ

value-domain標準DNSサーバ側のレコード設定

DNS-01チャレンジでは、_acme-challenge.対象ドメイン名(negix.orgの場合は_acme-challenge.negix.org)のTXTレコードに、 Let’s Encrypt側から指定された値を設定することで、ドメインの所有権が確認されます。 ここで指定される値は毎回ランダムであるため、TXTレコードの設定は自動化が必須となっています。

しかし、value-domainの標準レンタルDNSサーバなどを用いているとなかなか自動化し辛いのが現状です。 (Route53やCloudflare DNSなどのイマドキなクラウドサービスを利用しているのであれば話は別かもしれませんが…) また、DNSサーバを完全に自前で運用しようとすると、それなりに面倒なことになりがちです。

自動化し辛いDNSサーバを利用しており、現状をなるべく維持したい場合は、 _acme-challenge.対称ドメイン名のNSレコードを自身が用意したDNSサーバ(letsencrypt.negix.org)に向け、 そこだけ自前で管理する手法が手軽でしょう。

今回は、「value-domainのDNSサーバのnegix.org用レコード」に、下記のように追記しました。

1
2
ns _acme-challenge letsencrypt.negix.org.
a letsencrypt 160.251.76.38

これにより、_acme-challenge.negix.org以下に関するクエリは、letsencrypt.negix.orgに飛んでくるようになります。 drillコマンドで確認してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
% drill @8.8.8.8 NS _acme-challenge.negix.org.
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 60654
;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; _acme-challenge.negix.org. IN      NS

;; ANSWER SECTION:
_acme-challenge.negix.org.    1       IN      NS      letsencrypt.negix.org.

;; AUTHORITY SECTION:

;; ADDITIONAL SECTION:

;; Query time: 40 msec
;; SERVER: 8.8.8.8
;; WHEN: Fri Feb 24 18:21:20 2023
;; MSG SIZE  rcvd: 65

他のドメインもまとめて管理したい場合

他のドメインもまとめて管理したい場合は、CNAMEレコードを設定します。

例えば、hachune.netもまとめて管理したい場合は、「value-domainのDNSサーバのhachune.net用レコード」に 下記のように記述し、_acme-challenge.hachune.net_acme-challenge.negix.orgのエイリアスにします。

1
cname _acme-challenge _acme-challenge.negix.org.

設定できたら、またdrillコマンドで確認してみましょう。 _acme-challenge.hachune.net.のNSレコードを参照すると、_acme-challenge.negix.org.のエイリアスであることが分かり、 _acme-challenge.negix.org.のNSレコードはletsencrypt.negix.org.である、となっていることが確認できます。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
% drill @8.8.8.8 NS _acme-challenge.hachune.net.
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 6535
;; flags: qr rd ra ; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; _acme-challenge.hachune.net.   IN      NS

;; ANSWER SECTION:
_acme-challenge.hachune.net.  1800    IN      CNAME   _acme-challenge.negix.org.
_acme-challenge.negix.org.    1       IN      NS      letsencrypt.negix.org.

;; AUTHORITY SECTION:

;; ADDITIONAL SECTION:

;; Query time: 197 msec
;; SERVER: 8.8.8.8
;; WHEN: Fri Feb 24 18:23:13 2023
;; MSG SIZE  rcvd: 104

自前DNSサーバ(letsencrypt.negix.orgのnsd)の設定

_acme-challenge.negix.org.のクエリを処理できるよう、自前DNSサーバを構築します。

/etc/nsd/nsd.conf

インストール時のまま変更していません。 設定の追記は、/etc/nsd/nsd.conf.d/*.confファイルに対して行います。

1
2
3
4
5
server:
        # log only to syslog.
        log-only-syslog: yes

include: "/etc/nsd/nsd.conf.d/*.conf"

/etc/nsd/nsd.conf.d/negix.org.conf

サーバの設定と、ゾーンごとの設定を記述します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server:
    # 0.0.0.0:53をbindする
    ip-address: 0.0.0.0
    port: 53
    # IPv4を用いる
    do-ip4: yes
    # nsdのバージョンを隠す
    hide-version: yes
    # drill CH TXT id.server.した際に返される値
    identity: "negix.org authoritative DNS"
    # ゾーンファイルを置くディレクトリ
    zonesdir: "/etc/nsd/zone"

# _acme-challenge.negix.orgゾーンの設定を記述
zone:
        name: _acme-challenge.negix.org.
        # ゾーンファイルとして/etc/nsd/zone/_acme-challenge.negix.org.zoneが読まれる
        zonefile: _acme-challenge.negix.org.zone

/etc/nsd/zone/_acme-challenge.negix.org.zone

とりあえず、_acme-challenge.negix.orgのNSレコードを問い合わせられた際に、 自分自身(letsencrypt.negix.org)であることを返すようにしましょう。

1
2
3
4
5
6
7
8
9
10
11
12
$ORIGIN _acme-challenge.negix.org.
$TTL 1

@       IN      SOA     letsencrypt.negix.org. admin.negix.org. (
           1674698406     ; serial number
           600            ; Refresh
           300            ; Retry
           86400          ; Expire
           600            ; Min TTL
           )

@       IN      NS      letsencrypt.negix.org.

後でこのファイルは、Let’s Encryptからの指示に従って自動的に置き換えられるようにします。

サーバの起動とレコードの確認

systemctlnsdを有効化しつつ起動します。 systemctl statusで確認し、active (running)になっていればとりあえず動作はしているはずです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
% sudo systemctl enable --now nsd
% sudo systemctl status nsd
● nsd.service - Name Server Daemon
     Loaded: loaded (/lib/systemd/system/nsd.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2023-02-24 19:03:59 JST; 2min 49s ago
       Docs: man:nsd(8)
   Main PID: 2103013 (nsd: xfrd)
      Tasks: 3 (limit: 1030)
     Memory: 117.0M
        CPU: 177ms
     CGroup: /system.slice/nsd.service
             ├─2103013 /usr/sbin/nsd -d -P ""
             ├─2103015 /usr/sbin/nsd -d -P ""
             └─2103016 /usr/sbin/nsd -d -P ""

Feb 24 19:03:58 lumia systemd[1]: Starting Name Server Daemon...
Feb 24 19:03:59 lumia nsd[2103013]: nsd starting (NSD 4.3.9)
Feb 24 19:03:59 lumia nsd[2103015]: nsd started (NSD 4.3.9), pid 2103013
Feb 24 19:03:59 lumia systemd[1]: Started Name Server Daemon.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
% drill @127.0.0.1 ns _acme-challenge.hachune.net
;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 3777
;; flags: qr aa rd ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;; _acme-challenge.negix.org. IN      NS

;; ANSWER SECTION:
_acme-challenge.negix.org.    1       IN      NS      letsencrypt.negix.org.

;; AUTHORITY SECTION:

;; ADDITIONAL SECTION:

;; Query time: 1 msec
;; SERVER: 127.0.0.1
;; WHEN: Fri Feb 24 19:07:38 2023
;; MSG SIZE  rcvd: 65

nsdがうまく起動しない場合

systemd-resolvedがUDP53を掴んでいて、nsdの起動に失敗することがよくあります。 そのような時は、/etc/systemd/resolved.confを下記のように編集してDNSSutbListenerを無効化し、 systemd-resolvedを再起動してみてください。

1
2
[Resolve]
DNSStubListener=no

今どのプロセスがどのポートを掴んでいるかは、ssコマンドで確認できます。

1
2
3
4
5
6
7
% sudo ss -4 -l -p -n
Netid    State     Recv-Q    Send-Q        Local Address:Port        Peer Address:Port   Process
udp      UNCONN    0         0                   0.0.0.0:53               0.0.0.0:*       users:(("nsd: server 1",pid=2103016,fd=4),("nsd: main",pid=2103015,fd=4),("nsd: xfrd",pid=2103013,fd=4))
udp      UNCONN    0         0        160.251.76.38%eth0:68               0.0.0.0:*       users:(("systemd-network",pid=149157,fd=18))
tcp      LISTEN    0         256                 0.0.0.0:53               0.0.0.0:*       users:(("nsd: server 1",pid=2103016,fd=5),("nsd: main",pid=2103015,fd=5),("nsd: xfrd",pid=2103013,fd=5))
tcp      LISTEN    0         128                 0.0.0.0:22               0.0.0.0:*       users:(("sshd",pid=148045,fd=3))
tcp      LISTEN    0         16                127.0.0.1:8952             0.0.0.0:*       users:(("nsd: server 1",pid=2103016,fd=7),("nsd: main",pid=2103015,fd=7),("nsd: xfrd",pid=2103013,fd=7))

レコード更新スクリプトを作成

certbotを実行すると、Let’s EncryptのサーバからDNS-01チャレンジ用の値が送られてきます。 この値をTXTレコードに自動的に登録するためのスクリプトを用意します。

nsdの場合、レコードの動的な追加ができないため、ゾーンファイルを更新するスクリプトを記述します。

/etc/letsencrypt/template-acme-challenge.zone

ゾーンファイルの元となるファイルです。 これにLet’s Encryptから送られてきた値を追記し、nsdに再読み込みさせます。

1
2
3
4
5
6
7
8
9
10
11
12
$ORIGIN _acme-challenge.negix.org.
$TTL 1

@ IN SOA letsencrypt.negix.org. admin.negix.org. (
           SERIAL_NUMBER  ; serial number
           600            ; Refresh
           300            ; Retry
           86400          ; Expire
           600            ; Min TTL
           )

@ IN NS letsencrypt.negix.org.

/etc/letsencrypt/auth.sh

certbotがLet’s Encryptからチャレンジの値を受け取った際に実行されるスクリプトです。 チャレンジの値はCERTBOT_VALIDATION変数に入るようになっているので、 これをゾーンファイルのTXTレコードに追記し、nsd-control reloadでゾーンファイルを再読み込みさせます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/sh

# env | tee /tmp/letsencrypt.log
# 変数の例
# CERTBOT_VALIDATION=A7sjrWIkY5r67aG7zSiYh0FE9_0_M14Qe8-UyCuKnNE
# CERTBOT_DOMAIN=hachune.net

SERIAL_NUMBER=`date '+%s'`
TMP_RECORD_FILE='/etc/letsencrypt/record.zone'
ZONE_TEMPLATE_FILE='/etc/letsencrypt/template-acme-challenge.zone'

echo DOMAIN: ${CERTBOT_DOMAIN}
echo CODE: $CERTBOT_VALIDATION
echo "@ IN TXT ${CERTBOT_VALIDATION}" >> "${TMP_RECORD_FILE}"

if [ "$CERTBOT_REMAINING_CHALLENGES" = "0" ]; then
  cat "${ZONE_TEMPLATE_FILE}" "${TMP_RECORD_FILE}" |
  sed -e "s/SERIAL_NUMBER/${SERIAL_NUMBER}/" \
    > /etc/nsd/zone/_acme-challenge.negix.org.zone
  nsd-control reload
  sleep 3
  drill @127.0.0.1 TXT _acme-challenge.negix.org.
fi

/etc/letsencrypt/cleanup.sh

DNS-01チャレンジが完了した後に実行される、不要になったTXTレコードを削除するスクリプトです。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

TMP_RECORD_FILE='/etc/letsencrypt/record.zone'
ZONE_TEMPLATE_FILE='/etc/letsencrypt/template-acme-challenge.zone'
SERIAL_NUMBER=`date '+%s'`

echo Remove "${TMP_RECORD_FILE}"
rm -f "${TMP_RECORD_FILE}"

sed -e "s/SERIAL_NUMBER/${SERIAL_NUMBER}/" "${ZONE_TEMPLATE_FILE}" \
  > /etc/nsd/zone/_acme-challenge.negix.org.zone
nsd-control reload

certbotの実行

negix.org*.negix.orgの証明書が必要な際は、下記のようにcertbotを実行します。 今回はサーバ証明書の公開鍵にECDSAを用いたかったため、--key-type ecdsa--elliptic-curve secp384r1を追加しています。 (デフォルトではRSA 2048bitになるはずです。)

DNS-01チャレンジを用いて証明書を取得するため、--preferred-challenges dns01を指定します。 また、DNSサーバにチャレンジの値を追加/削除するスクリプトを--manual-auth-hook--manual-cleanup-hookで指定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
% sudo certbot certonly \
  --manual \
  --expand \
  --domain "negix.org" --domain "*.negix.org" \
  --key-type ecdsa \
  --elliptic-curve secp384r1 \
  --email "てきとうな自分のメールアドレス" \
  --agree-tos \
  --manual-public-ip-logging-ok \
  --manual-auth-hook /etc/letsencrypt/auth.sh \
  --manual-cleanup-hook /etc/letsencrypt/cleanup.sh \
  --keep-until-expiring \
  --preferred-challenges dns-01

証明書を発行できる回数には制限がある(時間・IPアドレス単位)ので注意しましょう。 実際に証明書は発行せず、チャレンジの動作テストを行いたい時は、 --dry-run--server "https://acme-staging-v02.api.letsencrypt.org/directoryオプションを追加します。

出力される証明書

生成された証明書は、/etc/letsencrypt/live/ドメイン名以下に出力されます。 (各ファイルが/etc/letsencrypt/archive/ドメイン名からのシンボリックリンクになっています。 archiveディレクトリ以下にはこれまで発行した証明書と秘密鍵が保存されており、 最新のものがliveディレクトリにシンボリックリンクされる形となっています。)

live/ドメイン名ディレクトリ内の各ファイルの内容は下記の通りです。

ファイル名 役割
cert.pem サーバ証明書(通常単体では使用しない)
chain.pem ルート認証局と中間局の証明書。OCSPで用いる。
fullchain.pem ルート~中間~サーバ証明書をすべてまとめたもの。Webサーバの設定ではこれを用いる。
privkey.pem サーバ証明書の公開鍵に対する秘密鍵。秘密なのでアクセス権限に注意(通常は600)。

証明書の更新

次回以降証明書を更新する際は、certbot renewコマンドを実行するだけです。 systemdのtimerで、数日~1週間ぐらいおきに定期実行するようにしておきましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
% sudo certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/hachune.net.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/negix.org.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The following certificates are not due for renewal yet:
  /etc/letsencrypt/live/hachune.net/fullchain.pem expires on 2023-04-26 (skipped)
  /etc/letsencrypt/live/negix.org/fullchain.pem expires on 2023-04-26 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

参考文献