SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

Ubuntu 24.04でCertbotをインストールしてNginxをHTTPS化する方法

Ubuntu 24.04でCertbotをインストールする手順を解説します。aptとsnapのどちらを選ぶべきか、またLet's Encryptの証明書発行で失敗しやすい80番ポートの接続制限や更新時の注意点を詳しく説明します。

Certbot のインストール: apt または snap

Ubuntu 24.04 では、sudo apt install certbot python3-certbot-nginx により、実際に発行できる公開信頼済みの Let's Encrypt 証明書を扱う Certbot を利用できます。Certbot の上流ドキュメントでは snap が案内されています。違いは限定的です。snap は上流のリリースを追跡します。アーカイブパッケージは LTS に同梱されたバージョンを基にし、セキュリティ修正を受け取ります。

どちらか一方を選択してください。Certbot を 2 つインストールすると、同じ /etc/letsencrypt ツリーを対象とする更新タイマーも 2 つになります。存在を忘れていた方が問題を引き起こします。

apt を使用する場合:

sudo apt update
sudo apt install certbot python3-certbot-nginx

これにより、/usr/bin/certbot、nginx プラグイン、certbot.service と certbot.timer の組み合わせ、および systemd では何も実行しない /etc/cron.d/certbot エントリがインストールされます。

snap を使用する場合:

sudo apt remove certbot python3-certbot-nginx
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

snap には独自の snap.certbot.renew.timer タイマーが含まれています。snap をインストールする前に、apt パッケージを削除してください。

その後の動作は、更新を実行するタイミングの違い(後述の Renewal で説明)を除き、どちらのインストール方法でも同じです。Certbot 2.x はデフォルトで ECDSA(P-256)鍵を使用します。ECDSA を利用できないクライアントに対してのみ --key-type rsa を指定してください。すべての状態は /etc/letsencrypt 以下に保存されます。archive/ には実際の鍵と証明書ファイルがあり、live/ は現在のファイルを指し、renewal/ には証明書ごとに 1 つの設定ファイルがあり、accounts/ には ACME アカウント鍵があります。

HTTP-01 の実際の動作と、port 80 が必須である理由

HTTP-01 challenge はコールバックです。example.com を対象とする証明書を Let's Encrypt に要求すると、Let's Encrypt は公開 DNS で名前を解決し、見つかったアドレスの port 80 に接続して http://example.com/.well-known/acme-challenge/<token> を要求します。サーバーは、Certbot が直前にディスクへ書き込んだトークンの内容をそのまま返します。仕組みはこれだけです。ここから 3 つの点が導かれます。発行に失敗する原因の大半は、これらで説明できます。

  • port 80 は、ノート PC からだけでなく、インターネットから到達可能でなければなりません。 ufw ルール、クラウドプロバイダーのセキュリティグループ、VPS コンソールのファイアウォールが 443 だけを許可していると、発行だけでなく、その後の更新もすべて失敗します。
  • DNS は事前にこのサーバーを指していなければなりません。 検証サーバーは外部から独自に名前を解決します。手元の /etc/hosts エントリやブラウザーキャッシュは影響しません。
  • AAAA レコードを公開している場合、IPv6 が先に試行されます。 IPv6 接続がタイムアウトした場合に限り、Let's Encrypt は IPv4 で再試行します。IPv6 接続が拒否された場合は再試行されません。また、接続を受け付けて別の内容を返すホストを指す古い AAAA レコードがあると、検証は明確に失敗します。

リダイレクトは使用できます。検証は HTTP リダイレクトに従って HTTPS へ進み、接続先の証明書が存在しない、期限切れである、または自己署名であることは問題にしません。ただし、port 80 以外から開始することはありません。Certbot には TLS-ALPN-01 の実装がないため、「443 だけを使う」という回避策は利用できません。

認証方式の選択: --nginx, --webroot, --standalone

