はじめに

前回の記事に引き続き、今回は人気の高性能ウェブサーバであるNginxの導入と設定、そしてセキュアなHTTPSを構築する方法について説明していきます。

シリーズは今回で第二回となります。

これまでに公開した当シリーズに関する記事は以下のとおりです。

なお、このシリーズではホスティングにVultrを使っています。

サーバのスペックおよび構成は以下の通りです。

  • $ cat /etc/lsb-release

    • DISTRIB_ID=Ubuntu
    • DISTRIB_RELEASE=20.04
    • DISTRIB_CODENAME=focal
    • DISTRIB_DESCRIPTION="Ubuntu 20.04.3 LTS"
  • Vultr : High Frequency Server

    • 32GB MVMe
    • 1CPU
    • 1GB Memory
    • Additional Features
      • Virtual Private Clouds: ON

Nginxの導入

このセクションではNginxの導入手順について説明します。

IPアドレス直打ちによるサイト表示ではなく、自身で用意したドメインを使う場合には、事前にドメインに対するDNS(Domain Name System)の設定を行っておいてください。

テスト用のドメインを取得したい場合、Freenomを使えば無料でドメインを取得することができます(ユーザ情報をある程度埋めないと取得時にエラーが出るので注意)。

なお、このまま普通にインストールを行うとUbuntuで用意された古いバージョンのNginxがインストールされてしまうため、リポジトリ(ファイルの収納庫)を追加する必要があります。また、安全のために署名鍵検証も行う必要があります。

署名鍵のインポート

  1. 追加するリポジトリの安全性を検証するために、Nginxの署名鍵をインポートします。この際、よく使われていたapt-keyは廃止予定となっているため、代わりにGNU Privacy Guard(GPG)を使う必要があります。

$ curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg

  1. これにより、Nginxの署名鍵は/usr/share/keyrings/nginx-archive-keyring.gpgに格納されました。

リポジトリの追加

  1. 次にリポジトリの追加を行うため、$ sudo vim /etc/apt/sources.list.d/nginx.listでNginx用のリストを作成します。

nginx.list

## Replace $release with your corresponding Ubuntu release.
# deb https://nginx.org/packages/ubuntu/ $release nginx
# deb-src https://nginx.org/packages/ubuntu/ $release nginx

