Let's EncryptでSSL対応したNginxのリバースプロキシを構築する

# 構築環境

```bash
$ cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
```

# 概要

[Kibana](https://www.elastic.co/jp/kibana)を構築したが、Kibanaへアクセスする際のURLが http://[ホスト名]:[ポート番号]/ を指定する必要があり、ポート番号を指定することが扱いづらいと感じた。そこで、Nginxによって5601ポートへのアクセスを、リバースプロキシすることにした。また、合わせて Let's Encrypt によるSSL対応を実施した。

以下は、今回のやりたいことのイメージ図である。

![リバースプロキシイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266508/39ea6047-0351-7c21-0c9b-a1219604f8d9.png)

なお、今回はDockerを使用してNginxのコンテナサービスを起動する。

# 実施内容

* [Certbotの設定](#Certbotの設定)
* [ファイアーウォールの設定](#ファイアーウォールの設定)
* [Dockerの設定](#Dockerの設定)
* [Nginxの設定](#Nginxの設定)
* [動作確認](#動作確認)
* [実装でハマったこと](#実装でハマったこと)

# Certbotの設定

## インストール

以下のコマンドを実行して、インストールする。[公式インストール手順](https://certbot.eff.org/lets-encrypt/centosrhel8-other)

```bash
$ wget https://dl.eff.org/certbot-auto
$ sudo mv certbot-auto /usr/local/bin/certbot-auto
$ sudo chown root /usr/local/bin/certbot-auto
$ sudo chmod 0755 /usr/local/bin/certbot-auto
```

### [Certbotとは？](https://certbot.eff.org/about/)

Certbotは無料のOSSで、手動で管理するウェブサイト上で、Let's Encrypt証明書を自動で利用し、HTTPSを有効化するものです。

## Let's Encrypt証明書の取得

以下のコマンドを実行して、Let's Encrypt証明書を取得する。

```bash
$ /usr/local/bin/certbot-auto certonly --standalone
```

緊急の更新およびセキュリティ通知に使用されるE-Mailアドレスの入力を求められるので、入力する。

```
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel):mail@exmaple.com
```

規約の同意を求められるので、同意する。

```
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel:A
```

ニュースやキャンペーン等のメールを送ってよいか？と聞かれる。申し訳ないが、今回はNoを選択する。

```
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing 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: N
```

ドメイン名の入力を求められるので、入力する。なお、『,』またな『スペース』で複数のドメインを入力することが可能である。

```
Please enter in your domain name(s) (comma and/or space separated)  (Enter 'c'
to cancel):www.example.com
```

Congratulations!と表示され、Let's Encrypt証明書の取得に成功した。

```
IMPORTANT NOTES:
 - 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
   Your cert will expire on 2020-08-17. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot-auto
   again. To non-interactively renew *all* of your certificates, run
   "certbot-auto renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - 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
```

上記の重要な注意事項に記載されている要点をまとめてみた。

|用語|説明（配置パスや有効期限）|
|---|---|
|証明書と中間証明書を連結したファイル|/etc/letsencrypt/live/www.example.com/fullchain.pem|
|秘密鍵|/etc/letsencrypt/live/www.example.com/privkey.pem|
|有効期限|2020-08-17|
|Certbot設定ファイル保存場所|/etc/letsencrypt|
|すべての証明書の更新コマンド|certbot-auto renew|

「有効期限が決まっているから、定期的に更新してね！」って説明されている。

## Let's Encrypt証明書の自動更新設定

Certbot公式が、cronによる定期的な自動更新を推奨しているため、以下のコマンドを実行してcrontabに自動更新ジョブを追加する。

```bash
$ echo "0 0,12 * * * root python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && /usr/local/bin/certbot-auto renew -q" | sudo tee -a /etc/crontab > /dev/null
```

自動更新ジョブがデフォルトのcrontabに追加されていることを確認する。

```bash
$ cat /etc/crontab
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name  command to be executed

0 0,12 * * * root python3 -c 'import random; import time; time.sleep(random.random() * 3600)' && /usr/local/bin/certbot-auto renew -q
```

# ファイアーウォールの設定

http,httpsの通信を許可するために、ポートを解放する。

```bash
$ firewall-cmd --add-service=http --permanent
$ firewall-cmd --add-service=https --permanent
$ firewall-cmd --reload
```

http,httpsのポートが解放されたことを確認する。

```bash
$ firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: cockpit dhcpv6-client http https
  ports: 10215/tcp
  protocols:
  masquerade: yes
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
```

# Dockerの設定

今回はDockerコンテナ上でNginxを動作させるため、予めDockerをインストールする

## インストール

インストール手順に関しては、別記事で紹介しているのでこちらを参照ください。

* [Dockerインストール手順](https://qiita.com/kenchan1193/items/78b58b5e293b7b5d5b05)
* [DockerComposeインストール手順](https://qiita.com/kenchan1193/items/b28ff451c08d981460ec)

# Nginxの設定

DockerComposeにて、Nginxのコンテナサービスを起動する手順を説明する。
なお、Dockerを使用せずにNginxをインストールする場合は、以下の手順を参照ください。

[Nginxインストール手順](https://qiita.com/kenchan1193/items/c25347c8c95ec0028ece)

## ディレクトリ構成

以下のディレクトリ構成で、構築する。

```bash
$ tree --charset=C
.
|-- conf
|   |-- default.conf
|   `-- nginx.conf
|-- docker-compose.yml
`-- html
    |-- 50x.html
    `-- index.html
```

## 各種設定ファイルの作成

DockerComposeよってコンテナサービスを起動するため、docker-compose.ymlを作成する。

```yml:docker-compose.yml
version: '3'
services:
  nginx:
    container_name: nginx
    image: nginx
    volumes:
      - /etc/letsencrypt/live/example.com/fullchain.pem:/etc/nginx/letsencrypt/fullchain.pem
      - /etc/letsencrypt/live/example.com/privkey.pem:/etc/nginx/letsencrypt/privkey.pem
      - /opt/docker/nginx/conf/default.conf:/etc/nginx/conf.d/default.conf
      - /opt/docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
      - /opt/docker/nginx/html:/usr/share/nginx/html
      - /var/log/nginx:/var/log/nginx
    ports:
      - 80:80
      - 443:443
    networks:
      - elasticstack
    restart: always

networks:
  elasticstack:
    external: true
```

Nginxの設定ファイルを作成する。

```nginx.conf:nginx.conf
user nginx;
worker_processes auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  512;
    multi_accept off;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    server_tokens off;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;
    #gzip           on;

    include /etc/nginx/conf.d/*.conf;
}
```

```nginx.conf:default.conf
server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;
    return       301 https://$host$request_uri;
}

server {
    listen       443 ssl http2;
    listen       [::]:443 ssl http2;
    server_name  _;

    ssl_certificate           /etc/nginx/letsencrypt/fullchain.pem;
    ssl_certificate_key       /etc/nginx/letsencrypt/privkey.pem;
    ssl_session_cache         shared:SSL:10m;
    ssl_session_timeout       10m;
    ssl_protocols             TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers               HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    keepalive_timeout         75;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    location /kibana/ {
        rewrite /kibana/(.*)$ /$1 break;
        proxy_pass                             http://example.com:5601;
        proxy_set_header Host                  $host;
        proxy_set_header X-Real-IP             $remote_addr;
        proxy_set_header X-Forwarded-Host      $host;
        proxy_set_header X-Forwarded-Server    $host;
        proxy_set_header X-Forwarded-For       $proxy_add_x_forwarded_for;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```

```html:index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

```html:50x.html
<!DOCTYPE html>
<html>
<head>
<title>Error</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>An error occurred.</h1>
<p>Sorry, the page you are looking for is currently unavailable.<br/>
Please try again later.</p>
<p>If you are the system administrator of this resource then you should check
the error log for details.</p>
<p><em>Faithfully yours, nginx.</em></p>
</body>
</html>
```

### Nginxの設定に関する補足

#### worker_processes

ワーカープロセスの数を定義する。最適な値はCPUコア数、ハードディスクドライブ数、負荷パターンを含む多くの要因によって異なるが、基本的には使用可能なCPUコア数に設定することが推奨されている（autoを設定することで、CPUコア数を自動で検出し設定してくれる）。なお、デフォルトは「1」が設定されている。

今回は汎用的に利用可能な設定ファイルを作成したかったため、autoを設定した。

#### worker_connections

ワーカープロセスで開くことのできる同時接続の最大数を定義する。この数には、クライアントの接続だけでなく、すべての接続（プロキシサーバとの接続など）が含まれる点は注意が必要である。また、同時接続の実際の数は、worker_rlimit_nofileで設定可能なオープンファイルの最大数の制限を超えることができない点にも注意が必要である。なお、デフォルトは「512」が設定されている。

今回はアクセス増加等を考える状況になかったため、一旦デフォルトの512を設定した。

#### multi_accept

multi_acceptが無効である場合は、ワーカープロセスは一度に一つの新しい接続しか受け入れることができない。一方で有効である場合は、ワーカープロセスは一度にすべての新しい接続を受け入れることができる。なお、デフォルトは「off」が設定されている。

今回は低スペックのサーバを使用しようしているため、offで設定した。

#### server_tokens

エラーページ及び「サーバ」応答ヘッダーフィールドにおけるnginxバージョン情報表示の有無を設定する。なお、デフォルトは「on」が設定されている。

今回は、バージョン情報が表示されてしまうとセキュリティ上良くないため、無効に設定した。

#### sendfile

コンテンツのファイルの読み込みとクライアントへのレスポンスの送信にsendfileを使用するかを設定する。sendfileを使用するとカーネル空間内でファイルの読み込みと送信が完了するため、効率良くファイルの内容をclientに送信することができる。なお、デフォルトは「off」が設定されている。

今回は、あまり深く考えずにonを設定した。

#### HTTPSサーバの最適化

SSL操作は追加のCPUリソースを消費する。最もCPUを集中的に使用する操作はSSLハンドシェイクです。このCPUリソースを消費を最小限に留めるには２つの方法があります。1つ目はキープアライブ接続で１つの接続を介して複数のリクエストを送信できるようにすることです。２つ目は、SSLセッションパラメータを再利用して、並列接続と後続接続のSSLハンドシェイクを回避する方法です。詳しくは[公式HTTPSサーバの最適化](http://nginx.org/en/docs/http/configuring_https_servers.html)を参照。

##### keepalive_timeout

キープアライブ接続のタイムアウトを設定する。デフォルトは「60s」が設定されている。

今回は特別こだわる要件はなかったが、75sで設定した。

##### ssl_session_cache

セッションパラメータを格納するキャッシュのタイプとサイズを設定する。なお、デフォルトは「none」であり、セッションパラメータをキャッシュに保存しない設定となっている。

今回はすべてのワーカープロセス間で共有されるキャッシュのタイプであるsharedを設定した。また、サイズは10mで設定した。

##### ssl_session_timeout

クライアントがセッションパラメータを際利用できる時間を指定する。なお、デフォルトは「5m」が設定されている。

今回は、10mで設定した。

## Nginxコンテナサービスの起動

DockerComposeによって、Nginxコンテナサービスを起動する

```bash
$ docker-compose up -d
```

# 動作確認

下記動作確認が期待通りであれば、完成！

1. **http://example.com/ にブラウザからアクセスする**
期待する動作：ブラウザ上に表示されたURLが https://example.com/ であり、「Welcome to nginx!」ページが表示されること
2. **https://example.com/ にブラウザからアクセスする**
期待する動作：ブラウザ上に表示されたURLが https://example.com/ であり、「Welcome to nginx!」ページが表示されること
3. **http://example.com/kibana/ にブラウザからアクセスする**
期待する動作：ブラウザ上に表示されたURLが https://example.com/kibana/ であり、Kibanaトップページが表示されること
4. **https://example.com/kibana/ にブラウザからアクセスする**
期待する動作：ブラウザ上に表示されたURLが https://example.com/kibana/ であり、Kibanaトップページが表示されること

# 実装でハマったこと

## Dockerコンテナ内のローカルホストに関して

Nginxの設定で、プロキシパスをローカルホストに設定したが、ホストOS上のlocalhostへ通信することができなかった。これは、コンテナ内で要求したlocalhostが、コンテナ内を示してしまうためである。

```nginx.conf
proxy_pass                             http://localhost:5601;
```

そこで今回は、プロキシパスをホストOSのIPアドレス（正確にはドメイン）を指定することで通信を実現した。

```nginx.conf
proxy_pass                             http://example.com:5601;
```

なお、Docker Desktop for Macに限った話ではあるが、Docker18.03移行では、host.docker.internalと呼ばれるホストOSのlocalhostへ通信可能な特別なDNS名が提供されている。

[host.docker.internalに関する公式の説明](https://docs.docker.com/docker-for-mac/networking/)

## KibanaのベースURLを変更する必要があった件

Kibanaのアクセスをリバースプロキシしたところ、上手く動作しなくなりました。原因はKibanaのhtmlが存在しないjavascriptファイルの呼び出しを行ってしまっていたためでした。

Kibanaのルートパスは、リバースプロキシでサブフォルダを指定したため http://example.com:5601/kibana/ がルートパスとなります。一方で、Kibanaのhtmlに書かれたjavascriptの呼び出しパスは、下記のようにルートパスとは異なっていたため、存在しないファイルとして扱われてしまい、上手く動作していませんでした。

```html
</script><script src="/kibana/bundles/app/kibana/bootstrap.js"></script>
```

この解決策として、[Kibanaの公式ドキュメントに](https://www.elastic.co/guide/en/kibana/master/settings.html)以下の記載があります。

> Enables you to specify a path to mount Kibana at if you are running behind a proxy. Use the server.rewriteBasePath setting to tell Kibana if it should remove the basePath from requests it receives, and to prevent a deprecation warning at startup. This setting cannot end in a slash (/).

つまり、server.basePathによってKibanaのベースURLを変更する必要があります。実際にkibana.ymlに対して以下の行を加えたところ、問題を解決することができました。

```yml:kibana.yml
server.basePath: "/kibana"
```

なお、Kibanaの構築手順に関しては以下の記事で紹介しています。

[ElasticStack環境をDocker上で構築して、「コロナ」に関するツイートをタグクラウド化した話](https://qiita.com/kenchan1193/items/9320390d48f3d2ae883c)