nginx が既に稼働しており、対象ドメインを配信している場合は --nginx が標準的な選択肢です。Certbot が設定ファイルを解析し、一時的なチャレンジ用ロケーションを挿入して nginx をリロードし、検証後に TLS ディレクティブをサーバーブロックへ書き込みます。ダウンタイムは発生しません。

sudo certbot --nginx -d example.com -d www.example.com

新規サーバー構築時のスクリプト例:

sudo certbot --nginx \
  -d example.com -d www.example.com \
  --agree-tos -m ops@example.com --no-eff-email \
  --redirect --non-interactive

Certbot に nginx の設定ファイルを操作させたくない場合は --webroot が適しています。テンプレートから生成した設定を git で管理している場合や、Ansible で配布している場合に有効です。Certbot はチャレンジ用ファイルのみを、既に配信中のディレクトリへ書き込みます。

sudo certbot certonly --webroot -w /var/www/example.com \
  -d example.com -d www.example.com \
  --deploy-hook "systemctl reload nginx"

ポート 80 で何も待ち受けていない場合(メールサーバー、443 番のみを扱う API、nginx インストール前の初回起動スクリプトなど)は --standalone が適しています。Certbot が数秒間だけ自身でポート 80 をバインドします。nginx が稼働していると失敗するため、実行前後で停止させる必要があります:

sudo certbot certonly --standalone -d mail.example.com \
  --pre-hook "systemctl stop nginx" \
  --post-hook "systemctl start nginx"

これらのフックは証明書の更新設定に記録されるため、更新時にも同様の停止・起動処理が自動的に行われます。

証明書の有無にかかわらず動作する server block

いわゆる鶏と卵の問題です。存在しないファイルを指す ssl_certificate があると nginx は起動を拒否し、nginx が停止していると Certbot は検証できません。まずポート 80 でサイトを起動します。

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com;
    index index.html;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
        try_files $uri =404;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

sudo nginx -t && sudo systemctl reload nginx を実行し、curl -I http://example.com/ がサーバーの 外部 から応答することを確認してから、証明書を発行します。その後、次を実行します。

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

より長い ACME プレフィックスがすでに location / より優先されるため、return 301 がチャレンジリクエストを受け取ることはありません。^~ 修飾子が有効なのは別の場面です。このプレフィックスが一致すると、nginx は正規表現の location を完全にスキップします。そのため、後から追加した location ~ /\. { deny all; } などのルールによってチャレンジがブロックされることはありません。この location をポート 80 に残しておけば、サイトの他の部分を HTTPS 専用にした後も更新が機能します。

上記の両方のブロックはディスク上のファイルを提供します。nginx でアプリケーションを前段プロキシする場合は、location / が proxy_pass ブロックになります。リバースプロキシの server block を1行ずつ確認する手順では、そのアプリケーションに必要なヘッダーを説明しています。ACME location と TLS ディレクティブはそのままです。

HTTP/2 の構文は nginx のバージョンによって異なります。Ubuntu 24.04 には nginx 1.24 が含まれており、インライン形式の listen 443 ssl http2; を要求します。別個の http2 on; ディレクティブを指定すると起動に失敗します。Debian 13 には nginx 1.26 が含まれており、http2 on; を要求します。インライン形式については非推奨であるという警告だけを表示します。最初に nginx -v を確認してください。

nginx の参照先は live/ にし、archive/ には向けないでください。live/ シンボリックリンクは更新のたびに参照先が変更されます。archive/ 内の固定パスを使うと、期限切れになる証明書に固定されます。

ワイルドカード証明書には DNS-01 が必須であり、DNS-01 にはプラグインが必要です

ワイルドカード証明書(*.example.com)は HTTP-01 では検証できません。ファイルを取得するための単一のホスト名が存在しないためです。DNS-01 が唯一の手段であり、_acme-challenge.example.com TXT レコードを公開することでドメインの管理権限を証明します。Certbot がこれを自動で行うには DNS プロバイダーの API 認証情報が必要であり、そのためにプロバイダープラグインが存在します。ワイルドカード証明書の完全ガイドでは、TXT レコードの仕組みと手動モードにおける更新の落とし穴を解説しています。以下は Cloudflare を使用した簡潔な手順です。

sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare

apt を使用する場合、パスは sudo apt install python3-certbot-dns-cloudflare になります。認証情報は root ユーザーのみが読み取れるファイルに保存します。

# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_here

トークンの権限は、対象ゾーンに対する DNS 編集権限のみに限定してください。これは DNS の鍵となるため、慎重に扱ってください。

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

シェルによるワイルドカードの展開を防ぐため、ワイルドカード部分は引用符で囲んでください。DNS-01 は HTTP-01 では不可能なケースも解決します。例えば、公開ポート 80 を持たないホスト、内部サービス、VPS 上のセルフホスト WireGuard VPN 経由でのみ到達可能なボックス、プライベートインターフェース上の管理パネルなどが対象です。

更新: 90 日、timer、deploy hook

Let's Encrypt の証明書の有効期間は 90 日です。apt パッケージ版(Certbot 2.9.0)は、残り 30 日未満になると更新します。snap は最新の Certbot を追従します。Certbot はバージョン 4.1 以降、ACME Renewal Info(ARI)を通じて Let's Encrypt に推奨更新時期も問い合わせるため、より早く更新する場合があります。Let's Encrypt の証明書では通常、有効期間の約 3 分の 2 が経過した時点です。いずれの場合も、更新に失敗しても障害になる前に修正できる、およそ 30 日間の余裕があります。来年以降は更新頻度が上がる見込みです。Let's Encrypt のデフォルトプロファイルは、2027 年 2 月 10 日に 64 日証明書へ、2028 年 2 月 16 日に 45 日証明書へ移行します。Let's Encrypt は有効期限の警告メールを送信しなくなりました。誰かが知らせてくれることもないため、監視は自分で行う必要があります。

インストール時に提供された timer を確認します。

systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificates

certbot renew は /etc/letsencrypt/renewal/ 内のすべての設定を順に確認し、まだ更新時期ではないものをスキップして、残りを各設定に保存されたオプションで更新します。これらは、certbot reconfigure で後から変更していない限り、最初の実行時に指定したフラグです。最初の実行が重要なのは、その内容が記録されるためです。

ディスク上の証明書ファイルを更新しても、それだけでは何も変わりません。nginx は、reload を指示されるまでメモリ上の古い証明書を提供し続けます。deploy hook を 1 回設定します。

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

renewal-hooks/deploy/ 内にある実行可能ファイルは、更新が成功するたびに実行されます。--deploy-hook フラグを使うと、1 つの証明書に対して同じ処理を行い、renew_hook = ... をその更新設定に保存します。certbot --nginx は自動的に reload しますが、--webroot と --standalone の構成では自動的に reload しません。サイトが期限切れの証明書を提供しているのに、certbot certificates が新しい証明書を正常に報告するのは、hook がないことが原因です。起動時に証明書を読み込むその他のソフトウェアにも同じ hook が必要です。Docker、TLS、バックアップを使用する Nextcloud VPS のインストール のようなコンテナ化されたアプリケーションでは、独自の restart または reload 手順もここに設定します。

実際の更新をテストする

sudo certbot renew --dry-run

これは Let's Encrypt のステージング環境に対して完全なチャレンジを実行します。実行経路、ファイアウォール、DNS は本番と同じです。本番環境のレート制限には影響しません。ステージング環境には、より緩やかな独自の制限があります。証明書もディスクには保存されません。今日このテストに成功すれば、サーバー側の条件が変わらない限り、60 日後の自動更新にも成功します。

dry run だけでは、reload hook が実行されることまでは確認できません。この動作は Certbot のバージョンによって異なります。その部分は手動でテストします。hook script を直接実行し、systemctl reload nginx が成功することを確認して、sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf を確認します。

