R e i - D r e a m
for Laravel
Guest
login

最終投稿日:2026年06月24日

【検証環境】- acme.sh - Laravel Ver.12 - Windows11 - Rocky Linux Ver.8

SSL証明書の自動更新設定を『Cloudflare』管理下で実施した話

はじめに

早いものでサイトを公開して2ヶ月が経過しようとしています。
あの 格闘の時間 がつい昨日の様にも感じられますね。
そして『2ヶ月』と言えば例の『積み残し案件』ですよね。そう、
    『SSL証明書の再発行』です。
以前環境を構築した際の相棒(AI)からの提案は以下の手順でした。
① クラウドフレアへ、ログインし自身のダッシュボードへ移動
② [DNS] > [レコード]と選択
③ 一覧から《タイプA》のレコードを「編集」押下
④ [プロキシステータス]を切り替えると、雲がグレーになるので「保存」押下
⑤ この状態で1分後くらいに自身のサーバから証明書を更新
    # certbot renew
⑥ クラウドフレアに戻り雲をオレンジにする
う~ん。面倒くさい。
この手順を自動化するのはもっと面倒くさいです。

実はSSL証明書は2通必要

ここで少し勉強も兼ねて、自身のサイトとクラウドフレアの関係をおさらいしましょう。
クラウドフレアの役割は超ざっくり言うと『プロキシによるネットからの強力な防壁(セキュリティ+高速化)』です。
イメージとしては、自分のサイトの前に「超優秀な警備員兼受付」を置くようなものです。
ネットからのアクセスをすべてこの警備員(クラウドフレア)がチェックし、
怪しい攻撃や大量の迷惑アクセスを遮断して、安全なアクセスだけを自身のサーバーに届けてくれます。
直接サーバーを晒さずに済む事は、
更に言い換えればクレジットカードの番号を晒さずにショッピングができる『d払い』と少し似ていますね。
本題に戻ります。
SSL証明書が何故2通必要か?
それはユーザーがあなたのサイトにアクセスする際に、以下の様なプロセスが発生する為です。
    『ユーザー』> 通信① >『クラウドフレア』> 通信② >『あなたのサイト』
もうお分かりですね。
通信①②でそれぞれSSLによる暗号化が必要です。
そして私の場合は『さくらのサーバー側で発行・設定した証明書』を使用しているため『通信②』が暗号化されています。
では『通信①』は?
はい、クラウドフレアが自動で証明書を発行して暗号化してくれています。ありがたや。。

SSL証明書の有効期限を「cURL」で確認する

まず環境は『Laravel Ver.12』となります(PHP8)
ここで1つ注意が必要です。
有効期限はブラウザからも確認できます(アドレスバーの左側の鍵アイコンから)
しかしこの有効期限、自身のサーバで発行した期限とズレてる事に気づいた方いますか?
    実はこの有効期限は『[ユーザー]⇔[クラウドフレア]』間のSSL通信の有効期限となります。
しかもこちらの有効期限は、クラウドフレアが自動的に更新してくれるので『放置案件』となります。
そこで肝心な、更新する必要のある有効期限の確認方法を『cURL』通信で取得したいと思います。
任意のクラス

<?php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
use App\Mail\SslCheckMail;

class BatchSslCheck extends Command {
    protected $signature = 'app:batch-sslCheck';
    protected $description = 'Command description';
    
    // バッチ実行

