無料SSL「Let’s Encrypt」でワイルドカードを設定し、ValueDomainで自動更新する

アフィリエイト広告を利用しています

このページの内容が役に立ったら X (旧twitter) でフォローして頂けると励みになります
挨拶や報告は無しで大丈夫です

社内資料として非公開していた情報を整理しながら公開していくシリーズです。
若干情報が古い場合もありますがご了承ください。

さて今回は、無料SSLで有名なLet's Encryptについて書いていってみたいと思います。

事前準備

まずは確認しましょう。

rpm -qa | grep centos-release
centos-release-7-9.2009.2.el7.centos.x86_64

Certbot のインストール

yum install epel-release
yum install certbot

Certbot のバージョンを確認

certbot --version
certbot 1.11.0

自動更新を設定する場合

Let's Encryptは3カ月以内ごとに更新処理をしないといけないことで有名です。

最初に説明する方法は最初から最後まで手動で行う方法です。

自動更新を考えている場合は、二度手間になりますので「バリュードメイン / Value Domainで証明書の自動更新設定」から始めてると良いです。

ワイルドカードドメインを登録

example.com は登録したいドメインに書き換えてください
下記の例では、「*.example.com」と「example.com」を登録しています

certbot certonly \
--manual \
--server https://acme-v02.api.letsencrypt.org/directory \
--preferred-challenges dns \
-d *.example.com \
-d example.com \
-m sample@example.com \
--agree-tos

最初の証明書が正常に発行されたら、Let's Encrypt プロジェクトの創設パートナーであり、Certbot を開発している非営利団体である Electronic Frontier Foundation に電子メール アドレスを共有していただけますか? Web の暗号化に関する私たちの取り組み、EFF のニュース、キャンペーン、デジタルの自由をサポートする方法についての電子メールをお送りします。

y を入力してEnter

下記、本当は英語ですが重要情報を含むのでスクショは無しで、日本語にすると次のような内容です。

アカウントが登録されました。
*.example.com および example.com の証明書を要求しています
次のチャレンジを実行しています:
example.com の dns-01 チャレンジ
example.com の dns-01 チャレンジ


次の値を持つ _acme-challenge.example.com という名前で DNS TXT レコードを展開してください:

xxxxxxxxxxxxxxxxxxxxxxxxxxxx

続行する前に、レコードが展開されていることを確認してください。

画面には「Press Enter to Continue」と出ていると思いますが、まだ次に進みません。

DNSの設定

設定しようとしているドメインのDNS設定画面を開きます。

txtレコードの
_acme-challenge.example.com に
xxxxxxxxxxxxxxxxxxxxxxxxxxxx を設定します。

指定方法は業者により異なりますが、私が管理しているところでは次のように指定します

txt _acme-challenge xxxxxxxxxxxxxxxxxxxxxxxxxxxx

ターミナルに戻り、Enter で進みます。

txtレコードの反映がまだなら進めない場合があります。

Enter をクリックするともう一度似たような表示になります。

よく見ると、コードが変わっています。

こちらもDNSの設定から登録します。

私の管理しているところでは次のように、2つ目として追加する形になります。

txt _acme-challenge xxxxxxxxxxxxxxxxxxxxxxxxxxxx
txt _acme-challenge yyyyyyyyyyyyyyyyyyyyyyyyyyyy

DNSを設定して、Enterを押すのが早いと失敗する可能性が高くなります。
数分程度ゆっくり待ってから Enter を押すなり工夫が必要になります。

証明書の確認

作成された証明書は次の場所にあります。

ls -la /etc/letsencrypt/live

httpd.confに設定

apache 2.4

SSLCertificateFile    /etc/letsencrypt/live/ドメイン/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/ドメイン/privkey.pem

apache 2.2

SSLCertificateKeyFile   /etc/letsencrypt/live/ドメイン/privkey.pem
SSLCertificateFile      /etc/letsencrypt/live/ドメイン/cert.pem
SSLCertificateChainFile /etc/letsencrypt/live/ドメイン/chain.pem

apacheの再起動

設定を反映させるため再起動します。

/etc/init.d/httpd restart

バリュードメイン / Value Domainで証明書の自動更新設定

前提として、PHP 7.2 以上がインストールされているものとします。
https://aulta.co.jp/technical/server-build/centos7/php/source-install-php-7-4-6

バリュードメインのAPIキーは、バリュードメインにログインして「マイページ > バリュードメインAPI」にあります。

最新版はGithubにあります

次のセクションからプログラムの説明をしていますが、若干古くなっております

最新版はGithubに入れておりますので参考にされる場合はGithubのほうをご確認ください。

https://github.com/aulta/letsencrypt-wildcard-dns-for-value-domain

プログラムの説明

APIキーを保存する設定ファイルを作成します。

vi /root/update_lets_encrypt_config.php
<?php
$config = [];
$config['value_domain_api_key'] = 'バリュードメインで発行したAPIキー';

[/SC_CODE]



<p>DNS設定を更新するPHPファイルを作成します</p>



vi /root/update_lets_encrypt_dns.php
<?php

define('LOG_PATH', __DIR__ . '/update_lets_encrypt.log');

function writeLog(string $message): void
{
    file_put_contents(LOG_PATH, date('Y-m-d H:i:s') . ' ' . $message . "\n", FILE_APPEND | LOCK_EX);
    echo $message . "\n";
}