実際に遭遇するエラー

Could not bind to IPv4 or IPv6., nginx がすでにポート 80 を使用しているため、--standalone は失敗します。--nginx または --webroot を使用するか、実行中だけ nginx を停止してください。sudo ss -lntp | grep ':80' でポートの占有者を確認します。

Timeout during connect (likely firewall problem), Let's Encrypt がポート 80 に到達できません。外側から順に確認してください。sudo ufw status (sudo ufw allow 'Nginx Full' で開きます)、次に VPS プロバイダーのファイアウォール、最後に DNS です。サーバー外の場所から curl -sSv http://example.com/.well-known/acme-challenge/test でテストしてください。古い AAAA レコードも同じメッセージを生成します。

unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404, ポート 80 には到達可能ですが、トークンが提供されていません。リクエストが別のサーバーブロックに到達しているか (default_server を所有しているブロックを確認してください)、-w に渡されたディレクトリが nginx の公開ディレクトリと一致していません。/var/www/example.com/.well-known/acme-challenge/test にファイルを配置して外部から取得を試みてください。そこで 404 が返る場合、問題は証明書ではありません。

DNS problem: NXDOMAIN looking up A for example.com, ドメイン名がパブリックに解決されません。伝播が完了していない新しいレコードか、レジストラが管理していないゾーンのレコードである可能性があります。

too many certificates (5) already issued ..., レート制限です。デバッグを繰り返すと遭遇します。メッセージには制限の種類と retry after の時間が記載されます。Let's Encrypt は、完全に同一のドメイン名のセットに対する証明書発行を週 5 回までに制限しており、この制限を解除する方法はありません。時間が経過するのを待つ必要があります。これとは別に、登録済みドメインごとに週 50 回の新規証明書発行が許可されています。この制限は申請により引き上げ可能です。既存の証明書と全く同じドメイン名のセットに対するリクエストは更新とみなされ、この制限の対象外となります。また、ARI を介した更新はすべての制限から除外されます。デバッグには --dry-run を使用してステージング環境に対して行ってください。

nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory, nginx が発行されていない証明書、または certbot delete で削除された証明書を参照するように設定されています。TLS サーバーブロックをコメントアウトし、nginx を起動して証明書を発行してから、ブロックを元に戻してください。

open() "/etc/letsencrypt/options-ssl-nginx.conf" failed, そのファイルは nginx プラグインパッケージに含まれています。python3-certbot-nginx がない certonly 環境では、プラグインを追加するか、include 行を独自の ssl_protocols および ssl_ciphers 設定に置き換えてください。

大規模運用で管理する

1 枚の証明書には最大 100 個の名前を含められます。単一の certbot --nginx -d a.example.com -d b.example.com ... は魅力的ですが、古い DNS レコードが 1 つでも検証に失敗すると、その証明書に含まれる他の名前もすべて影響を受けます。サイトごとに証明書を分ければ、それぞれが独立して失敗します。複数のサービスをホストするサーバーでは、この構成が適しています。サイト数が数個を超えたら、ACME 対応のフロントエンドを導入する価値があります。Docker Compose で複数のアプリを実行する Traefik リバースプロキシは、証明書の取得と更新を自動で行うため、Certbot は完全に不要になります。フロントエンドにどのプロキシを配置するかは別途判断が必要です。Nginx、Caddy、Traefik を比較する際の主な判断基準は、証明書管理とアプリごとの設定をどの程度プロキシに任せたいかです。

/etc/letsencrypt 全体を、sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt とシンボリックリンクを保持したままバックアップします。そのツリーには accounts/、つまり再生成できない ACME アカウントキーが含まれています。新しい VPS へ移行する場合は、-a を使ってツリーを rsync し、Certbot をインストールし、DNS の向き先を変更してから、切り替え前に certbot renew --dry-run を実行します。