    public function handle() {
        // ドメイン
        $domain = "reixxxxx.net";
        // IPアドレス
        $originIp = "xxx.xxx.xx.xxx";
        Log::info("【BatchSslCheck】実行(対象ドメイン:{$domain}、対象IP:{$originIp})", []);
                 // 証明書確認通信を2回実施する
        $subject_list = [0 => 'さくら', 1 => 'クラウドフレア'];
        foreach ($subject_list as $idx => $val) {
            // cURLの基本設定
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, "https://{$domain}:443");
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            // ヘッダーのみ(通信削減)
            curl_setopt($ch, CURLOPT_NOBODY, true);
            // 証明書情報を取得
            curl_setopt($ch, CURLOPT_CERTINFO, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
            curl_setopt($ch, CURLOPT_TIMEOUT, 5);
            // [curl_setopt]の再利用を禁止する
            curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
            curl_setopt($ch, CURLOPT_FORBID_REUSE, true);
            if ($idx == 0) {
                // 自身のサーバで発行した証明書を参照
                curl_setopt($ch, CURLOPT_RESOLVE, ["{$domain}:443:{$originIp}"]);
            } else {
                // クラウドフレア側の証明書を参照
                curl_setopt($ch, CURLOPT_RESOLVE, ["-{$domain}:443"]);
            }
            // 通信実行
            curl_exec($ch);
            $certInfo = curl_getinfo($ch, CURLINFO_CERTINFO);
            curl_close($ch);
            // 通信結果を整理して格納
            $certList = $certInfo[0] ?? [];
            $pem = null;
            // 配列の中から『PEM形式』の証明書テキストを探す
            foreach ($certList as $item) {
                if (str_contains($item, '-----BEGIN CERTIFICATE-----')) {
                    $pem = $item;
                    break;
                }
            }
            $parsedCert = "";
            $sslLimit = "";
            $countDay = "";
            // 証明書テキストが見つかった場合
            if ($pem) {
                // OpenSSL関数で直接証明書を解析
                $parsedCert = openssl_x509_parse($pem);
                $validTo = \Carbon\Carbon::createFromTimestamp($parsedCert['validTo_time_t'])->timezone('Asia/Tokyo');
                $daysRemaining = (int) \Carbon\Carbon::now('Asia/Tokyo')->diffInDays($validTo, false);
                $parsedCert = $parsedCert['issuer']['O'] ?? 'Unknown';
                $sslLimit = $validTo->toDateTimeString();
                $countDay = $daysRemaining;
                
                Log::info("【証明書発行元】" . $parsedCert, []);
                Log::info("【SSL有効期限】" . $sslLimit, []);
                Log::info("【SSL残日数】" . $countDay, []);
            } else {
                Log::warn("{$domain} の証明書を取得できませんでした。", []);
                return;
            }
            // メール送信設定
            $mail_flg = false;
            switch ($countDay) {
            case '90':
            case '30':
            case '20':
            case '10':
                $mail_flg = true;
                break;
            default:
                if ($countDay < 10) {
                    $mail_flg = true;
                }
            }
            // 該当しない残日数の場合はメール送信しない
            if (!$mail_flg) {
                continue;
            }
            // メール送信
            $mailTo = config('mail.from.address');
            $content = [
                'mailTo' => $mailTo,
                'subject' => $val . ' SSL通信有効期限確認',
                'parsedCert' => $parsedCert,
                'sslLimit' => $sslLimit,
                'countDay' => $countDay,
                'view' => 'sslCheck',
            ];
            try {
                Mail::to($mailTo)->send(new SslCheckMail($content));
            } catch(Exception $e) {
                throw new CustomException(
                    ['code'=>'xxxx-xxxxx-xxxxx', 'message'=>'メールの送信に失敗しました。', 'status'=>500]
                );
            }

        }
    }
}

サンプルは『バッチ』処理となっています。
また特定の残日数で『メール送信』します。
ポイント

cURLオプション「CURLOPT_RESOLVE」でIPアドレスとのマッピングをする事で自身のサイトを参照できます。
またループ処理で複数通信する場合、前回の内容を使い回さない様にする以下のオプションも付与します。

・CURLOPT_FRESH_CONNECT
・CURLOPT_FORBID_REUSE

Cloudflareから『APIトークン』を発行する

『cURL』通信でクラウドフレアの管理下にも関わらず『自サーバの証明書残日数』が確認できました。
そこである期待が心をよぎります。
    参照ができるんなら設定もできるのでは?
ちょっと相棒(AI)にご相談。
するとあっけなく『CloudflareのAPIトークンを使用して「acme.sh」経由でできるぞ』との回答。
むむむ。
何はともあれ『CloudflareのAPIトークン』が必要なのか。
当然会員だし、サクッと頂きますか!
手順は以下となります。
① ログイン後、右上のアカウントアイコンから「プロフィール」を選択
② 左メニューから「APIトークン」を選択
③ 画面右上にある「+トークンを作成する」を押下
④「ゾーン DNS を編集する」の「テンプレートを使用する」を押下
⑤ ゾーン リソースで自身のドメインを選択する
⑥「概要に進む」押下
⑦「トークンを作成する」押下
すると『APIトークン』が以下の様に表示されるので、忘れずメモします。
    cfut_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxde8