# Ubuntuのリリース名については`$ cat /etc/lsb-release`で確認することができます。
# Ubuntu20.04では$releaseの箇所がfocalとなる
deb [arch=amd64 signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/ubuntu/ focal nginx
deb-src [arch=amd64 signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/ubuntu/ focal nginx
  1. []内は先程インポートした署名鍵検証に使われる鍵を指定しており、これによりリポジトリごとに鍵が参照されます。

Nginxのインストールおよび起動

  1. Nginxのインストール準備が整いましたので$ sudo apt updateでファイルのアップデートをチェックします。

  2. そして$ sudo apt install nginxでNginxのインストールを行います。

  3. インストール後、$ nginx -vで現在のバージョンをチェックできます(執筆時点ではstableで1.20.2)。

  4. これまでのシリーズではUFWのポートは基本的に閉鎖しているため、ウェブサーバに必要な80番ポートを$ sudo ufw allow 80で開放します。

  5. インストール直後ではNginxは起動していないため、$ sudo systemctl start nginxでNginxを起動させます。

  6. $ systemctl status nginxでActiveの箇所がactive (running)となっていればNginxは起動状態となっています。

  7. この状態でサーバのIPアドレスにアクセスしてみましょう。Welcome to nginx!というページを表示できれば、無事Nginxの導入が完了です。

Nginx設定

では各種Nginxの設定を行っていきます。

  1. 例としてNginxが参照するウェブサイトのルートは/var/www/ドメイン名として扱います(example.comであれば/var/www/example.com)。そのため、$ sudo mkdir -p /var/www/ドメイン名でディレクトリを作成しておきます。

  2. そして、$ sudo cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/ドメイン名.confでNginxのサイト用設定ファイルを自身のサイト用としてコピーします。

  3. その後$ sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf_bというように、使わないデフォルトファイルをリネームしておきます。

  4. $ sudo vim /etc/nginx/nginx.confでNginxの全体設定を行うファイルを編集します。

nginx.conf

#### nginx documentation
#### https://nginx.org/en/docs/

# workerプロセスの実行ユーザを指定
user  nginx;

# workerプロセスの数(CPUの数)を指定
worker_processes auto;

# CPUの使用を均等にする
worker_cpu_affinity auto;

# workerプロセスの最大オープンファイル数の上限(RLIMIT_NOFILE)を変更
# workerプロセス数 * 設定する予定のworker_rlimit_nofileの値 < OS全体で扱えるファイル数以下の値
# OS全体で扱えるファイル数 / workerプロセス数 = 設定すべきworker_rlimit_nofileの値
#
# OSが扱える最大ファイル数
# $ cat /proc/sys/fs/file-max
#
# OS全体で扱えるファイル数 / workerプロセス数 = 設定すべきworker_rlimit_nofileの値(上限ギリギリにはしない)
#
# 余裕を持たせる場合はworker_connections の2-4倍以上の値を設定
# [Nginx]worker_connectionsとworker_rlimit_nofileの値は何がいいのか?
# https://qiita.com/mikene_koko/items/85fbe6a342f89bf53e89
worker_rlimit_nofile 100000;

# nginxのプロセスID格納先を指定
pid        /var/run/nginx.pid;

# PCRE JITを用いて正規表現の処理を高速化させる
pcre_jit on;

events {

    # 1つのworkerプロセスが処理する最大コネクション数を指定
    # 基本的に"worker_connections * 2 < worker_rlimit_nofile"を守る
    worker_connections 1024;

    # クライアントからのリクエストをできるだけ受け取る
    multi_accept on;

    # Linuxカーネル2.6以上の場合はepoll、BSDの場合kqueue
    # epollはselect/pollに比べて計算量が少なく、また監視対象のファイルディスクリプタの数が無制限
    use epoll;

    # workerプロセスが順番に新しい接続を受け入れる(接続量が少ない場合はリソースが無駄なので"off")
    accept_mutex        on;

    # mutexの確保に失敗した際の待機時間を調整
    accept_mutex_delay 100ms;
}

http {

    # HTTPレスポンスヘッダのContent_Typeに付与する文字コード
    charset UTF-8;

    # nginxがデフォルトで用意するMIMEタイプと拡張子のマッピングファイル
    include       /etc/nginx/mime.types;

    # マッピングにない拡張子のdefault
    default_type  application/octet-stream;

    # ログフォーマット。レスポンスタイプ「$request_time」を追加
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$request_time"';

    # 設定したアクセス数に対して制限を定義する(ドメイン名.confにも設定)
    # ファイルリクエスト1件ごとに判定が行われるので注意
    # Module ngx_http_limit_req_module
    # https://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req_zone
    # 20/sを超えるリクエストを行ったIPを制限ゾーンに配置する
    # limit_req_zone $binary_remote_addr zone=one:10m rate=20r/s;

    # 制限ゾーンに入ったIPは10reqを限度に処理を待機(超えると即エラー)
    # limit_req zone=one burst=10 nodelay;

    # limit_reqに関するログの発生レベルをerrorに設定
    # limit_req_log_level error;

    # 制限時のステータスを444で返却する
    # limit_req_status 444;

    # Nginxでログの出力を停止する
    # https://worklog.be/archives/2890
    # エラーログを出力しない
    #error_log   /dev/null crit;

    # エラーログのレベルをwarnに設定
    error_log  /var/log/nginx/error.log warn;

    # アクセスログの出力を停止する
    #access_log  /var/log/nginx/access.log  main;
    access_log off;

    # 毎回リクエストが飛ぶのでサーバ側でキャッシュ制御を行わない
    etag off;

    # nginxのバージョンを非表示
    server_tokens off;

    # ハードディスクio処理とsocket-io処理のバランスを取る
    sendfile on;

    #  一つのデータパッケージに全てのヘッダー情報を含む
    tcp_nopush on;

    # 送信済みデータの応答待ちの状態でも遅延させることなくデータ送信 負荷が高い場合はoff
    tcp_nodelay on;

    # HTTP通信をタイムアウトせずにいる秒数
    keepalive_timeout 10;

    # クライアントのリクエストヘッダに対する読み込みのタイムアウト時間
    client_header_timeout 10;

    # クライアントのリクエストボディに対する読み込みタイムアウト時間
    client_body_timeout 10;

    # タイムアウトした接続をリセットするかどうかの設定
    reset_timedout_connection on;

    # クライアントに応答を返すまでのタイムアウト時間
    send_timeout 10;

    # server_name が長い場合に発生するエラーへの対処
    server_names_hash_bucket_size 64;

    # コンテンツ配信などの最適化に利用するハッシュテーブルのサイズを設定
    types_hash_max_size 1024;

    # 同時接続数制限の処理で確保するメモリサイズ
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    # 1つのIPアドレスからの接続数の制限値を設定
    limit_conn addr 100;

    # レスポンスをgzipで圧縮する
    gzip  on;

    # レスポンスヘッダに Vary: Accept-Encoding を挿入
    gzip_vary on;

    # HTTPバージョン1.0から圧縮
    gzip_http_version 1.0;

    # IE6では圧縮されたファイルの展開に失敗することがあるので圧縮しない
    gzip_disable "msie6";

    # Viaヘッダが付いててもレスポンスをgzip圧縮
    # コンテンツキャッシュとVaryヘッダとnginx
    # https://qiita.com/cubicdaiya/items/09c8f23891bfc07b14d3
    gzip_proxied any;

    # 圧縮対象となるサイズを一定以上にすることでCPU負荷を軽減
    gzip_min_length 1024;

    # gzip圧縮サイズは1と9で大差ないので1を設定
    gzip_comp_level 1;

    # gzip圧縮で使用するバッファサイズ
    gzip_buffers 16 8k;

    # 指定されたMIMEタイプを圧縮する
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;

    # 事前にgz圧縮されたファイルがあればgzをそのまま相手に送る
    gzip_static on;

    # gzipに対応していないクライアントの場合、gzを解凍してから相手に送る
    gunzip on;

    # ファイルディスクリプタやサイズ、更新時間などのメタデータをキャッシュする最大数と非アクセス時の削除時間
    open_file_cache max=1000 inactive=10s;

    # キャッシュのチェック間隔
    open_file_cache_valid 60s;

    # 設定された回数使われていなければ、キャッシュされない
    open_file_cache_min_uses 1;

    # open_file_cacheによるファイルルックアップエラーのキャッシュの有無
    open_file_cache_errors on;

    # 各種confを読み込む
    include /etc/nginx/conf.d/*.conf;

}
  1. 次に$ sudo vim /etc/nginx/conf.d/ドメイン名.confでサイト側にのみ適用する設定を行います。

ドメイン名.conf

server {
    # 接続するポート番号を指定
    listen    80;

    # URLに表示されるドメインまたはIPアドレス
    # server_name localhost;
    # server_name example.com;
    server_name    ドメイン名;

    # ウェブサイト表示時に参照されるディレクトリ
    root /var/www/ドメイン名;

    # indexページとして読み込むファイル(indexは各locationにも継承される)
    index   index.php index.html index.htm;

    # 初期位置
    location / {
    }

    # 404ページの指定
    #error_page 404 /404.html;
    #    location = /404.html {
    #}

    # 50*ページの指定
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        # 50x.htmlを参照するルートを指定
        root   /usr/share/nginx/html;
    }

}
  1. $ sudo service nginx restartを実行することでこれまでの設定が反映されます。

  2. しかし、現在の設定では/var/www/ドメイン名に何もファイルが置いていない状態なので、Nginxは403ページを表示します。そのため、index.htmlを該当ルートに直接あるいはFTP経由などで設置すると、エラーが発生することがなくなります。

HTTPSに対応させる

問題なくウェブサーバおよびサイトが動作するならば、HTTPSはとても簡単に導入することができます。

また、何らかのドメインを所有していればLocalhost(ローカルホスト)でのHTTPS環境を作ることも可能です(先述したFreenomを使えば無料)。

localhostの場合は取得したドメインのDNSサーバ設定でAレコードのVALUE(サブドメインを入力する箇所ではない)に127.0.0.1を設定しておいてください。

今回の記事ではLet's Encryptと呼ばれる、無料で証明書が取得可能な認証局を使用してHTTPS化を行います。

LetsEncryptのインストールおよび証明書の取得

  1. HTTPSで通信を行うには443ポートが必要なので$ sudo ufw allow 443でUFWに許可させておきます。

  2. まず$ sudo apt updateでアップデートチェックを行った後、$ sudo apt install snapdでSnappyと呼ばれるパッケージ管理システムをインストールします(最近のUbuntuであれば基本的に既に入っています)。

  3. $ sudo snap install coreおよび$ sudo snap refresh coreで最新バージョンのsnapを使用しているか確認します。

  4. 次に$ sudo snap install --classic certbotでsnapのセキュリティ制限を回避(classic)しつつCertbotをインストールします。

    1. 旧バージョンのcertbotが入っていれば$ sudo apt remove certbotで証明書等のファイルを残したままcertbotを削除できます。

    2. $ sudo apt install certbotでcertbotをインストールする方法もありますが、バージョンが古すぎる(0.40)ので推奨されません。

    3. CertbotのインストールにSnap版を使うようになった理由は以下に記載されています。

  5. インストールが終われば、$ sudo certbot --nginx -d ドメイン名でCertbotを用いて証明書の取得を行います。

    1. $ sudo certbot --nginx -d example.com -d www.example.comというコマンドならば、www有り無し両方を取得できます。
    2. シリーズではこれまでの設定でNginxを用いているため、Nginxプラグインで証明書の取得を行います。
    3. 他の手段にはCertbot自身がウェブサーバ(ポート番号80を使用)を一時的に起動して認証する方法(standalone)、指定した公開ディレクトリに特定のファイルを配置する方法(webroot)があります。
    4. ワイルドカードやlocalhostでの証明書取得方法はDNSでの認証(dns-01)を使いますが、これは後述します。
  6. 実行後は連続して文章が出てきますので、その内容に沿ってタイプしていきます。

1. 証明書の更新やセキュリティに関するメールを受信するためのメールアドレスを入力する。
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): メールアドレスを入力

2. 規約に同意するかどうか同意(Yes)しないと次に進めない。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

3. メールアドレスをElectronic Frontier財団に登録するかどうか。
Yes(Y)で登録するとメールマガジンが配信される。
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
(Y)es/(N)o: Y

4.  問題無く証明書を取得できれば、証明書の場所とその有効期限が表示されます。
Account registered.
Requesting a certificate for 証明書を取得するドメイン名

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2022-01-01-.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for 証明書を取得するドメイン名 to /etc/nginx/conf.d/ドメイン名.conf
Congratulations! You have successfully enabled HTTPS on https://ドメイン名

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  1. SnapでCertbotのインストールを行った場合は証明書の自動更新に対応しています。

  2. $ view /etc/systemd/system/snap.certbot.renew.timerおよび$ view /etc/systemd/system/snap.certbot.renew.serviceで自動更新の日時をチェックすることができます。

  3. 次の更新が正しく行われるかどうかは$ sudo certbot renew --dry-runによって動作確認が可能です。

    1. 処理後にCongratulations, all simulated renewals succeededが表示されていれば正常です。

取得した証明書が格納されている場所は以下のとおりです。これは方法を問わず共通です。

証明書と中間証明書を連結したファイル
/etc/letsencrypt/live/ドメイン名/fullchain.pem;

秘密鍵
/etc/letsencrypt/live/ドメイン名/privkey.pem;

中間証明書
/etc/letsencrypt/live/ドメイン名/chain.pem

証明書
/etc/letsencrypt/live/ドメイン名/cert.pem

なお、/etc/letsencrypt/内には役割の異なる複数のディレクトリが存在しています。

/etc/letsencrypt/内のディレクトリ一覧

過去に取得した各証明書(cert,chain,fullchain,privkey)に番号+ドメインごとに格納(数字が多いもの程新しい)。
/etc/letsencrypt/archive/
例:/etc/letsencrypt/archive/ドメイン名/cert5.pem

archiveへのシンボリックリンクを行い、その中で最新の証明書にリンクさせている。
/etc/letsencrypt/live/
例:/etc/letsencrypt/live/ドメイン名/cert.pem(archive内の最新証明書)

証明書更新(renew)用の設定ファイルを格納している。
/etc/letsencrypt/renewal/
例:/etc/letsencrypt/renewal/ドメイン名.conf

ワイルドカード、localhostでの証明書取得

通常方法での証明書取得は非常に手軽ですが、ワイルドカードおよびlocalhostの場合はDNSを用いてチェックを行わなければなりません。

  1. コマンドは通常とは少し変わり、$ sudo certbot certonly --manual -d ドメイン名 --preferred-challenges dns-01を使います。
    1. ワイルドカードの場合は$ sudo certbot certonly --manual -d ドメイン名 -d *.ドメイン名 --preferred-challenges dns-01というようにサブドメインの部分にアスタリスク(*)を記述します。
1. 証明書の更新やセキュリティに関するメールを受信するためのメールアドレスを入力する。
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
メールアドレスを入力

2. 規約に同意するかどうか Agreeしないと次に進めない。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

3. メールアドレスをElectronic Frontier財団に登録するかどうか。
Yes(Y)で登録するとメールマガジンが配信される。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

4.  表示される※ランダムな文字列※を、ドメインのDNS設定でTXTレコードに保存します。
サブドメインの欄は _acme-challenge です。
※注意:TXTレコードがDNSに反映されていない時にEnterを押して進もうとした場合、
チェックが失敗するのでやり直しになります。
この場合、再度certbotを実行することにより、文字列が別物になってしまうことから、
再びTXTレコードに記入して待つ必要があります。
別のターミナルで"$ nslookup -type=txt _acme-challenge.ドメイン名 8.8.8.8"を実行すれば、
TXTレコードがDNSに反映されているかチェック可能です。
https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.ドメイン名 でチェックもOKです。

Account registered.
Requesting a certificate for ドメイン名

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.ドメイン名

with the following value:

※ランダムな文字列※

Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.ドメイン名
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

5. 証明書の取得が完了する。
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/ドメイン名/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/ドメイン名/privkey.pem
This certificate expires on 2022-01-01.
These files will be updated when the certificate renews.

NEXT STEPS:
- This certificate will not be renewed automatically. 
Autorenewal of --manual certificates requires the use of 
an authentication hook script (--manual-auth-hook) 
but one was not provided. 
To renew this certificate, repeat this same certbot command before 
the certificate's expiry date.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

  1. NEXT STEPS:と書かれた注意書きが表示されており、この証明書は自動的には更新されないようになっています。理由や自動更新の方法については次の項目で説明します。

DNS認証を用いた場合の自動更新(Freenom)

DNS認証を用いた証明書取得ではTXTレコードを毎回更新する必要があることから、通常の自動更新が利用できません。

つまり、手動で更新しなければならないので非常に手間がかかります。

スクリプトを組んで自動更新する方法もありますが、利用するサービスによって手法が異なります。

上記のような自動更新を使う場合には、ドメインを取得したサイトで適用されているDNSネームサーバを、自動更新に使うサービスのネームサーバへ移動させる必要があります。

例えばFreenomでドメインを取得したケースで考えると、FreenomのMy DomainsからManage Domainに移動し、Management Tools内のNameserversUse custom nameservers (enter below)を選んで移動したいDNSサービスのネームサーバを登録します。

ただし執筆時点では、Freenomで取得したドメイン(.cf, .ga, .gq, .ml, .tk)はCloudflareやClouDNSなどの無料で使えるDNSサービスからは弾かれています(本番環境で無料ドメインを使うケースは少ないとは思いますが)。

しかしながら、certbot-dns-freenomというPython製のプラグインを使用することで自動更新を行うことは可能です(Cloudflare版は後述)。

  1. 以前Certbotをインストールしていた場合は競合による不具合を避けるため、$ sudo apt remove certbotで既存のcertbotをアンインストールします。

    1. removeではなくpurgeを使った場合は証明書ごと消えるので注意。
  2. snapでもCertbotをインストールしていた場合には$ sudo snap remove certbotを実行してアンインストールします。

  3. Certbotにシンボリックリンクを設定していた場合には$ whereis certbotで場所を探し、$ sudo unlink リンクの場所でリンクも削除します。

  4. Certbot用のPython仮想環境を構築するために$ sudo apt install python3.8-venvでvenv(virtual environment)をインストールします。

  5. それが終われば$ sudo python3 -m venv /opt/certbot//opt/certbot/上に仮想環境を構築します。

  6. その後$ sudo /opt/certbot/bin/pip install --upgrade pipでpip(Pip Installs Packages)コマンドを最新にアップグレードしておきます。

  7. $ sudo /opt/certbot/bin/pip install certbotでCertbotをインストールします。

    1. $ certbot --versionで現在のCertbotのバージョンをチェックできます。
  8. インストールが完了したら$ sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbotによって/usr/bin/certbotにシンボリックリンクを作成しておきましょう。

    1. この場所に作っておくことでコマンド実行時にコマンドの場所を指定しなくて済みます。
  9. ここまで来ると通常のCertbotコマンドは使用可能になっていますが、FreenomのDNS認証用プラグインが必要なので、$ sudo /opt/certbot/bin/pip install certbot-dns-freenomを実行してインストールします。

  10. certbot-dns-freenomではfreenomにログインするための情報が必要となります。そのため、$ sudo vim /etc/letsencrypt/freenom_secret.iniでログイン情報を記述しておきます。

    1. 仮に/etc/letsencryptディレクトリが存在しない場合は$ sudo mkdir /etc/letsencryptでディレクトリを作成しておきましょう。

freenom_secret.ini

dns_freenom_username = freenomに登録したメールアドレス
dns_freenom_password = 上記に対するfreenomのログインパスワード
  1. freenom_secret.iniはログイン情報が入っているため、非常に大切なファイルです。なので$ sudo chmod 600 /etc/letsencrypt/freenom_secret.iniで権限を変更しておきましょう。

  2. あとはcertbot-dns-freenom専用のコマンドを実行するだけですが、これは少々長いので折り返しコマンド(\)で分けています(コピーすればまとめて実行可能)。

    1. これは--dry-runによってテスト実行を行っているため、正常に完了したらそのコマンドを削除してもう一度実行してください。
    2. このコマンドにはワイルドカードが入っていないので必要な方は-d *.ドメイン名というようにアスタリスク(*)を入れてください。
$ sudo certbot certonly --dry-run -a dns-freenom \
  --dns-freenom-credentials /etc/letsencrypt/freenom_secret.ini \
  --dns-freenom-propagation-seconds 330 \
  -d ドメイン名
  1. コマンドを実行中、DNSの反映を待つためにdns-freenom-propagation-secondsで330秒に設定しています(ただしFreenomでは変更が即座に反映されにくいため、場合によってはもっと秒数が必要になる場合もあります)。

  2. 無事証明書の発行に成功したら、あとは自動更新の設定を行うだけです。$ sudo vim /etc/crontabに追記します。

crontab

0 3 2.17 * * root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null

  1. 上記コマンドでは、毎月2日と17日の午前3時に証明書の更新を行うようにしています。

  2. 証明書取得時、/etc/letsencrypt/renewal/ドメイン名.confに取得時の設定が記載されているため、# certbot renewでも問題無く更新を行うことができます。

    1. 証明書は三ヶ月に一度更新すれば良いので、更新失敗したケースも考慮して月に2回更新作業を行います。
    2. 証明書の有効期限が30日以上では更新されずにスルーされるため、月2回でも特に問題はありません。

DNS認証を用いた場合の自動更新(Cloudflare)

このセクションではCloudflareを用いた無料自動更新の手順を紹介します。こちらの方法では有料ドメインの場合でのみ対応可能です(もともとはFreenomの無料ドメインをこちらを使う予定でしたが、最後の最後にエラーが発生して対応していない事に気付きました)。

Cloudflareのアカウントは既に登録してあることを前提として進めていきます

  1. ログイン後、Cloudflareでサイトを何も追加していなければYou currently don't have any websites.の下にAdd siteというボタンがあるため、それを選択して取得したドメイン名を追加します。

  2. その後、Select a planの項目が表示されますが、下部にFree $0がありますのでそちらを選んでContinueを選択します。

  3. Review your DNS recordsの項目が出てくるので現在のDNSの構成が正しければContinueを選んでください。

  4. Change your nameserversではネームサーバの変更を促されるため、ドメインを取得したサイト内にて現在のネームサーバからCloudflareが提供しているネームサーバへと変更を行います。

  5. 変更後は長くて24時間かかる場合がありますので、気長に待ってください。

    1. ネームサーバの反映チェックは$ nslookup -type=ns ドメイン名 8.8.8.8で確認可能です。チェック時にCloudflareのネームサーバが表示されていれば反映されています。
  6. 反映をチェックできたら、次は$ sudo apt-get install python3-certbot-dns-cloudflareでDNSを自動更新するためのパッケージをインストールします。

  7. 自動更新を行うにはCloudflareのAPI(Application Programming Interface)からGlobal API KeyViewすることでキーを取得する必要があります。

  8. キーを入手したら$ sudo vim /etc/letsencrypt/cloudflare_secret.iniで必要情報を追記します。

cloudflare_secret.ini

dns_cloudflare_email = Cloudflareに登録したメールアドレス
dns_cloudflare_api_key = 取得したGlobal API Key
  1. APIキーは重要な情報なので、$ sudo chmod 600 /etc/letsencrypt/cloudflare_secret.iniで権限(パーミッション)を変更しておきましょう。

  2. 一通りの作業が終わりましたので、以下のコマンドを実行します。こちらも少し長いので折り返しコマンド(\)で分けています(コピーすればまとめて実行可能)。

    1. ワイルドカード証明書を取得する場合は-d *.ドメイン名も追加してください。
$ sudo certbot certonly --dry-run --dns-cloudflare \
 --dns-cloudflare-credentials  /etc/letsencrypt/cloudflare_secret.ini \
 --dns-cloudflare-propagation-seconds 60 -d ドメイン名
  1. 無事テストが完了したら、--dry-runを外して証明書の取得を行うことが可能です。

  2. このままでは自動更新とはならないため、$ sudo vim /etc/crontabで定期更新を行うための設定を行います。

crontab

0 3 2.17 * * root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null

  1. Freenomの時と同じコマンドを用いています。

  2. 同じ説明を以下に掲載します。

    1. 上記コマンドでは、毎月2日と17日の午前3時に証明書の更新を行うようにしています。
    2. 証明書取得時、/etc/letsencrypt/renewal/ドメイン名.confに取得時の設定が記載されているため、# certbot renewでも問題無く更新を行うことができます。
    3. 証明書は三ヶ月に一度更新すれば良いので、更新失敗したケースも考慮して月に2回更新作業を行います。
    4. 証明書の有効期限が30日以上では更新されずにスルーされるため、月2回でも特に問題はありません。

TLS設定

証明書を無事取得できたら、Nginxの設定ファイルを編集します。

今回は比較的安全性の高いTLS設定だけでなく、一部のセキュリティヘッダーも含めた、全体的にセキュアな構成を作成します。

  1. $ sudo vim /etc/nginx/conf.d/ドメイン名.sh.confで新規に設定ファイルを編集します。
    1. これは記述量が多くなるためにファイルを別にしているのであって、問題なければドメイン名.conf内にある、本体のServerディレクティブ内に記述しても大丈夫です(TLSを使うServerディレクションには証明書をそれぞれ配置してください)。
    2. 証明書の取得直後、/etc/nginx/conf.d/ドメイン名.conf内に# managed by Certbotという文言が追加されている場合があります。その場合は一度それらをコメントアウトしてください(同じ項目が二重となるため)。
    3. /etc/nginx/nginx.confinclude /etc/nginx/conf.d/*.conf;を設定しているため、Nginxでサイトを複数管理している場合は証明書等でエラーが発生します。その場合は/etc/nginx/conf.d/ドメイン名.conf内に設定をすべて書き加えてもOKです。
    4. レポートにはreport-uri.comを用いています。

ドメイン名.sh.conf

# 一部設定をコメントアウトしていますが、サイトごとに調整してください
#
# TLS設定
#
# 証明書の場所を指定
ssl_certificate /etc/letsencrypt/live/ドメイン名/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ドメイン名/privkey.pem;

# キャッシュをsharedのみ使用する(10メガバイト分)
ssl_session_cache shared:SSL:10m;

# タイムアウト時間を5分に指定
ssl_session_timeout 5m;

# 前方秘匿性のためにセッションのキャッシュを保持させない
ssl_session_tickets off;

# 使用するTLSのバージョンを指定
ssl_protocols TLSv1.2 TLSv1.3;

# DH鍵交換に使用するパラメータファイルの場所を指定
ssl_dhparam /etc/ssl/certs/dhparam.pem;

# 楕円曲線暗号の種類を指定
ssl_ecdh_curve secp384r1;

# サーバが対応する暗号化スイートを指定
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

# TLS1.2からはクライアント側ハードウェアが優先暗号化方式を選択できるためoff
ssl_prefer_server_ciphers off;

# HSTSを常に有効化、期限を2年間に指定、サブドメインにも適用および先読みリストを使用
# add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# 証明書誤発行の防止および監視(CertificateTransparency)を1日間行い、違反した場合は接続拒否を指示する
# ただし現在はSigned Certificate Timestamp(SCT)の方が優先されており、LetsEncryptも対応済みなのでほぼ不要
# Expect-CT, SCT and Let's Encrypt
# https://community.letsencrypt.org/t/expect-ct-sct-and-lets-encrypt/142612
# add_header Expect-CT " max-age=86400, enforce, report-uri='https://アカウント名.report-uri.com/r/d/ct/reportOnly'" always;

# OCSP staplingを有効化
ssl_stapling on;

# CSCPの問い合わせ結果を検証する
ssl_stapling_verify on;

# 名前解決の際に使用するDNSサーバを指定、TTLキャッシュ時間を300秒に指定
# Google:8.8.8.8 8.8.4.4(安定性)
# Cloudflare 1.1.1.1 1.0.0.1(速度)
# Quad9 9.9.9.9 149.112.112.112(セキュリティ)
resolver 1.1.1.1 1.0.0.1 valid=300s;

# 名前解決のタイムアウト時間を指定
resolver_timeout 5s;

# OCSPの問い合わせに利用する証明書を指定(LetsEncryptで用意されている)
ssl_trusted_certificate /etc/letsencrypt/live/ドメイン名/chain.pem;

#
# Security Header 設定 (すべてにalwaysを付与する)
#
# Content Security Policy(CSP)設定
# CSPはよく調べて自サイトの構成ごとに設定する
# 以下は例
#add_header Content-Security-Policy "default-src 'none'; object-src 'none'; style-src 'self'; script-src 'self'; img-src 'self'; manifest-src 'self'; child-src 'self'; connect-src 'self'; prefetch-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; require-trusted-types-for 'script'; trusted-types default; upgrade-insecure-requests; block-all-mixed-content; report-uri https://アカウント名.report-uri.com/r/d/csp/enforce; report-to default" always;

# Reporting APIを有効化
# add_header Report-To "{'group':'default','max_age':31536000,'endpoints':[{'url':'https://アカウント名.report-uri.com/a/d/g'}],'include_subdomains':true}" always;

# Network Error Loggingを有効化
# add_header NEL "{'report_to':'default','max_age':31536000,'include_subdomains':true}" always;

# DNSの先読みを禁止し、DNSリクエスト発行による情報漏洩を防止する
add_header X-DNS-Prefetch-Control "off" always;

# クロスサイトスクリプティング(XSS)攻撃を検出した際、ページの読み込みを停止させ、レポートを送信する
add_header X-XSS-Protection "1; mode=block; report=https://アカウント名.report-uri.com/r/d/xss/enforce" always;

# ファイルのダウンロード時にファイルを直接実行させない(開かせない)
add_header X-Download-Options "noopen" always;

# iframeの制御を行い、クリックジャッキングを防止する
add_header X-Frame-Options "DENY" always;

# MIMEタイプが一致しない限りファイルを読み込みを拒否し、誤判定を利用したXSSなどの攻撃を防ぐ
add_header X-Content-Type-Options "nosniff" always;

# Adobe AcrobatおよびFlash関連のポリシーファイルを全て拒否し、クロスドメインアクセスを防止する
add_header X-Permitted-Cross-Domain-Policies "none" always;

# リファラを一切送らないことでURL情報を漏洩させない
add_header Referrer-Policy "no-referrer" always;

# アクセス時に指定した情報のキャッシュをクリアする
#add_header Clear-Site-Data "cache, cookies, storage, executionContents, *" always;

# 自サイトのコンテンツを外部で使用させない(CORP)
add_header Cross-Origin-Resource-Policy "same-origin" always;

# 埋め込むリソースに対しCORPを強制的に明示し許可していないリソースを読み込ませない(COEP)
add_header Cross-Origin-Embedder-Policy "require-corp" always;

# window.openerによるプロパティアクセスを防止する
add_header Cross-Origin-Opener-Policy "same-origin" always;

# ブラウザによる様々な特殊機能を制限する
# add_header Permissions-Policy "accelerometer=(),autoplay=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),geolocation=(),gyroscope=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(),publickey-credentials-get=(),screen-wake-lock=(),sync-xhr=(self),usb=(),web-share=(),xr-spatial-tracking=()" always;

# 一切のキャッシュを保存および共有せず、取得するコンテンツを常に新しいものとして扱う
# MDN を信じずに「Cache-Control」には「private」を含めよう
# https://srad.jp/comment/4073928
# add_header Cache-Control "private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0" always;

# 全ファイルのキャッシュ期限を7日に指定(静的ファイルはクエリ"?="で更新判定を行うと管理しやすい(htmlはlast-modifiedで判定))
add_header Cache-Control "public, max-age=604800" always;
expires 7d;

# HTTP1.0でのアクセス時にはキャッシュしない
add_header Pragma "no-cache" always;

# Cookieの付与をhttpsでの同一サイトのみに限定し、JavaScriptでのアクセスを禁止する
# add_header Set-Cookie "__Host-id=$request_id; Path=/; Secure; Samesite=Strict; HttpOnly" always;

# ウェブサイト内でCookieを無効化、Cookieを一切必要としないサイトなら設定しても可
# Is it possible to set up nginx without cookies?
# https://stackoverflow.com/questions/45356963/is-it-possible-to-set-up-nginx-without-cookies
# proxy_hide_header Set-Cookie;
# proxy_ignore_headers Set-Cookie;
# proxy_set_header Cookie "";

  1. 上記設定内で/etc/ssl/certs/dhparam.pemを記述していますが、現在このファイルは存在していないため、$ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096により、Diffie-Hellman鍵交換で使われる素数を格納しているファイルを出力します(CPUの性能により数分~数十分ほど時間がかかります)。

  2. それが終わりましたら、$ sudo vim /etc/nginx/conf.d/ドメイン名.confでサイト内の個別設定を行います。

ドメイン名.conf

# IP直打ちのアクセスを排除(IP直打ちを許可するならコメントアウト)
# defalut_serverを指定することでサーバの優先順位を明示する
server {
    listen   80 default_server;
    listen [::]:80 default_server; # ipv6接続設定

    listen   443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server; # ipv6接続設定

    # 無意味なサーバネームを指定することで未定義のHostをフィルタリングする
    server_name    _;

    # ステータスコード444はnginxがレスポンスを返さずに接続を閉じる
    return    444;
}

# 上記でdefault_serverを使いたくない場合はこちら
# こちらを使う場合は本体側のlistenにdefault_serverを加えておくことを推奨
# IP直打ちでのアクセスを排除
#server {
#    listen   80;
#    listen [::]:80; # ipv6接続設定

#    listen   443 ssl http2;
#    listen [::]:443 ssl http2; # ipv6接続設定

#    # サーバのIPを指定する
#    server_name サーバのIPアドレス;

#    # ステータス444を返す
#    return 444;
#}

# httpでの接続をwww有りのhttpsにリダイレクトする
server {
    listen   80;
    listen [::]:80;

    # server_name localhost;
    # www有りを使わないなら"www.ドメイン名" は消す
    server_name    ドメイン名 www.ドメイン名;
    return 301 https://ドメイン名$request_uri;
}

# www有りがサイトURLのデフォルトである場合
# www無しでの接続をwww有りhttpsにリダイレクトする
#server {
#    listen   443 ssl http2;
#    listen [::]:443 ssl http2;

#    server_name    www無しのドメイン名;

#    return 301 https://www.ドメイン名$request_uri;
#}

# リダイレクトを流される側(本体)の設定
server {
    # 接続するポート番号を指定
    listen    443 ssl http2;
    listen [::]:443 ssl http2;

    # URLに表示されるドメインまたはIPアドレス
    # server_name localhost;
    # server_name example.com;
    server_name    ドメイン名;

    # ウェブサイト表示時に参照されるディレクトリ
    root /var/www/ドメイン名;

    # indexページとして読み込むファイル
    index   index.php index.html index.htm;

    # 初期位置
    location / {
    }

    # 404ページの指定
    #error_page 404 /404.html;
    #    location = /404.html {
    #}

    # 50*ページの指定
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        # 50x.htmlを参照するルートを指定
        root   /usr/share/nginx/html;
    }

}


  1. すべての設定が完了したら$ sudo service nginx restartでNginxを再起動させます。

  2. サイトにアクセスした際にhttpsから始まるURLで表示されつつ、何もエラーが発生していないならば、無事HTTPS化の完了です。

次回は静的サイトジェネレータZolaの導入および設定を紹介

執筆時点では、今回のHTTPS構成だとSSL Labsの採点で満点近いスコアを出す事が可能です。

SSL Labsの採点に関してもう一点加えるならば、DNS CAAの設定するのも良いでしょう。

よりセキュアな構成を求める場合はCSP等の設定が求められますが、HTTPSに関しては今の所はこれで十分だと思われます。

CSPやセキュリティヘッダについての詳細は更に長くなってしまうため、別の記事で解説しようと考えています。

次回は当サイトでも採用している静的サイトジェネレータである、Zolaの導入と設定について紹介していきます。

参考資料