サーバーを再構築したり、新しい LTS に移行したりしても、更新タイマーは自動的には引き継がれません。移行、スナップショットの復元、ディストリビューションのアップグレードの後は、systemctl list-timers 'certbot*' と --dry-run を 1 回実行します。これを省くと、更新されていると思われていた証明書が 89 日後の午前 3 時に期限切れとなり、サイトが停止することがあります。

上記の HTTP-01 のルートは、管理下にあるマシンを前提としています。つまり、パブリック IP アドレスを持ち、インターネット全体から port 80 にアクセスできる VPS などです。DNS-01 にはそのどちらも必要ありません。上記の手順は、いずれの環境でも同じです。

同じ証明書の手順をnginx ではなく Apache で実施する場合にも適用できます。公開証明書を使えない場合は、Ubuntu で自己署名証明書を作成する方法で内部サービスに対応できます。

FAQ

HTTPS しか提供しないサイトでも、port 80 を開く必要がありますか?

はい。HTTP-01 challenge で必要です。Let's Encrypt は必ず port 80 で検証リクエストを開始します。Certbot には TLS-ALPN-01 の実装がないため、443 だけを開く firewall では、初回発行と、その後の自動更新の両方がブロックされます。port 80 から HTTPS への redirect は問題ありません。検証は redirect に従います。port 80 を完全に省略する唯一の方法は DNS-01 です。provider plugin を使うか、Certbot の --manual --preferred-challenges dns を使います。この場合は TXT record を自分で追加します。--manual-auth-hook script を指定しない限り、更新は自動ではなくなります。

Ubuntu 24.04 で nginx 用に Certbot をインストールする場合、apt と snap のどちらを使うべきですか?

apt を使います。sudo apt install certbot python3-certbot-nginx では Ubuntu 24.04 に Certbot 2.9.0 がインストールされます。このバージョンは、このガイドの内容に必要な機能を十分に備えています。unattended-upgrades を通じて security patch も適用され、snapd も必要ありません。最新 release をすぐに使う必要がある場合、または snap 限定で配布される DNS plugin が必要な場合だけ snap を選びます。どちらの場合も、選ぶのは必ず1つだけにしてください。2つをインストールすると、同じ /etc/letsencrypt tree を対象とする renewal timer が2つ動作します。忘れられた方が問題を起こします。

Certbot で nginx 用の wildcard certificate を発行できますか?

DNS-01 の場合に限り可能です。*.example.com のような wildcard には、challenge file を取得する単一の hostname がありません。そのため、--nginx、--webroot、--standalone はすべて使用できません。DNS provider 用の plugin をインストールし、scope を限定した API token を root のみが読み取れる credentials file に保存します。そのうえで certbot certonly --dns-cloudflare -d example.com -d '*.example.com' を実行します。wildcard が shell で glob 展開されないよう、引用符で囲んでください。

更新が成功したのに、nginx が古い certificate を提供し続けるのはなぜですか?

nginx は certificate をメモリに保持しているため、ディスク上の新しい file を reload するまで変更を認識しません。certbot --nginx は自動的に reload しますが、--webroot と --standalone の実行では reload されません。そのため、更新自体は成功していても、browser には有効期限が近い certificate が表示されることがあります。/etc/letsencrypt/renewal-hooks/deploy/ に実行可能な script を配置し、nginx -t && systemctl reload nginx を実行するようにします。この script は、更新が成功するたびに実行されます。

certbot renew --dry-run を実行すれば、更新が機能すると証明できますか?

おおむね確認できます。production 環境の rate limit に影響を与えず、certificate もディスクに保存せずに、staging 環境に対して実際の challenge を実行します。firewall、DNS、code path は本番と同じなので、成功すれば network 部分に問題がないことを確認できます。ただし、deploy hook が実行されることを確実に証明できるわけではありません。これは別にテストしてください。hook script を手動で実行し、sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf を確認します。