はじめに
AWS ACM を使っていると、証明書の更新を意識することはほとんどない。DNS 検証にしておけば、期限が来ても勝手に更新される。
では、ACM が使えない環境——EC2 で直接 TLS 終端する、オンプレ、VPS——では何が起きているのか。
そこで登場するのが ACME プロトコル(Let's Encrypt など)による証明書の自動発行・更新だ。
この記事では:
- ドメイン検証(DV)の仕組み(なぜ「所有確認」が必要か)
-
HTTP-01(
/.well-known/acme-challenge/の正体) - DNS-01(ワイルドカード証明書はこれ一択)
- TLS-ALPN-01(443番だけで完結する方式)
- 更新の自動化(systemd timer と deploy hook)
- AWS ACM との比較(ACM が隠している作業の正体)
を順番に整理する。コマンドと出力例つき。
前提:ドメイン検証(DV)とは
CA(認証局)は、証明書を発行する前に「申請者がそのドメインの持ち主かどうか」を確認する必要がある。
これが**ドメイン検証(Domain Validation, DV)**だ。
申請者「example.com の証明書をください」
CA 「本当に example.com の持ち主? 証明してみて」
申請者「(ドメインの支配権を使って証明する)」
CA 「OK、発行します」
この「証明してみて」のやりとりを自動化するプロトコルが ACME(Automatic Certificate Management Environment)で、代表的な CA が Let's Encrypt。
証明の方法(チャレンジ)は3方式ある:
| 方式 | 使うもの | ポート | ワイルドカード | 主な用途 |
|---|---|---|---|---|
| HTTP-01 | Web サーバー上のファイル | 80 | ❌ | 一般的な Web サーバー |
| DNS-01 | DNS の TXT レコード | 53(DNS側) | ✅ | ワイルドカード、Webサーバーなし |
| TLS-ALPN-01 | TLS ハンドシェイク | 443 | ❌ | 80番を開けられない環境 |
👉 どの方式も証明していることは同じ。「そのドメインの支配権を持っている」こと。Web サーバーに置けるのも、DNS を書き換えられるのも、持ち主だけだから。
HTTP-01:well-known/acme-challenge の正体
最もよく使われる方式。Web サーバーのログで見かける /.well-known/acme-challenge/ へのアクセスは、この検証のためのものだ。
検証の流れ
1. certbot が Let's Encrypt に「example.com の証明書がほしい」と申請
2. Let's Encrypt がトークン(ランダム文字列)を発行
3. certbot がトークンを含むファイルを Web サーバーに配置
→ /var/www/html/.well-known/acme-challenge/<トークン>
4. Let's Encrypt が外部から HTTP でアクセスして確認
→ GET http://example.com/.well-known/acme-challenge/<トークン>
5. 内容が一致すれば検証成功 → 証明書発行
👉 「そのドメインの Web サーバーにファイルを置ける = ドメインの持ち主」という理屈。
certbot で実行する
webroot モード(稼働中の nginx / Apache を止めずに検証できる):
$ sudo certbot certonly --webroot -w /var/www/html -d www.example.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for www.example.com
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/www.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/www.example.com/privkey.pem
This certificate expires on 2026-10-14.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
検証中に何が置かれているのか
検証の瞬間、webroot にはこんなファイルが置かれる(通常は一瞬で消えるので --debug-challenges で止めると観察できる):
$ ls /var/www/html/.well-known/acme-challenge/
5jmVXCLIN-Wx7Yjky4TDMIhBWJ0OWZKWMOJFmANLDDU
$ cat /var/www/html/.well-known/acme-challenge/5jmVXCLIN-Wx7Yjky4TDMIhBWJ0OWZKWMOJFmANLDDU
5jmVXCLIN-Wx7Yjky4TDMIhBWJ0OWZKWMOJFmANLDDU.xxN2ZS3giFsw9-N0LOOwtBIrLBHcMcQjLBOwXK0jvvE
ファイルの中身は <トークン>.<アカウント鍵のフィンガープリント>。Let's Encrypt は外部からこう確認する:
$ curl http://www.example.com/.well-known/acme-challenge/5jmVXCLIN-Wx7Yjky4TDMIhBWJ0OWZKWMOJFmANLDDU
5jmVXCLIN-Wx7Yjky4TDMIhBWJ0OWZKWMOJFmANLDDU.xxN2ZS3giFsw9-N0LOOwtBIrLBHcMcQjLBOwXK0jvvE
❌ 「HTTPS の証明書の検証だから 443 番で行われる」は誤解。HTTP-01 の検証は必ず 80 番の HTTP から始まる。まだ有効な証明書がない状態で検証するのだから当然だ(80→443 へのリダイレクトには追従する)。
HTTP-01 の制約
- 80 番ポートが外部から到達可能であることが必須。閉じている環境では使えない
- ワイルドカード証明書(
*.example.com)は取得できない - ロードバランサ配下で複数台に分散している場合、検証リクエストがファイルを置いたサーバーに届くとは限らない
- 👉 対策:
/.well-known/acme-challenge/だけ特定サーバーにルーティングする、共有ストレージに置く、または DNS-01 を使う
- 👉 対策:
DNS-01:TXT レコードで証明する
Web サーバーではなく DNS に検証値を置く方式。
検証の流れ
1. certbot が申請 → Let's Encrypt がトークンを発行
2. certbot(または管理者)が TXT レコードを作成
→ _acme-challenge.example.com. IN TXT "<検証値>"
3. Let's Encrypt が DNS を引いて確認
4. 一致すれば検証成功 → 証明書発行
👉 「DNS レコードを書き換えられる = ドメインの持ち主」という理屈。Web サーバーの存在すら不要。
certbot で実行する(手動モード)
仕組みを見るために、まず手動モードで:
$ sudo certbot certonly --manual --preferred-challenges dns -d '*.example.com'
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for *.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:
_acme-challenge.example.com.
with the following value:
X4a9pVoh_kT3rGm2LcYuA8sBq1DfE6wZhJnP0iRxOyM
Before continuing, verify the TXT record has been deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
TXT レコードを設定したら、続行する前に自分でも確認する:
$ dig TXT _acme-challenge.example.com +short
"X4a9pVoh_kT3rGm2LcYuA8sBq1DfE6wZhJnP0iRxOyM"
Enter を押すと検証が走り、証明書が発行される:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2026-10-14.
DNS-01 の特徴
- ワイルドカード証明書(
*.example.com)はこの方式でしか取得できない - 80/443 番が閉じていても使える(検証は DNS 側で完結)
- 内部向けサーバー(外部から HTTP 到達不可)の証明書も取得できる
❌ ただし手動モードは運用に耐えない。更新のたびに TXT レコードの値が変わるので、90日ごとに手作業が発生する。
👉 実運用では DNS プロバイダの API と連携するプラグインで自動化する:
# Route 53 の場合
$ sudo certbot certonly --dns-route53 -d 'example.com' -d '*.example.com'
certbot-dns-cloudflare、certbot-dns-google など主要プロバイダのプラグインが揃っている。
TLS-ALPN-01:443番だけで完結する方式
3つ目の方式。HTTP を一切使わず、TLS ハンドシェイクの中で検証する。
検証の流れ
1. クライアントが申請 → Let's Encrypt がトークンを発行
2. クライアントが検証値を埋め込んだ「検証専用の自己署名証明書」を用意
3. Let's Encrypt が 443 番に TLS 接続
→ ALPN 拡張で "acme-tls/1" プロトコルを指定
4. サーバーが検証専用証明書を提示 → 中身が一致すれば成功
👉 ALPN(Application-Layer Protocol Negotiation)は TLS ハンドシェイクで「この接続で話すプロトコル」を合意する拡張。HTTP/2 の h2 ネゴシエーションと同じ仕組みを検証に流用している。
certbot は非対応、lego などで使う
certbot は TLS-ALPN-01 に対応していない。Go 製の ACME クライアント lego での例:
$ sudo lego --email admin@example.com --domains example.com --tls run
2026/07/16 10:00:00 [INFO] [example.com] acme: Obtaining bundled SAN certificate
2026/07/16 10:00:01 [INFO] [example.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/1234567890
2026/07/16 10:00:01 [INFO] [example.com] acme: use tls-alpn-01 solver
2026/07/16 10:00:02 [INFO] [example.com] acme: Trying to solve TLS-ALPN-01
2026/07/16 10:00:05 [INFO] [example.com] The server validated our request
2026/07/16 10:00:07 [INFO] [example.com] acme: Validations succeeded; requesting certificates
2026/07/16 10:00:08 [INFO] [example.com] Server responded with a certificate.
使いどころ
- 80 番を開けられない(ポリシーやファイアウォールの制約)環境
- Traefik / Caddy などは TLS-ALPN-01 を組み込みでサポートしており、リバースプロキシが証明書取得まで面倒を見る構成で使われる
👉 ニッチに見えるが、「443 しか開いていないサーバーで、DNS API も触れない」ときの選択肢として覚えておくと効く。
更新の自動化:発行して終わりではない
Let's Encrypt の証明書は有効期限90日。意図的に短くして「自動更新しない運用」を許さない設計になっている。
更新のテスト
certbot renew は期限が30日を切った証明書だけを更新する。まずは dry-run で通ることを確認:
$ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/www.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for www.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/www.example.com/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
👉 更新時は発行時と同じ検証がもう一度走る。webroot なら 80 番が、DNS プラグインなら API 認証情報が、更新時にも有効である必要がある。
定期実行の確認
パッケージ版の certbot は systemd timer(または cron)を自動で仕込む。動いているか確認:
$ systemctl list-timers certbot.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Wed 2026-07-16 23:41:00 JST 6h left Wed 2026-07-16 11:14:32 JST 2h ago certbot.timer certbot.service
1日2回起動し、期限が近い証明書だけ更新する。
更新後の反映:deploy hook
❌ ありがちな事故:「renew は成功しているのに、サイトの証明書が古いまま」。
nginx は起動時に証明書ファイルを読み込む。ファイルが更新されても reload しない限り古い証明書を掴んだままだ。
deploy hook で更新成功時だけ reload させる:
$ sudo certbot renew --deploy-hook 'systemctl reload nginx'
一度 --deploy-hook 付きで実行すると renewal 設定に永続化され、以降の自動更新でも実行される:
# /etc/letsencrypt/renewal/www.example.com.conf(抜粋)
[renewalparams]
authenticator = webroot
webroot_path = /var/www/html
renew_hook = systemctl reload nginx
👉 /etc/letsencrypt/renewal-hooks/deploy/ にスクリプトを置く方法もある。全証明書共通のフックならこちら。
AWS ACM との比較:ACM が隠していたもの
ここまで読むと、ACM の DNS 検証で「この CNAME を DNS に追加してください」と言われる意味がわかる。
$ aws acm request-certificate --domain-name example.com --validation-method DNS
{
"CertificateArn": "arn:aws:acm:ap-northeast-1:123456789012:certificate/11111111-2222-3333-4444-555555555555"
}
$ aws acm describe-certificate \
--certificate-arn arn:aws:acm:ap-northeast-1:123456789012:certificate/11111111-2222-3333-4444-555555555555 \
--query 'Certificate.DomainValidationOptions[0].ResourceRecord'
{
"Name": "_3d1bf4e6a7c9e2f8b5a0d4c1e6f9a2b3.example.com.",
"Type": "CNAME",
"Value": "_9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c.acm-validations.aws."
}
「DNS にレコードを置いてドメインの支配権を証明する」——構造は DNS-01 とまったく同じだ。
違いは、ACM は検証レコードを CNAME で AWS 側に委譲させること。検証値の管理と再検証を AWS が握るので、レコードを置いたままにすれば更新も全自動になる。
👉 つまり ACM は「ACME 相当の検証と更新を、マネージドにして見えなくしたもの」。魔法ではない。
比較表
| AWS ACM | Let's Encrypt + certbot | |
|---|---|---|
| 検証方式 | DNS 検証(DNS-01 相当)/ メール検証(自動更新できず非推奨気味) | HTTP-01 / DNS-01 / TLS-ALPN-01 |
| 自動更新 | 完全自動(レコード設置のみ) | certbot renew + timer を自前で構成 |
| 秘密鍵 | 取り出せない(標準のパブリック証明書。AWS 内部で管理) |
/etc/letsencrypt/live/ に平文で保持 |
| 使える場所 | ALB / CloudFront / API Gateway など AWS マネージドサービスのみ | どこでも(EC2・オンプレ・VPS) |
| 有効期限 | 13ヶ月(自動更新) | 90日(自動更新前提) |
| 料金 | 無料(パブリック証明書) | 無料 |
👉 2025年からは ACM でもエクスポート可能なパブリック証明書(有料オプション)を発行でき、秘密鍵を取り出して EC2 やオンプレに入れることもできる。ただし標準の無料証明書は従来どおり取り出せない。
選び方
- ALB / CloudFront で TLS 終端する → ACM 一択。秘密鍵の管理からも解放される
- EC2 やオンプレのサーバー自身で TLS 終端する → ACM の証明書は持ち出せないので certbot(ACME)を使う
- 両方ある構成 → ALB は ACM、バックエンドや社内向けは certbot、の併用が普通
❌ 「ACM があるから certbot は不要」ではない。ACM の証明書は AWS のマネージドサービスにしかアタッチできない。EC2 の nginx に入れる証明書は、自分で取るか、有料のエクスポート可能証明書を使うしかない。
まとめ
- 証明書の発行には**ドメインの支配権の証明(DV)**が必要で、ACME はそれを自動化するプロトコル
-
HTTP-01:
/.well-known/acme-challenge/にファイルを置く。検証は必ず 80 番から。ワイルドカード不可 -
DNS-01:
_acme-challengeの TXT レコードで証明。ワイルドカードはこれ一択。運用は DNS プラグインで自動化 - TLS-ALPN-01:443 番の TLS ハンドシェイクだけで完結。certbot 非対応、lego や Traefik などで
- 発行して終わりではない。renew の定期実行 + deploy hook での reload までが証明書運用
- ACM の DNS 検証は DNS-01 と同じ構造。ACM は ACME 相当の作業をマネージドにして見えなくしたもの
- ACM の証明書は AWS マネージドサービス専用。サーバー自身で TLS 終端するなら ACME クライアントが必要