Let's Encrypt SSL証明書をリバースプロキシ用途のnginxに対して設定したい

# はじめに

Let's Encrypt の SSL 証明書を発行する手順は色々な人が記述してくれているが、やはり新規インストールを想定したものが多い。
今回、以下の状況を想定した場合にどうすればノーダウンタイムでSSL証明書を取得できるのかを検証したので、その内容を書き記す。

- nginx をリバースプロキシ用途で利用している (後ろに別のウェブサーバーがあり、ウェブコンテンツの実態は別のサーバーにある)
- 複数ドメインのリバースプロキシとして動かしているので、nginx を停止できない


## webrootモード

ノーダウンタイムでの更新は考慮されていて、そのために webroot モードがある。
webroot モードという言葉だけを聞くと少しわからなかったが、概略だけまとめると、

- 1. セットアップするサーバーの上でウェブサーバーアプリケーションが既に動いている
- 2. セットアップツールでサーバーの上に認証コンテンツを動的に作成する
- 3. 手順2.で作成されたコンテンツは、既に起動しているウェブサーバーアプリケーションに対するHTTPアクセスで取得することができるようになる
- 4. 外部(Let's Encrypt側のサーバー)から手順3.でコンテンツを取得することで、DNS経由で正しくサーバーにアクセスできていることを確認する

といった方法により、DNS/Serverの紐づけのチェックを行い、このチェックができた場合にSSL証明書を発行してくれる、といった流れになる。

より正確には、以下のページの HTTP-01 チャレンジを実施している、ということになる。

https://letsencrypt.org/ja/docs/challenge-types/

なので、webrootモードを利用する場合のコマンドのドキュメントには "ウェブサーバーのドキュメントルートを指定する" と書かれているが、より正確には `/.well-known/acme-challenge/` へアクセス可能なドキュメントルートを指定する、というのが正しい。

## 具体的な手順

### 注意点

以下の点には注意が必要。

- 既にDNSレコードの設定が行われていて、設定するサーバーのIPアドレスが十分に浸透していること
    - DNSキャッシュなどの影響から、レコードを書き換えてもすぐに反映されるわけではない点には注意
- 任意のサーバーからの HTTP/S アクセス (TCP:80/443) を許可しておくこと
    - ファイヤフォール (firewall-cmd や iptables) の設定、外部セキュリティグループの設定、ネットワーク構成による外部アクセスの到達不可能なサーバーへの設定、などはこの記事では取り扱わない。

### 設定手順


ここでは、`www.example.com` のSSL証明書を発行することとする。 また、 `/etc/nginx/letsencrypt` をチャレンジのためのドキュメントルートとして用意する。 
この場合、リバースプロキシで動作しているnginxに対して、以下の設定を行う。

```nginx.conf
    server {
        listen       80;  # HTTP チャレンジは HTTP (80) で受け入れる
        server_name  www.example.com;
        # 中略。既存設定がある場合は特に何もしない。

        location /.well-known/acme-challenge/ {
            # このロケーションを追加
            root /etc/nginx/letsencrypt;
        }
    }
```

```bash:コマンド実行
# 以下のコマンドをrootで実行
sudo su - 

# ドキュメントルートの製紙絵、必要なロケーションを追記、設定の検証、nginxへの設定反映
# パスはデフォルトインストールのものなので、環境ごとに置き換えてください
mkdir -p /etc/nginx/letsencrypt
vim /etc/nginx/nginx.conf
/usr/sbin/nginx -t 
systemctl reload nginx

# SSL証明書の発行
# Amazon Linux2 で検証した場合は certbot だったが、certbot-auto などがあればそちらでもOK
certbot --version   # 今回検証のバージョンは certbot 1.0.0 
certbot certonly --webroot -w /etc/nginx/letsencrypt/ -d www.example.com
```

以下のようなメッセージが出力されれば正常にSSL証明書は発行されている。

```
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/www.example.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/www.example.com/privkey.pem
```

あとは、これらのSSL証明書と秘密鍵を nginx の SSL server セクションに記述してやればよい。

```nginx.conf
    server {
        listen       443 ssl; # http2 などはプロジェクトに応じて設定
        server_name  www.example.com;

        # 今回取得したSSL証明書と秘密鍵を設定
        ssl_certificate "/etc/letsencrypt/live/www.example.com/fullchain.pem";
        ssl_certificate_key "/etc/letsencrypt/live/www.example.com/privkey.pem";

        # 後略
    }
```

最後に、間違いがないかを確認して設定を反映。

```bash
vim /etc/nginx/nginx.conf
/usr/sbin/nginx -t
systemctl reload nginx
```

これで `curl https://www.example.com/` としてアクセスし、200 OK が取得できればサイトのSSL化は完了。


## 補足: 複数サーバーの場合

この手順は1台のサーバーの場合はうまくいくが、複数台サーバーの場合はうまくいかない。 DNSで複数あるサーバーのいずれかに振り分けられるが、その内、コンテンツを提供しているのは certbot を実行した1台のサーバーのみだからである。

この場合、「サーバーコンテンツの取得に失敗した場合、その次のサーバーのコンテンツの取得を試す」という処理を upstream と try_files と [proxy_next_upstream](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream) の組み合わせで実現すればよい。

- 引用元URL - https://worklog.be/archives/3352

```nginx.conf
# https://worklog.be/archives/3352 から引用
upstream acme-challenge {
    server 192.168.0.1:80;
    server 192.168.0.2:80;
    server 192.168.0.3:80;
}
 
server {
 
    listen       80;
    # 中略
    location ^~ /.well-known/acme-challenge/ {
        root /home/www/letsencrypt;
        try_files $uri @acme-challenge;
    }
 
    location @acme-challenge {
        proxy_set_header Connection "";
        proxy_set_header HOST $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://acme-challenge;
        proxy_next_upstream http_404;
        proxy_intercept_errors on;
    }
 
}
```