また同時に『テストスクリプト』も表示されるので、ターミナルから実行してみましょう。
    curl "https://api.cloudflare.com/client/v4/user/tokens/verify" -H "Authorization: Bearer cfut_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxde8"
ここまで想定通りにできた方は、この章は読み飛ばして下さい。
問題は『APIトークン』を発行出来なかった方となります。
手順①~⑦でメールで送信される『確認キー』で認証しろ的なダイアログがPOPしましたか?
そして何度認証キーを入れても画面左下に、いかにも怪しいメッセージが出ませんでしたか?
please verify your email.
それは『完全にやらかしてます』
実はクラウドフレアに入会した際に『メールアドレス』を登録したハズです。
すると返す刀でクラウドフレアから以下のメールが送信されているハズです。
Verify your email address
そのメールを開くと以下のボタン(翻訳してます)があるハズです。
メールを確認する
そうです。
    これを押下しないと『APIトークン』は発行出来ないのです。
さぁ今すぐ探すのですっ!!
    ※ 最悪メールアドレスを変更する手段もあります

SSL更新を『acme.sh』経由で実行する準備

『acme.sh』を一言でざっくり言うと以下となります。
[Let's Encrypt]などの無料SSL証明書を、コマンド1つで自動発行・更新してくれる軽量な便利ツール
○ acme.shの存在確認
ワンチャン『acme.sh』が既にインストールされているか確認します。
    find ~ -name "acme.sh"
何も表示されなければインストールされていません。
○ tarコマンド確認
軽量パッケージのOS(私の場合はRocyLinux)の場合「tar」がインストールされていない場合があるので以下で確認します。     tar --version
存在しない場合は以下でインストールします。
TeraTerm

$ sudo dnf install -y tar
トランザクションを実行しています
準備中                      :                                                          1/1
インストール中        : tar-2:1.30-11.el8_10.x86_64            1/1
scriptletの実行中      : tar-2:1.30-11.el8_10.x86_64            1/1
検証中                      : tar-2:1.30-11.el8_10.x86_64            1/1

インストール済み:
tar-2:1.30-11.el8_10.x86_64
$

○ acme.shのインストール
基本的に一般ユーザーで実施する事をお勧めします。
TeraTerm

$ curl https://get.acme.sh | sh -s email=[email protected]
[2026年 8月 10日 月曜日 21:52:40 JST] Installing cron job
[2026年 8月 10日 月曜日 21:52:40 JST] bash has been found. Changing the shebang to use bash as preferred.
[2026年 8月 10日 月曜日 21:52:41 JST] OK
[2026年 8月 10日 月曜日 21:52:41 JST] Install success!
$

『success!』と表示されれば成功です
※「[email protected]」はご自身の環境に合わせます

SSL更新を『acme.sh』経由で実行する

それでは実際に更新用SSL証明書を発行します。
① クラウドフレアで作成した『APIトークン』をサーバの環境変数に登録します。
export CF_Token="cfut_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxde8"
② 新しい有効期限の証明書を取得。
TeraTerm

$ source ~/.bashrc
$ acme.sh --issue --dns dns_cf -d あなたのドメイン名 -d www.あなたのドメイン名
[2026年 8月 10日 月曜日 21:57:04 JST] Using CA: https://acme.zerossl.com/v2/DV90
[2026年 8月 10日 月曜日 21:57:04 JST] Account key creation OK.
[2026年 8月 10日 月曜日 21:57:04 JST] No EAB credentials found for ZeroSSL, let's obtain them
~ 省略 ~
[2026年 8月 10日 月曜日 21:58:35 JST] Le_LinkCert='https://acme.zerossl.com/v2/DV90/cert/xxxxxxxxxxxxxxxxxxxxx'
[2026年 8月 10日 月曜日 21:58:37 JST] Cert success.
-----BEGIN CERTIFICATE-----
MIIDjTCCAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwRjEL
MAkGA1UEBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxXWmVy
~ 省略 ~
ZWlkcmVhbxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx15C5
oYiL+Wif/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPQBq
Dg==
-----END CERTIFICATE-----
~ 省略 ~
[2026年 8月 10日 月曜日 21:58:38 JST] ARI suggestedWindow:
2026-10-24T23:59:59Z to 2026-10-26T23:59:59Z
[2026年 8月 10日 月曜日 21:58:38 JST] Next renewal time picked from ARI window:
2026-10-26T12:58:37Z
$

成功しましたが、何やら日付が2つ『2026-10-24』『2026-10-26』があります。
これは、次回証明書の推奨更新期間となります。
つまり『今回証明書発行したので、次回はこの期間で再度発行してね』って事です。
初回に取得した『Let's Encrypt』での更新タイミングは60日後だったので少し長くなってますね。
③ 発行した証明書を前回の証明書に上書き保存するコマンドを作成。
まずは何処に保存するか調べる必要があるので、以下コマンドを叩きます。
TeraTerm

$ sudo grep -riE "SSLCertificateFile|SSLCertificateKeyFile" /etc/httpd/
/etc/httpd/conf.d/reidream-le-ssl.conf:SSLCertificateFile /etc/letsencrypt/live/xxxxx/fullchain.pem
/etc/httpd/conf.d/reidream-le-ssl.conf:SSLCertificateKeyFile /etc/letsencrypt/live/xxxxx/privkey.pem
$

表示された内容に従い以下のコマンドを完成させます。
書式

acme.sh --install-cert -d あなたのドメイン名 -d www.あなたのドメイン名 \
--key-file            SSLCertificateKeyFile のパス \
--fullchain-file    SSLCertificateFile のパス \
--reloadcmd      "sudo systemctl reload httpd"

④ 上書き保存前の確認。
コマンドが作成できたら、実際に書き込み権限があるか確認します。
今後の自動更新を考慮して一般ユーザーで作業ができるかがポイントとなります。
    cd /etc/letsencrypt/live
移動が出来ない場合は権限が不足していますので以下で変更します。
    sudo chmod 755 /etc/letsencrypt
    sudo chmod 755 /etc/letsencrypt/live
これで移動は出来ましたが、以下コマンドで書き込み権限がある確認します
    ll
    drwxr-xr-x ~
上記の様な感じだと一般には書き込み権限がないので、以下コマンドで付与します
    sudo chown -R $USER:$USER /etc/letsencrypt/
⑤ 発行した証明書を前回の証明書に上書き保存する。
それでは③で作成したスクリプトを実行します。
複数行ありますが、1度に実行ですよ。
成功すると最後の行に以下の文字が表示されるハズです。
    『Reload successful』
これは証明書を上書きして『Apache』のリロードもしたよって意味です。
⑥ 今後の自動更新処理を登録する。
証明書の自動更新を『cron』に設定します。
TeraTerm

$ acme.sh --install-cronjob
[2026年 8月 10日 月曜日 22:54:21 JST] Installing cron job
40 4,10,16,22 * * * "/home/xxxxx/.acme.sh"/acme.sh --cron --home "/home/xxxxx/.acme.sh" > /dev/null
$

※ 毎日6時間おきの[40分]にバッチが起動し有効期限前になると、自動的にSLL証明書を更新します
⑦ 証明書が更新されたか確認する。
ブラウザからの確認は無駄(クラウドフレア側の証明書のため)なので、以下コマンドで確認します。
TeraTerm

$ openssl x509 -in /etc/letsencrypt/live/xxxxx.net/fullchain.pem -noout -dates
notBefore=Aug 10 00:00:00 2026 GMT
notAfter=
Nov 8 23:59:59 2026 GMT
$

上記の様に有効期限が延長されていれば、SSLの更新自体は成功しています。
    ※ 2026年11月8日
または『SSL証明書の有効期限を「cURL」で確認する』の章で紹介した方法でPHP経由でも確認できます。

自動更新の成功可否

さて、SSL証明書の自動更新の設定まで、全ての手順が完了しました。
でも今までの作業は『全て手作業』でしたよね。
本来の真価を発揮するのは、約2.5ヶ月後です。
ですので本当の意味での『成功』『失敗』は、現在空白の以下に追記しますね。
    SSL証明書の自動更新は『  』しました!
それでは、最後までお付き合いくださりありがとうございました。
レンズモード 【新規購入限定用】
ログインしてコメントを残そう!!


きっぷる