$actions = [];
$actions[] = 'add';
$actions[] = 'clear';

$record_names = [];
$record_names[] = '_acme-challenge';

$action = '';
$domain = '';
$record_name = '';
$record_value = '';

if ($argc >= 2) {
    $action = (string) $argv[1];
}

if ($argc >= 3) {
    $domain = (string) $argv[2];
}

if ($argc >= 4) {
    $record_name = (string) $argv[3];
}

if ($argc >= 5) {
    $record_value = (string) $argv[4];
}

if ( ! in_array($action, $actions, true)) {
    writeLog('不正なパラメータ (1)');
    exit;
}

if ( ! preg_match('/\A[a-z0-9\.-]+\z/', $domain)) {
    writeLog('不正なパラメータ (2)');
    exit;
}

if ( ! in_array($record_name, $record_names, true)) {
    writeLog('不正なパラメータ (3)');
    exit;
}

if ($action === 'add') {
    if (empty($record_value)) {
        writeLog('不正なパラメータ (4)');
        exit;
    }
}

$config = [];
require_once __DIR__ . '/update_lets_encrypt_config.php';

$api_url = 'https://api.value-domain.com/v1/domains/' . $domain . '/dns';

$headers = [
    'Authorization: Bearer ' . $config['value_domain_api_key'],
    'Content-Type: application/json'
];

$ch = curl_init($api_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
if(curl_errno($ch)) {
    writeLog('cURLエラー: ' . curl_error($ch));
    exit;
}
curl_close($ch);

$dns_records = json_decode($response, true);

if (isset($dns_records['error'])) {
    writeLog('DNSレコードの取得に失敗しました: ' . print_r($dns_records['error'], true));
    exit;
}

if (empty($dns_records['results'])) {
    writeLog('DNSレコードの取得に失敗しました: results が空');
    exit;
}

// print_r($dns_records);
// echo "\n";

$update_data = [];
$update_data['domain'] = $dns_records['results']['domainname'];
// $update_data['ns_type'] = $dns_records['results']['ns_type'];
// $update_data['ttl'] = $dns_records['results']['ttl'];

if ($action === 'add') {

    $update_data['records'] = $dns_records['results']['records'] . "\n" . 'txt ' . $record_name . ' ' . $record_value;

} elseif ($action === 'clear') {

    $records = $dns_records['results']['records'];
    $records = str_replace(["\r\n", "\r"], "\n", $records);
    $records = explode("\n", $records);

    $new_records = [];
    foreach($records as $record) {

        if (empty($record)) {
            continue;
        }

        if (strpos($record, 'txt ' . $record_name . ' ') === 0) {
            continue;
        }

        $new_records[] = $record;
    }

    $update_data['records'] = implode("\n", $new_records) . "\n";
}

$ch = curl_init($api_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($update_data));

$update_response = curl_exec($ch);
if (curl_errno($ch)) {
    writeLog('cURLエラー: ' . curl_error($ch));
    exit;
}
curl_close($ch);

$update_response = json_decode($update_response, true);

if (isset($update_response['error'])) {
    writeLog('txt ' . $record_name . ' の更新に失敗しました: ' . print_r($update_response['error'], true));
    exit;
}

writeLog('txt ' . $record_name . ' を更新しました');

次のように実行します
手動で実行して、バリュードメインのコントロールパネル側に反映されることを確認しておきます。

# クリア
/usr/local/lib/php-7.1.6-mysqlc-mysqlnd/bin/php-7.1.6-mysqlc-mysqlnd /root/update_lets_encrypt_dns.php clear example.com _acme-challenge

# 追加
/usr/local/lib/php-7.1.6-mysqlc-mysqlnd/bin/php-7.1.6-mysqlc-mysqlnd /root/update_lets_encrypt_dns.php add example.com _acme-challenge sample_123456

CertbotがDNS-01チャレンジ用のTXTレコードを設定するためのフックスクリプトを作成します。

vi /root/update_lets_encrypt_dns_challenge.sh
#!/bin/bash

/usr/local/lib/php-7.1.6-mysqlc-mysqlnd/bin/php-7.1.6-mysqlc-mysqlnd /root/update_lets_encrypt_dns.php add example.com _acme-challenge "$CERTBOT_VALIDATION"

sleep 130
chmod +x /root/update_lets_encrypt_dns_challenge.sh

メインスクリプトを作成します

vi /root/update_lets_encrypt_dns.sh
#!/bin/bash

/usr/local/lib/php-7.1.6-mysqlc-mysqlnd/bin/php-7.1.6-mysqlc-mysqlnd /root/update_lets_encrypt_dns.php clear example.com _acme-challenge

certbot certonly \
--manual \
--server https://acme-v02.api.letsencrypt.org/directory \
--preferred-challenges dns \
-d *.example.com \
-d example.com \
-m sample@example.com \
--agree-tos \
--manual-auth-hook "/root/update_lets_encrypt_dns_challenge.sh" \
--manual-cleanup-hook "/usr/local/lib/php-7.1.6-mysqlc-mysqlnd/bin/php-7.1.6-mysqlc-mysqlnd /root/update_lets_encrypt_dns.php clear example.com _acme-challenge"
chmod +x /root/update_lets_encrypt_dns.sh

下記を実行して証明書が更新されるか確認します
CRONに登録するのもこのファイルになります

/root/update_lets_encrypt_dns.sh