エラーの内容
certbot renew を実行すると、以下のエラーで失敗します:
open() "/var/www/html/.well-known/acme-challenge/xxxxxxxxxxxxxxxx" failed (2: No such file or directory)
Let's Encryptは既知のURLにトークンファイルを配置し、HTTPポート80でそのファイルを取得してドメインの所有権を確認します。Nginxが404を返すと、検証が失敗し、証明書が更新されません。
原因
よく見られる順に、3つの原因があります:
- HTTP→HTTPSリダイレクトがチャレンジリクエストをブロックする — ACMEサーバーは常にポート80にアクセスします。Nginxがポート80のトラフィックをすべてHTTPSにリダイレクトしている場合、チャレンジファイルが確認される前に302レスポンスが返されます。これが圧倒的に多いケースです。
- webrootパスの不一致 — Certbotは特定のディレクトリにトークンを書き込みますが、Nginxの
rootディレクティブが別の場所を指しています。トークンはディスクに存在しますが、Nginxが見つけられません。 - ディレクトリが存在しない —
.well-known/acme-challenge/ディレクトリ自体がディスクに存在しません。
修正手順
ステップ1:CertbotのターゲットwebrootパスPer確認
cat /etc/letsencrypt/renewal/yourdomain.com.conf
webroot_path の行を確認します。Nginxが実際にファイルを配信しているディレクトリと一致している必要があります。不一致の場合は、このファイルのパスを修正するか、Nginxのrootディレクティブを更新してください。
ステップ2:チャレンジディレクトリが存在しない場合は作成する
mkdir -p /var/www/html/.well-known/acme-challenge
chmod 755 /var/www/html/.well-known/acme-challenge
ステップ3:リダイレクトより前にlocationブロックを追加する
ポート80のNginxサーバーブロックを開き、return 301 や rewrite より上にチャレンジ用のlocationブロックを追加します:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# ACMEチャレンジ — HTTPSリダイレクトより前に記述する
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
try_files $uri =404;
}
# それ以外はHTTPSへ
return 301 https://$host$request_uri;
}
^~ 修飾子により、このブロックがマッチした時点でNginxの正規表現処理が停止します。チャレンジURLに対してリダイレクトが発火することはありません。
ステップ4:設定をテストしてリロードする
nginx -t && systemctl reload nginx
ステップ5:ドライランで確認する
certbot renew --dry-run
恒久的な対策:全ドメイン共通スニペット
複数のドメインを管理している場合は、再利用可能なスニペットを作成することで、すべてのサーバーブロックにこの設定を漏れなく適用できます:
# 作成先: /etc/nginx/snippets/acme-challenge.conf
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
default_type "text/plain";
try_files $uri =404;
}
各HTTPサーバーブロックでこのスニペットをインクルードします:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
include snippets/acme-challenge.conf;
return 301 https://$host$request_uri;
}
include を1行追加するだけで、新しいドメインにも自動的に正しいチャレンジ設定が引き継がれます。
エッジケース:webrootが異なる複数ドメインの更新
別々のディレクトリに配置されたドメインをまとめた証明書を更新する場合は、更新設定ファイルの [[webroot_map]] セクションを確認してください:
# /etc/letsencrypt/renewal/yourdomain.com.conf
[renewalparams]
authenticator = webroot
webroot_path = /var/www/html,
[[webroot_map]]
yourdomain.com = /var/www/html
www.yourdomain.com = /var/www/html
api.yourdomain.com = /var/www/api
各ドメインはNginxが配信するディレクトリにマッピングされます。パスが1つでも誤っていると、そのドメインだけでなく更新全体が失敗します。
動作確認
推測に頼らず、テストファイルを配置してHTTP経由で直接取得して確認しましょう:
# テストトークンを配置する
echo "ok" > /var/www/html/.well-known/acme-challenge/healthcheck
# HTTP経由で取得 — リダイレクトではなく200が返ることを確認する
curl -v http://yourdomain.com/.well-known/acme-challenge/healthcheck
本文 ok を含む HTTP/1.1 200 OK が返ることを確認します。まだ404やリダイレクトが返る場合は以下を実行してください:
# 有効な設定にlocationブロックが含まれているか確認する
nginx -T | grep -A5 'well-known'
# ポート80が使用しているrootパスを確認する
nginx -T | grep root
curlで200が返ったら、ドライランを実行します:
certbot renew --dry-run
ドライランが成功すれば、certbot.timer によるスケジュール更新も正常に動作します。確認後はテストファイルを削除してください:
rm /var/www/html/.well-known/acme-challenge/healthcheck

