WordPressをNginx+無料のSSL証明書 Let's Encrypt でHTTPS化する

WebサイトのHTTPS化が加速しているなか、個人利用においては証明書の取得コストが若干の障壁になっていましたが、無料のSSL証明書発行の仕組み Lets’s Encrypt が正式版になったので利用してみます。なお、“WordPress” と書いていますが技術的には Nginx 配下のサービス全てに適用可能な内容です。

無料のSSL証明書 Let’s Encrypt とは

Let’s Encrypt - Free SSL/TLS Certificates

SSLを用いたHTTP通信の暗号化を誰でも手軽に行えるようにするために立ち上がったのがEFF、Mozilla、Cisco Systems、Akamai Technologies、IdenTrust、ミシガン大学の研究者などで、これらのメンバーがHTTPS普及のためにスタートした取り組みが「Let’s Encrypt」です。これまで手間がかかり金銭的な負担も大きいと言われてきたサーバー証明書の発行を無料で行えるようになるのですが、同取り組みはついにベータ版から正式版にサービスを移行しています。

無料で証明書を発行してHTTPSの導入をサポートする「Let’s Encrypt」がベータ版から正式版に - GIGAZINE

本エントリの目的

  • (#1) 手始めに、WordPressで構築しているBLOGをHTTPS化します
  • (#2) 当該サイトは、R-PROXY(nginx)による多段構成でWordPressに接続します

構成は以下のとおりとします。

+--------------+            +----------+            +--------------+
| Client(User) | ---------> | R-Proxy  | ---------> | Apache       |
|              | HTTPS(#1)  | (nginx)  |  HTTP(#2)  | (wordpress)  |
+--------------+  http2     +----------+            +--------------+

ハマりポイント1. WordPressがHTTPのURLを吐きつづける

(#1)のR-PROXY(nginx)に対してSSL証明書を適用する部分は証明書が Let’s Encrypt 発行になるだけで、適用プロセスそのものは通常の証明書と同様、特別なことはありません。むしろCSR発行が作業不要なので楽です。しかし(#2)の部分でHTTPを利用しているところが曲者で、WordPress本体としてはHTTPでアクセスを受け付けているのでHTMLに書き出される各種リソースURLの部分がHTTPのままになってしまい、ユーザーアクセスがいつまでたってもHTTPSに置き換わりません。

対処として以下のとおりコンフィグレーションします。

バックエンドのWordPressに対してプロトコルを通知する

httpsであることを通知するために X-Forwarded-Proto ヘッダがよく利用されています。nginxでもそれを連携できるようします。

前提として、リバースプロキシ構成を取る時の一般的なコンフィグ location / { ... } にてバックエンドのWordPressに中継している場合とします。

# ... 省略
  location / {
      proxy_pass http://wordpress;
      # ...中略
      proxy_set_header X-Forwarded-Proto $scheme;
  }
...

WordPress側でのHTTPS出力設定

wp-config.php に以下の記載を追加します。

  • WP_HOME および WP_SITEURL でのアドレスは各自ご利用のものに書き換えます。HTTPS化を前提としていますので、もちろん https://xxxx で記載します
  • FORCE_SSL_LOGIN と FORCE_SSL_ADMIN は、管理画面のみをSSL化する場合のオプションです。サイト全体をSSL化する場合は必要ありませんが明示のために気分で付与しています
  • if文で HTTP_X_FORWARDED_PROTO に関する記載があります。ここで、WORDPRESSがコンテンツ出力する時のURL群に適用する scheme を動的に変化させています。つまり、ユーザーからHTTPSアクセスが発生した祭には nginx にて X-Forwarded-Proto: https がWordPressのサーバーへ連携され、WordPressではそれを見てBODY出力の scheme をHTTPSにします
define('WP_HOME',    'https://example.com');
define('WP_SITEURL', 'https://example.com');
define('FORCE_SSL_LOGIN', true);
define('FORCE_SSL_ADMIN', true);
if ( ! empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ) {
  $_SERVER['HTTPS']='on';
}

HTTP_X_FORWARDED_PROTO の設定があれば、わざわざ WP_HOMEWP_SITEURL に対して設定は必要ないようにも見えるのですが、http のままにしておくと以下のように この接続は安全ではありません。このページの一部(画像など)は安全ではありません。 と警告が発生します。この状況は Mixed Content (混在コンテンツ) と呼ばれるもので、HTTPとHTTPSが混在している通信は安全ではないと判定されるためです。混在コンテンツ - Security | MDN

なお、WP_HOMEWP_SITEURL は WordPress 管理者画面 -> 設定 -> 一般設定にある項目と同じ役割を示しています。

wp-config.php上の表現 管理者画面上の表現
WP_SITEURL WordPress アドレス (URL)
WP_HOME サイトアドレス (URL)

WP_SITEURL = サイトアドレス(URL)のように見えますが入れ子になっているので要注意

管理者画面上から変更してもよいのですが、環境によっては設定を失敗すると無限ループが発生して管理者画面へアクセスできなくなり、その場合はSQLを直接叩いて修正する必要がでてきます。wp-config.phpの方が効力が強いので上書き可能ですが、構成検証のし易さの観点で、ここでは初めから wp-config.php に記載しておきます。

無限ループが発生してしまった場合

wp_optionsテーブルに対して、以下のSQLで対処可能です。 現在利用しているバージョンのWordPressでも同様であるかは念のための確認を推奨します。

各キーが存在するか、そしてその内容が現在の遭遇している状態と正しいかを確認する。

select * from wp_options where option_name = 'home';
select * from wp_options where option_name = 'siteurl';

値を書き換える。

update wp_options set option_value = '(正しいURL)' where option_id = (確認したoption_id);

ハマりポイント2. 一部URLがHTTPのまま残り続ける

恐らく理由としては2通りです。

  1. plugin に http 固定値での文字列が埋め込まれている
  2. 自分で作成したwidgetやテーマなど何らかの形でhttpの固定値が埋め込まれている

この状態だと、いつまで経ってもブラウザのHTTPS暗号化マーク(鍵マーク)が表示されません。これは、対象のページ内で利用されているコンテンツが全てHTTPSで保護された接続になっている必要があるためです。

地道ですが、Chromeのデベロッパーツール(Google 検索)などを利用しながらHTTPアクセスとなっている部分を確認し、スクリプトの修正が、場合によってはPluginの見直し(廃止やその他同類プラグインへの置き換え)を実施します。

Let’s Encrypt 証明書適用方法

ハマりポイントを先に記載したところで、以下通常の適用方法を記載します。 以下の点を予め抑えておくと理解が捗ります。

  • CSR作成や証明書発行機関への送付などの面倒臭い処理は、すべてLet’s Encryptが配布しているスクリプトにより自動化されている
  • 証明書などの各種ファイルは、/etc/letsencrypt/へドメイン毎に格納されている
  • 発行された証明書の有効期限は90日と短め。自動更新のcronを設定することが望ましい
  • 発行時にはDV認証(ドメインの保有者であることの確認。組織の認証は行わない)が行われる。その際、スクリプト実行時のサーバーの80または443が利用される
  • 更新(renew)コマンドは、証明書有効期限30日未満の場合は実行されない

80または443をどういう優先順で利用されるのかは調べておらず、把握していません。 DV認証時、既に動いているWebサーバーがある場合はポートが競合しますので、対応として2種類の方法が存在します。

共通作業:Let’s Encryptが配布するスクリプトをダウンロード

gitを利用します。保存場所は任意で構いません。

$ git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt
$ ./letsencrypt-auto --help

実行時にsudoをする必要はありません。必要な時はスクリプト内でパスワードを求められます。

稼働中のサーバーを落として認証用のサーバーを起動 (standalone)

sandaloneモードでスクリプトを起動します。

$ ./letsencrypt-auto certonly -a standalone -d example.com
                                              # (ドメイン部分は適宜修正)

メッセージと共に /etc/letsencrypt/live/(ドメイン)/fullchain.pem が生成されていれば成功です。

DV認証そのものにあたっては既存Webサーバーに対して設定変更が不要ですが、cronでの更新自動化を行うには、nginxの停止・起動を含めて処理を組む必要があります。

現在稼働中サーバーに認証用のアクセスを許容する(webroot)

$ ./letsencrypt-auto certonly --webroot -w /path/to/doc_root/ -d example.com
                                                      # (ドメイン部分は適宜修正)

webrootの仕組み /path/to/doc_root/で設定した部分に、スクリプトが /.well-known/acme-challenge/xxxxxx(←ランダム文字) という構造でファイルを作成し認証しているようです。従って、WordPressをリバースプロキシしているnginxに対しては、以下のコンフィグを適用する必要があります。

    location ^~ /.well-known/acme-challenge {
        root /path/to/doc_root/;
    }

standaloneモードに対し、こちらの場合は一度設定してしまえばnginxの停止・起動は不要になります。但し、SSL証明書を更新後の再読み込みにはreloadが必要ですので、その処理は残ります。

nginxへのSSL証明書適用

ここは一般的なノウハウとなります。証明書は以下のとおり。

    ssl_certificate /etc/letsencrypt/live/(ドメイン)/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/(ドメイン)/privkey.pem;

しばらくは 80, 443 いずれも残した状態で動作確認を行い、443ポートでの展開ができる状態になったら、元々のHTTP(80ポート)へのアクセスは全てHTTPSページで誘導するよう、リダイレクト設定を行います。

server {
    listen 80; # httpへのアクセスをhttpsへリダイレクトする
    listen [::]:80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    ...
    省略
}

まとめ

証明書発行は楽だし、更新も自動化できるし、ドメイン数を気にせず発行できるし、良い仕組みだと思います。