はじめに
ある日、開発環境のサイトをブラウザで開くと、こんな警告が出るようになりました。
サイトのプログラムは何も変えていません。調べてみると、原因は SSL/TLS 証明書の期限切れでした。
この記事では、
- そもそも証明書とは何か
- なぜ期限が切れてしまったのか
- 手動で証明書を更新する方法
- 二度と期限切れにならないよう、自動更新にする方法
を、初めての人にも分かるように、たとえ話を使いながら説明します。
この記事の環境
| 項目 | 内容 |
|---|---|
| サーバー | AWS EC2(Linux) |
| Web サーバー | Apache(httpd) |
| DNS | AWS Route53 |
| 証明書 | Let's Encrypt(certbot で取得) |
| ドメイン |
dev.example.com(記事用の例) |
💡
example.comは、説明や記事で例として使うために予約されているドメインです。記事中のdev.example.comは、自分のドメインに読み替えてください。
そもそも証明書って何?
まずは SSL/TLS と HTTPS の関係
証明書の話に入る前に、よく出てくる言葉を整理しておきます。
HTTP = Web ページをやり取りする約束事(中身が丸見え)
TLS = 通信を暗号化する約束事
HTTPS = HTTP + TLS(HTTP のやり取りを TLS で包んだもの)
| 用語 | たとえ |
|---|---|
| HTTP | お店とお客さんの会話の仕方(「これください」「はいどうぞ」) |
| TLS | 会話を外に聞こえなくする個室 |
| HTTPS | 個室の中で会話すること |
| 証明書 | 個室に入る前に見せる営業許可証 |
TLS は、個室に入る前に「このお店は本物か」を確かめるために証明書を使います。だから「SSL/TLS 証明書」と呼ばれます。
SSL と TLS の違い
同じものの旧名と新名です。
| 名前 | 状態 |
|---|---|
| SSL(Secure Sockets Layer) | 古いバージョン。弱点が見つかり、今はもう使われていない |
| TLS(Transport Layer Security) | SSL の後継。今使われているのは全部こちら |
実際に使われているのは TLS だけですが、SSL という名前が広まりすぎたので、今でも「SSL 証明書」と呼ばれることが多いみたいです。
「SSL/TLS」と並べて書くのは、「SSL と呼ばれがちだけど、実際は TLS のこと」という意味です。
お店と営業許可証で考える
証明書の仕組みは、お店と営業許可証にたとえると分かりやすいです。
| 技術用語 | たとえ |
|---|---|
| Web サイト | お店 |
| ブラウザ | お客さん |
| SSL/TLS 証明書 | お店に掲げる営業許可証 |
| Let's Encrypt(認証局) | 許可証を出してくれる役所 |
| 証明書の期限切れ | 許可証の期限が切れている |
お客さん(ブラウザ)はお店に入るとき、まず入口の許可証を確認します。
- 許可証がある → 「ちゃんとしたお店だ」と安心して入る
- 許可証の期限が切れている → 「このお店、大丈夫?」と警告を出す
今回ブラウザに出た警告は、まさにこの「期限切れの許可証を見て引き返した」状態です。お店の中身(サイトのプログラム)は何も壊れていません。
証明書の 2 つの役割
証明書には大きく 2 つの役割があります。
1. 通信を暗号化する
ブラウザとサーバーの間でやり取りするデータ(パスワード、Cookie など)を、途中で盗み見されないようにします。たとえるなら、お店とお客さんの会話が外に聞こえない個室を用意するようなものです。URL の https の「s」は、この暗号化を表しています。
2. 相手が本物だと証明する
「このサイトは、本当にそのドメインの持ち主が運営しています」と、第三者(認証局)が保証します。これがないと、偽物のサイトが本物になりすましても見分けがつきません。
Let's Encrypt とは
Let's Encrypt は、無料で証明書を発行してくれる認証局です。たとえで言うと「無料で許可証を出してくれる役所」です。
ただし、Let's Encrypt の証明書は有効期限が 90 日と短めです。期限が短いのは、万が一証明書が盗まれても、悪用できる期間を短くするためです。その代わり、自動で更新するのが前提になっています。
certbot とは
certbot は、Let's Encrypt とやり取りして証明書を取ってきてくれるツールです。たとえで言うと、役所に許可証を取りに行ってくれる店員です。
役所はどうやって「本当の持ち主」を確かめるの?
ここが今回いちばん大事なところです。
役所(Let's Encrypt)は、誰にでも許可証を出すわけにはいきません。他人のお店の許可証を勝手に取られたら困るからです。そこで、申請してきた人が本当にそのドメインの持ち主かを確かめます。
確かめ方はいくつかありますが、今回使ったのは DNS-01 という方法です。
役所「
dev.example.comの許可証が欲しいんですね。本当にあなたのお店か確かめます。この合言葉を、お店の看板に貼っておいてください。こちらで見に行って、貼ってあれば許可証を出します」
看板に合言葉を貼れるのは、お店の持ち主だけです。だから「貼れた=持ち主」と判断できる、という仕組みです。
| たとえ | 実際の仕組み |
|---|---|
| お店の看板 | DNS(今回は AWS Route53) |
| 合言葉 | ランダムな文字列 |
| 看板に合言葉を貼る | DNS に _acme-challenge.dev.example.com という名前の TXT レコードを登録する |
| 役所が看板を見に来る | Let's Encrypt が DNS に問い合わせて、TXT レコードを確認する |
DNS は、「このドメイン名のサーバーはどこにあるか」といった情報を世界中に公開している仕組みです。インターネットの看板のようなものだと思ってください。
TXT レコードは、DNS に自由な文字列を書いておける欄です(TXT は Text の略)。
_acme-challenge は自分で決めた名前?
いいえ、世界共通の決まった名前です。
_acme-challenge . dev.example.com
└──────┬──────┘ └──────┬──────┘
決まった名前 自分のドメイン
(変えられない) (証明書が欲しいドメイン)
Let's Encrypt は ACME(Automatic Certificate Management Environment)という「証明書の申請・本人確認・発行をプログラムで自動でやり取りするための約束事」で動いています。この ACME で、「DNS で本人確認するときは _acme-challenge.<ドメイン> の TXT に合言葉を置く」と決まっています。別の名前に貼っても、役所は見に来てくれません。
たとえるなら、「合言葉は看板の右下の決まった枠に貼ること」と役所が決めているようなものです。
先頭の _(アンダースコア)は、DNS で特別な用途のために予約された名前に付ける慣習です。www や dev のような普通のサーバー名とぶつからないようにしています。
なぜ DNS 認証(DNS-01)なの?
実は、もう 1 つ HTTP-01 という確かめ方もあります。看板ではなく、お店の中に合言葉の紙を置いて、役所の人に店まで見に来てもらう方法です。こちらなら DNS を触る権限は要りません。
ただ、今回の開発環境では使えませんでした。開発環境は、決められた IP アドレスからしかアクセスできないように制限していたからです(AWS のセキュリティグループで設定)。役所(Let's Encrypt)の確認用サーバーは IP アドレスを公開しておらず、毎回変わるので、お店に入れてもらえません。
| 方法 | 役所の確かめ方 | 今回使える? |
|---|---|---|
| HTTP-01 | お店(サーバー)まで来て、中に置いた合言葉を見る | ❌ お店に IP 制限があって入れない |
| DNS-01 | 看板(DNS)に貼った合言葉を見る | ✅ 看板は誰でも見られる |
全体の構造
登場人物と、それぞれがどこにいるかを図にするとこうなります。

自動化とは、③(合言葉を貼る)と、①〜⑥を定期的に回すことを、人間ではなく certbot にやらせることです。
証明書はどこにあるの?
よく誤解されますが(私もそうでした)、証明書そのものは DNS(Route53)には置きません。
| 場所 | 置いてあるもの | 役割 |
|---|---|---|
| サーバー(EC2)の中 | 証明書と秘密鍵 | Apache がこれを読み込んでブラウザに見せる |
| DNS(Route53) | TXT レコード(合言葉) | 証明書を発行してもらうときの本人確認にだけ使う |
何が起きていたのか
1. まず証明書の期限を確認する
本当に期限切れなのかを、手元のターミナルから確認します。
準備: ドメインを変数に入れておく
この記事のコマンドでは、ドメインを毎回書く代わりに DOMAIN という変数を使います。最初に 1 回だけ、自分のドメインを入れてください。
DOMAIN=dev.example.com
| 部分 | 意味 |
|---|---|
DOMAIN=... |
DOMAIN という名前の入れ物に、ドメインを入れる(= の前後にスペースを入れない) |
"$DOMAIN" |
コマンドの中でこう書くと、入れておいたドメインに置き換わる |
こうしておけば、あとのコマンドは書き換えずにコピーするだけで動きます。
⚠️ 変数は、そのターミナルを閉じたり、SSH でサーバーに入ったりすると消えます。サーバーに入ったあとも、もう一度
DOMAIN=...を実行してください。
期限を確認する
echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 2>/dev/null | openssl x509 -noout -dates
ちょっと長いので、分解して説明します。
| 部分 | 意味 |
|---|---|
openssl s_client -connect "$DOMAIN":443 |
ブラウザと同じように HTTPS(443 番ポート)でサイトに接続し、サーバーが出してくる証明書を受け取る |
-servername "$DOMAIN" |
「このドメインの証明書をください」と伝える(1 台のサーバーが複数のサイトを持っていることがあるため) |
echo | |
接続したらすぐ終わらせるためのもの(何も送らずに切る) |
2>/dev/null |
余計なメッセージを表示しない |
openssl x509 -noout -dates |
受け取った証明書から、有効期限だけを表示する |
結果はこうでした。
notBefore=Aug 10 01:59:14 2026 GMT
notAfter=Nov 8 01:59:13 2026 GMT
notAfter(この日時より後は無効)が、もう過ぎていました。期限切れが原因で確定です。
2. サーバーに入る
証明書はサーバーの中にあるので、SSH でサーバーに入って作業します。
ssh -i ~/.ssh/my-key.pem ec2-user@203.0.113.10
| 部分 | 意味 |
|---|---|
ssh |
離れた場所にあるサーバーに、自分のターミナルからログインするコマンド |
-i ~/.ssh/my-key.pem |
ログイン用の鍵ファイル。パスワードの代わりになる(自分の鍵ファイルに読み替える) |
ec2-user@203.0.113.10 |
「このユーザー名」で「このサーバー」に入る(203.0.113.10 は例として予約されている IP アドレス。自分の EC2 のパブリック IP アドレスに読み替える) |
たとえるなら、203.0.113.10 はお店の住所、ec2-user はお店の裏口から入るスタッフの名前、鍵ファイルはスタッフ用の合鍵です。住所が分かっても、合鍵を持っているスタッフしか中には入れません。
入ったら、管理者(root)に切り替えます。証明書のファイルや Apache の操作には管理者の権限が必要だからです。そのあと、ドメインの変数を入れ直します。
sudo su
DOMAIN=dev.example.com
3. 普通に更新しようとしたら、失敗した
certbot には、期限が近い証明書をまとめて更新する renew という命令があります。まずはこれを試しました。
certbot renew && systemctl restart httpd
| 部分 | 意味 |
|---|---|
certbot renew |
期限が近い証明書を更新する |
&& |
前のコマンドが成功したときだけ、次のコマンドを実行する |
systemctl restart httpd |
Web サーバー(Apache)を再起動する |
ところが、失敗しました。
Failed to renew certificate dev.example.com with error: The manual plugin is not working; there may be problems with your existing configuration.
The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.')
ポイントは manual plugin という部分です。
この証明書は、最初に取ったときに**手動方式(manual)**で取られていました。手動方式とは、「看板に合言葉を貼るのは人間が手でやります」という約束です。
一方、certbot renew は人間がいなくても動く前提の命令です。店員(certbot)は「合言葉を看板に貼るのは人間の担当なのに、人間がいない。自分では貼れない」と言って止まってしまいました。
これが、今回期限切れになった根本原因です。 手動方式で取った証明書は、自動では更新されません。90 日ごとに人間が同じ作業をする必要があり、それを忘れていたため期限が切れました。
まずは手動で直す
自動化の前に、まずはサイトを直します。手動方式で証明書を取り直します。
4. 証明書の取り直しを始める
certbot certonly --preferred-challenges dns-01 --authenticator manual --domain "$DOMAIN"
| 部分 | 意味 |
|---|---|
certonly |
証明書を取るだけにする(Apache の設定ファイルは書き換えない) |
--preferred-challenges dns-01 |
本人確認を DNS(看板)に合言葉を貼る方式でやる |
--authenticator manual |
合言葉を貼るのは人間が手でやる |
--domain "$DOMAIN" |
どのドメインの証明書が欲しいか |
実行すると、certbot が合言葉を教えてくれて、待ち状態になります。
Please deploy a DNS TXT record under the name:
_acme-challenge.dev.example.com.
with the following value:
D7AbIgOQ6QYi2Fxp4oY5ReeCifAtEgs_xxxxxxxxxxx
Before continuing, verify the TXT record has been deployed.
...
Press Enter to Continue
⚠️ ここではまだ Enter を押さないでください。 看板に合言葉を貼る前に Enter を押すと、役所が見に来たときに合言葉が無く、失敗します。
5. 看板(Route53)に合言葉を貼る
合言葉は申請のたびに新しく作られます。前回の合言葉が残っていても使えないので、手動方式では更新のたびに毎回この作業が必要です(これが、手動方式だと自動更新できない理由です)。
AWS のコンソールで Route53 を開き、該当するドメインのホストゾーンに TXT レコードを登録します。
| 項目 | 入力する値 |
|---|---|
| レコード名 | _acme-challenge.dev.example.com |
| レコードタイプ | TXT |
| 値 |
"D7AbIgOQ6QYi2Fxp4oY5ReeCifAtEgs_xxxxxxxxxxx"(ダブルクォートで囲む) |
| TTL | 60(秒) |
前回の更新で使った古い合言葉が残っていたら、上書きして構いません。合言葉は 1 回使えば役目が終わるからです。
TTL は「この情報を何秒間覚えておいてよいか」という設定です。合言葉はすぐ使い終わるので、短めの 60 秒にしておくと、古い値がいつまでも残りません。
6. 合言葉がちゃんと見えるか確かめる
DNS に登録しても、世界中から見えるようになるまで少し時間がかかることがあります。焦って Enter を押さず、dig コマンドで確認します。
dig +short TXT "_acme-challenge.$DOMAIN"
| 部分 | 意味 |
|---|---|
dig |
DNS(看板)に書いてある内容を問い合わせるコマンド |
+short |
結果だけを短く表示する |
TXT "_acme-challenge.$DOMAIN" |
その名前の TXT レコードを見せて、という指定 |
さっき登録した合言葉が表示されれば OK です。
"D7AbIgOQ6QYi2Fxp4oY5ReeCifAtEgs_xxxxxxxxxxx"
もっと確実に確かめたい場合
実は看板は 1 枚ではなく、**世界に複数のコピー(ネームサーバー)**があります。Route53 の場合は 4 つです。役所がどのコピーを見に来ても合言葉が見えるように、全部で確認しておくと安心です。
for ns in $(dig +short NS "$DOMAIN"); do
echo "$ns: $(dig +short TXT "_acme-challenge.$DOMAIN" @"$ns")"
done
| 部分 | 意味 |
|---|---|
dig +short NS "$DOMAIN" |
このドメインの看板(ネームサーバー)の一覧を取る |
for ns in ...; do ...; done |
一覧の 1 つずつについて、中の処理を繰り返す |
@$ns |
「このネームサーバーに聞いて」と指定する |
4 つすべてで新しい合言葉が出れば、準備完了です。
7. Enter を押す → 証明書が発行される
certbot の画面に戻って Enter を押すと、役所(Let's Encrypt)が看板を見に来て、合言葉を確認し、新しい証明書を発行してくれます。
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/dev.example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/dev.example.com/privkey.pem
This certificate expires on 2027-02-07.
| ファイル | たとえ | 説明 |
|---|---|---|
fullchain.pem |
許可証 | 証明書本体。ブラウザに見せるもの |
privkey.pem |
お店だけが持つ印鑑 | 秘密鍵。許可証が本物だと示すために使う。絶対に外に出してはいけない |
8. Apache を再起動する
ここで終わりではありません。
Apache は、起動したときに証明書を読み込み、そのあとは覚えたものを使い続けます。新しい証明書をファイルとして置いただけでは、お店の壁に掛かっているのは古い許可証のままです。
再起動して、新しい許可証に掛け替えます。
systemctl restart httpd
9. 直ったか確かめる
最初と同じコマンドで、期限を確認します。
echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 2>/dev/null | openssl x509 -noout -dates
notBefore=Nov 9 04:16:57 2026 GMT
notAfter=Feb 7 04:16:56 2027 GMT
期限が延びています。ブラウザで開き直すと、警告も消えました。🎉
自動更新にする
方針: 店員に「看板の鍵」を渡す
今までは「看板に合言葉を貼るのは人間の担当」でした。これを、店員(certbot)が自分で看板に合言葉を貼れるようにします。
| やること | たとえ |
|---|---|
| ① certbot に Route53 プラグインを入れる | 店員に「看板に合言葉を貼る方法」を教える |
| ② サーバーに Route53 を書き換える権限を渡す | 店員に「看板だけ開けられる鍵」を渡す |
| ③ 証明書を Route53 方式で取り直す | 「合言葉は店員が貼る」と約束を変える |
④ 毎日 certbot renew を動かす |
店員に「毎日、許可証の期限を見て」と頼む |
① Route53 プラグインを入れる
certbot には、Route53 に自動で合言葉を貼ってくれる certbot-dns-route53 というプラグインがあります。まず、インストールできるか確認します。
dnf list available 'python3-certbot*'
python3-certbot-apache.noarch 2.6.0-4.amzn2023.0.1 amazonlinux
python3-certbot-dns-rfc2136.noarch 2.6.0-4.amzn2023.0.1 amazonlinux
python3-certbot-dns-route53.noarch 2.6.0-4.amzn2023.0.1 amazonlinux
python3-certbot-nginx.noarch 2.6.0-4.amzn2023.0.1 amazonlinux
python3-certbot-dns-route53 があるので、これを入れます。
dnf install -y python3-certbot-dns-route53 && certbot plugins | grep route53
* dns-route53
dns-route53 と出れば OK です。
② サーバーに権限を渡す(IAM ロール)
certbot が Route53 を書き換えるには、AWS の権限が必要です。EC2 に付いている IAM ロールに権限を足します。サーバーにアクセスキーを置かずに済むので安全です。
ここで大事なのが最小権限です。看板の鍵を渡すといっても、お店の金庫の鍵まで渡す必要はありません。「_acme-challenge の TXT レコードだけ書き換えられる」鍵にします。
AWS コンソールで「IAM → ロール → EC2 に付いているロール → 許可を追加 → インラインポリシーを作成」と進み、次の JSON を貼り付けます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["route53:ListHostedZones", "route53:GetChange"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "route53:ChangeResourceRecordSets",
"Resource": "arn:aws:route53:::hostedzone/<ホストゾーンID>",
"Condition": {
"ForAllValues:StringEquals": {
"route53:ChangeResourceRecordSetsNormalizedRecordNames": ["_acme-challenge.dev.example.com"],
"route53:ChangeResourceRecordSetsRecordTypes": ["TXT"]
}
}
}
]
}
| 権限 | 何に使うか |
|---|---|
route53:ListHostedZones |
どの看板(ホストゾーン)に貼ればいいかを探す(見るだけ) |
route53:GetChange |
貼った合言葉が反映されたかを確認する(見るだけ) |
route53:ChangeResourceRecordSets |
合言葉を貼る・はがす。Condition で _acme-challenge の TXT だけに制限 |
③ Route53 方式で取り直す
いきなり切り替える前に、--dry-run でリハーサルします。本物の証明書は変わりません。
certbot certonly --dns-route53 -d "$DOMAIN" --dry-run
Simulating renewal of an existing certificate for dev.example.com
The dry run was successful.
リハーサルの最中に Route53 を見ると、_acme-challenge の TXT にcertbot が自分で合言葉を貼っているのが確認できます。
成功したら、実際に取り直します。
certbot certonly --dns-route53 -d "$DOMAIN" --force-renewal --deploy-hook "systemctl reload httpd"
| 部分 | 意味 |
|---|---|
--dns-route53 |
合言葉を貼るのは Route53 プラグイン(店員)がやる |
--force-renewal |
期限前でも取り直す(手動方式の設定を Route53 方式に切り替えるため) |
--deploy-hook "systemctl reload httpd" |
証明書が新しくなったら、Apache に読み込み直させる |
今回は Enter を押す場面がありません。reload は restart と違い、サーバーを止めずに証明書だけ読み込み直します。
切り替わったかは、certbot の設定ファイルで確認できます。
grep authenticator "/etc/letsencrypt/renewal/$DOMAIN.conf"
authenticator = dns-route53
manual から dns-route53 に変わっていれば成功です。
💡 終わったあとに Route53 を見ると、
_acme-challengeの TXT レコードが消えています。certbot は確認が終わると、自分で合言葉をはがしてくれます。消えていて正常です。
④ 毎日 certbot renew を動かす(cron)
発行時に certbot は「自動更新の予定を設定しました」と表示しましたが、確認すると何も設定されていませんでした。
systemctl list-timers --all | grep -i certbot # 何も出ない
ls /etc/cron.d/ # certbot 用のファイルが無い
表示をうのみにせず、自分で cron を設定します。cron は「決まった時刻にコマンドを動かす」仕組みです。
echo "0 3 * * * root $(command -v certbot) renew --quiet" > /etc/cron.d/certbot-renew
| 部分 | 意味 |
|---|---|
0 3 * * * |
毎日 3 時 0 分に実行 |
root |
管理者として実行 |
$(command -v certbot) |
certbot の置き場所(/usr/bin/certbot)に置き換わる |
renew --quiet |
期限が近い証明書を更新する。何も無いときは何も出力しない |
certbot renew は期限まで 30 日を切った証明書だけを更新するので、毎日動いても無駄に更新されることはありません。90 日のうち最後の 30 日間は毎日チャンスがあるので、1 日 1 回で十分です。
Apache の reload は、③の --deploy-hook が設定ファイルに保存されているので、ここで書かなくても自動で実行されます。
本当に自動で更新できるか、リハーサルする
最後に、cron が実行するのと同じ certbot renew をリハーサルします。
certbot renew --dry-run
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/dev.example.com/fullchain.pem (success)
success と出れば完了です。これで、人間が何もしなくても証明書が更新されます。🎉
まとめ
- ブラウザの「保護されていない通信」は、証明書の期限切れが原因だった
- 証明書はお店の営業許可証のようなもの。通信の暗号化と、サイトが本物であることの証明をしている
- Let's Encrypt は、DNS に合言葉(TXT レコード)を貼らせることで、ドメインの持ち主かを確かめる
- 証明書が**手動方式(manual)**で取られていたため、
certbot renewでは自動更新できず、期限が切れた - 手動で直すときは、合言葉が DNS に反映されたのを
digで確かめてから Enter を押す - 更新したら **Apache の再起動(または reload)**を忘れない
- 自動化するには、certbot に Route53 プラグインと最小権限の IAM ロールを渡し、cron で毎日
certbot renewを動かす - certbot の「自動更新を設定しました」という表示をうのみにせず、実際に設定されているか確認する
証明書の期限切れは、気づいたときにはサイトが見られなくなっている、地味に怖いトラブルです。手動の運用が残っていないか、一度確認してみてください。

