🕌

Enabling https access on Nginx reverse proxy - シリーズ投稿4/7

2022/12/26に公開

Series Top: Dockerで作るおうちLAN遊び場

この投稿はシリーズ第四弾です。前回までの投稿で、Jupyter NotebookやGhostといったwebサービスをDockerで実行させ、更に手前にリバースプロキシを用意することで、DNS名で各サービスにアクセスできるようにしました。今回の投稿ではいくらか例として追加したものをクリーンアップし、httpsを有効にするなどリバースプロキシの設定を更に見ていきます。

以下の図は今回の投稿後のサービスアクセスのイメージ図です。リバースプロキシへのアクセスはhttpsとなり、その他すでにあるサービスや今後追加するサービスはhttpのまま配置できます。

また今回の投稿では更に作成するファイルが増えてくるため、投稿の最後にディレクトリやファイルのリストをまとめています。

Cleaning up examples used in the previous post

前回は2つ目のJupyter NotebookやGhostブログを例として追加しましたが、それらを止めて、ファイルおよびDNSレコードをクリーンアップしましょう。

まず$HOME/mylan/jupyterdocker compose downでコンテナを消し、docker-compose.ymlファイルからjupyter2を消しましょう。docker compose up -dでサービスを改めて走らせると、jupyter一つだけが実行されている状態になります。

もともと1つ目のjupyterを動かしていた時のままですが、以下がdocker-compose.ymlファイルの中身です。

services:
  jupyter:
    container_name: jupyter
    image: jupyter/base-notebook:notebook-6.5.1
    user: root
    ports:
      - "8888:8888"
    command: "start-notebook.sh --ServerApp.password='' --ServerApp.token='' --ip=0.0.0.0 --no-browser"
    environment:
      - "CHOWN_EXTRA=/home/jovyan"
      - "CHOWN_EXTRA_OPTS=-R"
    volumes:
      - type: volume
        source: jupyter_volume
        target: /home/jovyan
        volume:
          nocopy: true

volumes:
  jupyter_volume: {}

次にGhostに関してはdocker compose downでコンテナを消します。

最後に、Nginx向けの各サービス用サーバコンフィグファイルを変更します。ここでは例としてjupyter2.confjupyter2.conf.bakと変更し、ブログ用も同様にblog.conf.bakとします。そしてNginxのコンテナをリスタートするとファイルとしては残るものの、改名したサーバ設定は読み込まれないようになっています。docker exec rp nginx -Tを実行して確認できます。

クリーンアップの結果、ディレクトリやファイルは以下のようになっています。

$ cd $HOME/mylan
$ tree
.
 |-jupyter
 | |-docker-compose.yml
 |-ghost
 | |-docker-compose.yml
 |-rp
 | |-docker-compose.yml
 | |-conf.d
 | | |-jupyter.conf
 | | |-blog.conf.bak
 | | |-jupyter2.conf.bak
 |-dns
 | |-docker-compose.yml
 | |-config
 | | |-a-records.conf


## btw here I am actually using "find" and "sed" and making it alias
$ grep tree ~/.bash_aliases
alias tree='find . -not -path "*/\.git/*"| sed -e "s/[^-][^\/]*\// |/g" -e "s/|\([^ ]\)/|-\1/"'

Preparation for https access

Nginxサーバでhttps通信を扱えるようにするために必要なものはSSL/TLSのプライベートキーと証明書です。自身のドメインを持っているならば、そのドメイン用の証明書とキーを生成するのがベストです。シリーズの最初に触れましたが、私の実環境では購入したドメイン名とそれ用の証明書でサービスを走らせています。

Private CA and self-signed certificate

このシリーズではローカルのドメイン、mylan.localで引き続き構築を進めていきます。この場合はインターネット上で通用する証明書をletsencryptなどで発行してもらうことができないので、以下いろいろと用意していきます:

  1. 自身のCA, certificate authorityとしてプライベートキーと証明書を生成
  2. リバースプロキシで利用する証明書用のCSRとプライベートキーを生成
  3. CAの証明書・キーでCSRを署名して証明書を生成
  4. DHパラメータファイルを生成

別ディレクトリ、$HOME/mylan/opensslを用意してそこで一連の作業を進めていきます。

cd $HOME/mylan
mkdir openssl
cd openssl

まずはCAの証明書とキーの生成です。

# this will generate two files, crt and key
openssl req -x509 \
  -sha256 -days 3650 \
  -nodes \
  -newkey rsa:2048 \
  -subj "/CN=ca.mylan.local" \
  -keyout rootCA.key -out rootCA.crt

# here is the result
$ ls
rootCA.crt  rootCA.key

次に*.mylan.local用のCSRとプライベートキーの生成です。

# generate a CSR for "*.mylan.local"
# along with the private key for this CSR/certificate
openssl req -new \
  -newkey rsa:2048 -keyout tls.key -nodes \
  -out tls.csr \
  -subj "/CN=*.mylan.local"

# again, the result
$ ls
rootCA.crt  rootCA.key  tls.csr  tls.key

見ての通りopensslを用いて準備を進めているのですが、次にCSRに署名して証明書を生成する時に使うopensslの設定ファイルを用意します。

cert.confというファイルで用意し、中身は以下です。

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = *.mylan.local

ついにCSRに署名して実際にリバースプロキシが使う証明書を生成します。出力は省略していますが、openssl x509 -in tls.crt -text -nooutで証明書の中身が確認できます。

openssl x509 -req \
  -in tls.csr \
  -CA rootCA.crt -CAkey rootCA.key \
  -CAcreateserial -out tls.crt \
  -days 365 \
  -sha256 -extfile cert.conf

# to check the content of the signed certificate file
openssl x509 -in tls.crt -text -noout

ついでにDHパラメータファイルも生成します。

# generate DH parameter file as dhparam.pem
openssl dhparam -out dhparam.pem 2048

# the end result
$ ls
cert.conf  dhparam.pem  rootCA.crt  rootCA.key  rootCA.srl  tls.crt  tls.csr  tls.key

Configuring https

https通信を扱うのに必要なファイルの生成が終わりました。$HOME/mylan/rpディレクトリに戻りリバースプロキシサーバの準備を進めていきます。

https通信用のディレクトリとしてtlsを用意してそこに必要なファイルをopensslディレクトリよりコピーしてきます。

cd $HOME/mylan/rp
mkdir tls
cd tls
cp $HOME/mylan/openssl/tls.key .
cp $HOME/mylan/openssl/tls.crt .
cp $HOME/mylan/openssl/dhparam.pem .

このtlsディレクトリにコンフィグファイルtls.confも作ります。このtlsディレクトリはコンテナ内の/etc/nginx/tlsとして配置する予定なので、コンフィグファイル内のssl_dhparam, ssl_certificate, ssl_certificate_keyの向き先はコンテナ内ファイル配置に応じた設定を書いておきます。

ホストマシンからコンテナ内に積み込んで読み込ませるファイルが増えていきます。Pathの指定で混乱してくるかもしれませんが、実際にサービスを走らせているコンテナ内でどのファイルがどこに置かれていることになるのかをイメージして、頑張って整理してみてください。

# this is tls.conf file

# dhparam
ssl_dhparam /etc/nginx/tls/dhparam.pem;

# options
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
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";

# cert/key
ssl_certificate /etc/nginx/tls/tls.crt;
ssl_certificate_key /etc/nginx/tls/tls.key;

このrpサービスコンテナに新たにtlsディレクトリを載せるよう、docker-compose.ymlファイルを更新します。また同時にポートも443に変えます。

services:
  rp:
    container_name: rp
    image: nginx:1.23.2
    ports:
      - "443:443"
    volumes:
      - ./conf.d:/etc/nginx/conf.d
      - ./tls:/etc/nginx/tls

各サービス用サーバコンフィグファイルも更新します。クリーンアップしたのでjupyter分のみですが、$HOME/mylan/rp/conf.d/jupyter.confファイルを更新してtls.confを利用させるようにし、更に応答ポート、サービスも443にします。

server {
    listen 443 ssl http2;
    server_name jupyter.mylan.local;

    # docker resolver
    resolver 127.0.0.11 valid=30s;

    # tls
    include /etc/nginx/tls/tls.conf;

    location / {
        set $upstream 192.168.1.56:8888;
        proxy_pass http://$upstream;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

rpのDocker Composeをリスタートすると、ブラウザ上でhttps://jupyter.mylan.localにアクセスすることでhttpsでJupyter Notebookサービスにアクセスできるようになっています。

Adding root CA certificate on my client machine

ブラウザ上ではhttps通信に関して警告メッセージが出ているはずです。メッセージがすでに示していると思いますが、ブラウザはアクセス先のサーバが提示している証明書を信用していません。ブラウザは一般的に信頼して良いとされている機関の情報を持っており、今回用いている証明書はあくまでプライベートに、自分で署名した証明書であり、信用に値しないからです。

Opensslで生成したrootCA.crtファイルをスマホなりラップトップなりにコピーし、そのシステムが信用するroot CAに追加してください。方法はOSによってまちまちですが検索すれば出てきます。

なおもちろん、パブリックに通用するドメインでパブリックに通用する証明書、キーを準備して構築しているのであれば、ブラウザは元から信用してくれます。

Files I have so far

今回の投稿は以上で、締めとしてディレクトリやファイルのまとめを以下に残しておきます。クリーンアップをどうしたかで残っているディレクトリ、ファイルは変わると思いますが、ここまでで実行しているサービス分を全て含めるとこのようになっています。

$ tree
.

 |-jupyter  # Jupyter用のディレクトリ
 | |-docker-compose.yml  # Jupyter Notebookを実行、ポート8888

 |-openssl  # 証明書関連のディレクトリ
 | |-rootCA.crt  # ルートCA証明書
 | |-dhparam.pem  # ウェブサーバで用いるDHパラメータファイル
 | |-tls.key  # *.mylan.localサーバ証明書用のプライベートキー
 | |-rootCA.srl  # ルートCAのシリアルファイル、CSR署名時に生成される
 | |-cert.conf  # CSR署名時に使ったopensslの設定ファイル
 | |-tls.crt  # *.mylan.localサーバ証明書
 | |-rootCA.key  # ルートCAのプライベートキー
 | |-tls.csr  # サーバ証明書生成依頼に用いたCSR

 |-rp  # リバースプロキシサーバのディレクトリ
 | |-docker-compose.yml  # Nginxを実行、ポート443
 | |-conf.d
 | | |-jupyter.conf  # jupyter.mylan.local用のサーバコンフィグファイル
 | |-tls
 | | |-tls.conf  # https関連の設定ファイル
 | | |-dhparam.pem  # DHパラメータファイル
 | | |-tls.key  # 証明書のプライベートキー
 | | |-tls.crt  # 証明書

 |-dns  # DNSのディレクトリ
 | |-docker-compose.yml  # Unboundを実行、ポート53、TCP/UDP両方
 | |-config
 | | |-a-records.conf  # DNSレコード追加用のコンフィグファイル

next: Adding authentication for web service access

Discussion