コンテナ化されたNode.jsアプリケーションをNginx、暗号化、Docker Composeで保護する方法

前書き

Node.jsアプリケーションの柔軟性とセキュリティを強化する方法は複数あります。 Nginxのようなreverse proxyを使用すると、リクエストの負荷分散、静的コンテンツのキャッシュ、およびTransport Layer Security(TLS)の実装が可能になります。 サーバーで暗号化されたHTTPSを有効にすると、アプリケーションとの通信が安全に保たれます。

コンテナにTLS / SSLを使用してリバースプロキシを実装するには、ホストオペレーティングシステムで直接作業する場合とは異なる一連の手順が必要です。 たとえば、サーバーで実行されているアプリケーションのLet’s Encryptから証明書を取得する場合は、必要なソフトウェアをホストに直接インストールします。 コンテナを使用すると、異なるアプローチを取ることができます。 Docker Composeを使用して、アプリケーション、Webサーバー、および証明書を取得できるようにするCertbot clientのコンテナーを作成できます。 これらの手順に従うことで、コンテナ化されたワークフローのモジュール性と移植性を活用できます。

このチュートリアルでは、Docker Composeを使用して、Nginxリバースプロキシを使用してNode.jsアプリケーションをデプロイします。 アプリケーションに関連付けられているドメインのTLS / SSL証明書を取得し、SSL Labsから高いセキュリティ評価を確実に受けられるようにします。 最後に、cronジョブを設定して証明書を更新し、ドメインの安全性を維持します。

前提条件

このチュートリアルを実行するには、次のものが必要です。

  • Ubuntu 18.04サーバー、sudo権限を持つroot以外のユーザー、およびアクティブなファイアウォール。 これらの設定方法のガイダンスについては、このInitial Server Setup guideを参照してください。

  • サーバーにインストールされたDockerおよびDocker Compose。 Dockerのインストールに関するガイダンスについては、How To Install and Use Docker on Ubuntu 18.04のステップ1と2に従ってください。 Composeのインストールに関するガイダンスについては、How To Install Docker Compose on Ubuntu 18.04のステップ1に従ってください。

  • 登録済みドメイン名。 このチュートリアルでは、全体を通してexample.comを使用します。 Freenomで無料で入手するか、選択したドメインレジストラを使用できます。

  • 次の両方のDNSレコードがサーバーに設定されています。 this introduction to DigitalOcean DNSをフォローして、DigitalOceanアカウントに追加する方法の詳細を確認できます(使用している場合)。

    • サーバーのパブリックIPアドレスを指すexample.comを含むAレコード。

    • サーバーのパブリックIPアドレスを指すwww.example.comを含むAレコード。

[[step-1 -—- cloning-and-testing-the-node-application]] ==ステップ1—ノードアプリケーションのクローン作成とテスト

最初のステップとして、Composeを使用してアプリケーションイメージをビルドするために使用するDockerfileを含むNodeアプリケーションコードを使用して、リポジトリのクローンを作成します。 最初に、リバースプロキシやSSLを使用せずに、docker run commandを使用してアプリケーションをビルドして実行することにより、アプリケーションをテストできます。

root以外のユーザーのホームディレクトリで、DigitalOcean Community GitHub accountからnodejs-image-demo repositoryのクローンを作成します。 このリポジトリには、How To Build a Node.js Application with Dockerで説明されているセットアップのコードが含まれています。

リポジトリをnode_projectというディレクトリに複製します。

git clone https://github.com/do-community/nodejs-image-demo.git node_project

node_projectディレクトリに移動します。

cd  node_project

このディレクトリには、Docker node:10 imageと現在のプロジェクトディレクトリの内容を使用してノードアプリケーションを構築するための手順を含むDockerfileがあります。 次のように入力して、Dockerfileの内容を確認できます。

cat Dockerfile
OutputFROM node:10-alpine

RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

WORKDIR /home/node/app

COPY package*.json ./

USER node

RUN npm install

COPY --chown=node:node . .

EXPOSE 8080

CMD [ "node", "app.js" ]

これらの手順では、プロジェクトコードを現在のディレクトリからコンテナにコピーし、npm installを使用して依存関係をインストールすることにより、ノードイメージを構築します。 また、プロジェクトのリストされた依存関係を含むpackage.jsonpackage-lock.jsonのコピーを、残りのアプリケーションコードのコピーから分離することにより、Dockerのcaching and image layeringを利用します。 最後に、この手順では、コンテナが非ルートnodeユーザーとして実行され、アプリケーションコードとnode_modulesディレクトリに適切な権限が設定されていることを指定しています。

このDockerfileとNodeイメージのベストプラクティスの詳細については、Step 3 of How To Build a Node.js Application with Dockerの完全な説明を参照してください。

SSLを使用せずにアプリケーションをテストするには、docker buildおよび-tフラグを使用してイメージをビルドおよびタグ付けします。 画像をnode-demoと呼びますが、別の名前を付けることもできます。

docker build -t node-demo .

ビルドプロセスが完了すると、docker imagesを使用してイメージを一覧表示できます。

docker images

次の出力が表示され、アプリケーションイメージのビルドが確認されます。

OutputREPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
node-demo           latest              23961524051d        7 seconds ago       73MB
node                10-alpine           8a752d5af4ce        3 weeks ago         70.7MB

次に、docker runでコンテナを作成します。 このコマンドには3つのフラグが含まれます。

  • -p:これにより、コンテナーのポートが公開され、ホストのポートにマップされます。 ホストではポート80を使用しますが、そのポートで別のプロセスを実行している場合は、必要に応じてこれを自由に変更してください。 これがどのように機能するかについての詳細は、port bindingに関するDockerドキュメントのこの説明を参照してください。

  • -d:これはコンテナをバックグラウンドで実行します。

  • --name:これにより、コンテナに覚えやすい名前を付けることができます。

次のコマンドを実行して、コンテナを構築します。

docker run --name node-demo -p 80:8080 -d node-demo

実行中のコンテナをdocker psで検査します。

docker ps

アプリケーションコンテナが実行されていることを確認する出力が表示されます。

OutputCONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
4133b72391da        node-demo           "node app.js"       17 seconds ago      Up 16 seconds       0.0.0.0:80->8080/tcp   node-demo

これで、ドメインにアクセスしてセットアップをテストできます:http://example.comexample.comを独自のドメイン名に置き換えることを忘れないでください。 アプリケーションには、次のランディングページが表示されます。

Application Landing Page

アプリケーションをテストしたので、コンテナーを停止してイメージを削除できます。 docker psを再度使用して、CONTAINER IDを取得します。

docker ps
OutputCONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
4133b72391da        node-demo           "node app.js"       17 seconds ago      Up 16 seconds       0.0.0.0:80->8080/tcp   node-demo

docker stopでコンテナを停止します。 ここにリストされているCONTAINER IDを、必ず独自のアプリケーションCONTAINER IDに置き換えてください。

docker stop 4133b72391da

停止したコンテナと、未使用の画像やぶら下がっている画像を含むすべての画像を、docker system prune-aフラグで削除できるようになりました。

docker system prune -a

出力でプロンプトが表示されたら%(​​t0)sと入力して、停止したコンテナーとイメージを削除することを確認します。 これによりビルドキャッシュも削除されることに注意してください。

アプリケーションイメージをテストしたら、Docker Composeを使用して残りのセットアップの構築に進むことができます。

[[step-2 -—- defining-the-web-server-configuration]] ==ステップ2—Webサーバー構成の定義

アプリケーションDockerfileを配置したら、Nginxコンテナーを実行するための構成ファイルを作成できます。 まず、ドメイン名、document root、プロキシ情報、およびCertbotの要求を.well-knownディレクトリに転送するためのロケーションブロックを含む最小限の構成から始めます。ここで、一時ファイルを配置して、それを検証します。ドメインのDNSはサーバーに解決されます。

まず、現在のプロジェクトディレクトリに構成ファイル用のディレクトリを作成します。

mkdir nginx-conf

nanoまたはお気に入りのエディターでファイルを開きます。

nano nginx-conf/nginx.conf

次のサーバーブロックを追加して、ユーザーリクエストをノードアプリケーションコンテナにプロキシし、Certbotのリクエストを.well-knownディレクトリに送信します。 必ずexample.comを独自のドメイン名に置き換えてください。

~/node_project/nginx-conf/nginx.conf

server {
        listen 80;
        listen [::]:80;

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;

        server_name example.com www.example.com;

        location / {
                proxy_pass http://nodejs:8080;
        }

        location ~ /.well-known/acme-challenge {
                allow all;
                root /var/www/html;
        }
}

このサーバーブロックにより、Nginxコンテナーをリバースプロキシとして起動し、Nodeアプリケーションコンテナーにリクエストを渡すことができます。 また、Certbotのwebroot pluginを使用して、ドメインの証明書を取得することもできます。 このプラグインはHTTP-01 validation methodに依存します。これは、HTTPリクエストを使用して、Certbotが特定のドメイン名に応答するサーバーからリソースにアクセスできることを証明します。

編集が完了したら、ファイルを保存して閉じます。 Nginxサーバーとロケーションブロックアルゴリズムの詳細については、Understanding Nginx Server and Location Block Selection Algorithmsに関するこの記事を参照してください。

Webサーバー構成の詳細が整ったら、docker-compose.ymlファイルの作成に進むことができます。これにより、アプリケーションサービスと、証明書の取得に使用するCertbotコンテナーを作成できます。

[[step-3 -—- creating-the-docker-compose-file]] ==ステップ3—Docker作成ファイルの作成

docker-compose.ymlファイルは、ノードアプリケーションやWebサーバーなどのサービスを定義します。 名前付きボリュームなどの詳細を指定します。これは、コンテナー間でSSL資格情報を共有するために重要であり、ネットワークおよびポート情報も同様です。 また、コンテナの作成時に実行する特定のコマンドを指定できます。 このファイルは、サービスの連携方法を定義する中心的なリソースです。

現在のディレクトリでファイルを開きます。

nano docker-compose.yml

最初に、アプリケーションサービスを定義します。

~/node_project/docker-compose.yml

version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped

nodejsサービス定義には、次のものが含まれます。

  • build:これは、Composeがアプリケーションイメージをビルドするときに適用される、contextおよびdockerfileを含む構成オプションを定義します。 Docker Hubなどのレジストリの既存のイメージを使用する場合は、代わりに、ユーザー名、リポジトリ、およびイメージタグに関する情報とともにimage instructionを使用できます。

  • context:これは、アプリケーションイメージビルドのビルドコンテキストを定義します。 この場合、それは現在のプロジェクトディレクトリです。

  • dockerfile:これは、Composeがビルドに使用するDockerfile(Step 1で確認したDockerfile)を指定します。

  • imagecontainer_name:これらはイメージとコンテナに名前を適用します。

  • restart:これは再起動ポリシーを定義します。 デフォルトはnoですが、コンテナが停止しない限り再起動するように設定しています。

このサービスにはバインドマウントが含まれていないことに注意してください。これは、セットアップが開発ではなく展開に重点を置いているためです。 詳細については、bind mountsおよびvolumesに関するDockerのドキュメントを参照してください。

アプリケーションとWebサーバーコンテナ間の通信を有効にするために、再起動定義の下にapp-networkというブリッジネットワークも追加します。

~/node_project/docker-compose.yml

services:
  nodejs:
...
    networks:
      - app-network

このようなユーザー定義のブリッジネットワークにより、同じDockerデーモンホスト上のコンテナー間の通信が可能になります。 これにより、アプリケーション内のトラフィックと通信が合理化されます。同じブリッジネットワーク上のコンテナ間のすべてのポートが開かれ、ポートが外部に公開されないためです。 したがって、フロントエンドサービスを公開するために必要なポートのみを開くことを選択できます。

次に、webserverサービスを定義します。

~/node_project/docker-compose.yml

...
 webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

nodejsサービスに対して定義した設定の一部は同じままですが、次の変更も加えました。

  • image:これは、Docker Hubから最新のAlpine-basedNginx imageをプルするようにComposeに指示します。 alpineイメージの詳細については、How To Build a Node.js Application with Dockerのステップ3を参照してください。

  • ports:これにより、ポート80が公開され、Nginx構成で定義した構成オプションが有効になります。

次の名前付きボリュームとバインドマウントも指定しました。

  • web-root:/var/www/html:これにより、web-rootというボリュームにコピーされたサイトの静的アセットがコンテナの/var/www/htmlディレクトリに追加されます。

  • ./nginx-conf:/etc/nginx/conf.d:これにより、ホスト上のNginx構成ディレクトリがコンテナ上の関連ディレクトリにバインドマウントされ、ホスト上のファイルに加えた変更がコンテナに反映されるようになります。

  • certbot-etc:/etc/letsencrypt:これにより、ドメインに関連するLet’sEncryptの証明書とキーがコンテナの適切なディレクトリにマウントされます。

  • certbot-var:/var/lib/letsencrypt:これにより、Let’sEncryptのデフォルトの作業ディレクトリがコンテナの適切なディレクトリにマウントされます。

次に、certbotコンテナーの構成オプションを追加します。 必ずドメインとメール情報を自分のドメイン名と連絡先メールアドレスに置き換えてください:

~/node_project/docker-compose.yml

...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email [email protected] --agree-tos --no-eff-email --staging -d example.com  -d www.example.com

この定義は、Docker Hubからcertbot/certbot imageをプルするようにComposeに指示します。 また、名前付きボリュームを使用して、certbot-etcのドメイン証明書とキー、certbot-varのLet’s Encrypt作業ディレクトリ、web-rootのアプリケーションコードなどのリソースをNginxコンテナと共有します。

ここでも、depends_onを使用して、webserverサービスの実行後にcertbotコンテナーを開始するように指定しました。

コンテナの起動時に実行するコマンドを指定するcommandオプションも含まれています。 これには、次のオプションを含むcertonlyサブコマンドが含まれています。

  • --webroot:これは、認証のためにwebrootプラグインを使用してファイルをwebrootフォルダーに配置するようにCertbotに指示します。

  • --webroot-path:これはwebrootディレクトリのパスを指定します。

  • --email:登録と回復のためのあなたの好みの電子メール。

  • --agree-tos:これは、ACME’s Subscriber Agreementに同意することを指定します。

  • --no-eff-email:これは、電子メールをElectronic Frontier Foundation(EFF)と共有したくないことをCertbotに通知します。 必要に応じて、これを省略しても構いません。

  • --staging:これは、Let’sEncryptのステージング環境を使用してテスト証明書を取得することをCertbotに通知します。 このオプションを使用すると、構成オプションをテストし、ドメイン要求の制限を回避できます。 これらの制限の詳細については、Let’s Encryptのrate limits documentationを参照してください。

  • -d:これにより、リクエストに適用するドメイン名を指定できます。 この場合、example.comwww.example.comを含めました。 これらを独自のドメイン設定に置き換えてください。

最後のステップとして、ボリュームとネットワークの定義を追加します。 ここのユーザー名は、必ず自分の非ルートユーザーに置き換えてください。

~/node_project/docker-compose.yml

...
volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge

名前付きボリュームには、Certbot証明書と作業ディレクトリのボリューム、およびサイトの静的アセットのボリュームweb-rootが含まれます。 ほとんどの場合、Dockerボリュームのデフォルトドライバーはlocalドライバーであり、Linuxではmount commandと同様のオプションを受け入れます。 このおかげで、アプリケーションの静的アセットを含むホスト上のviewsディレクトリを実行時にボリュームにマウントするdriver_optsを使用してドライバオプションのリストを指定できます。 その後、ディレクトリの内容をコンテナ間で共有できます。 viewsディレクトリの内容の詳細については、Step 2 of How To Build a Node.js Application with Dockerを参照してください。

終了すると、docker-compose.ymlファイルは次のようになります。

~/node_project/docker-compose.yml

version: '3'

services:
  nodejs:
    build:
      context: .
      dockerfile: Dockerfile
    image: nodejs
    container_name: nodejs
    restart: unless-stopped
    networks:
      - app-network

  webserver:
    image: nginx:mainline-alpine
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
    depends_on:
      - nodejs
    networks:
      - app-network

  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email [email protected] --agree-tos --no-eff-email --staging -d example.com  -d www.example.com

volumes:
  certbot-etc:
  certbot-var:
  web-root:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/views/
      o: bind

networks:
  app-network:
    driver: bridge

サービス定義を配置すると、コンテナを起動して証明書リクエストをテストする準備が整います。

[[ステップ-4 -—- ssl-certificates-and-credentialsの取得]] ==ステップ4—SSL証明書と資格情報の取得

コンテナはdocker-compose upで開始できます。これにより、指定した順序でコンテナとサービスが作成および実行されます。 ドメインリクエストが成功すると、出力に正しい終了ステータスが表示され、webserverコンテナの/etc/letsencrypt/liveフォルダに適切な証明書がマウントされます。

docker-compose upおよび-dフラグを使用してサービスを作成します。これにより、nodejsおよびwebserverコンテナーがバックグラウンドで実行されます。

docker-compose up -d

サービスが作成されたことを確認する出力が表示されます。

OutputCreating nodejs ... done
Creating webserver ... done
Creating certbot   ... done

docker-compose psを使用して、サービスのステータスを確認します。

docker-compose ps

すべてが成功した場合、nodejsおよびwebserverサービスはUpである必要があり、certbotコンテナーは0ステータスメッセージで終了します。

Output  Name                 Command               State          Ports
------------------------------------------------------------------------
certbot     certbot certonly --webroot ...   Exit 0
nodejs      node app.js                      Up       8080/tcp
webserver   nginx -g daemon off;             Up       0.0.0.0:80->80/tcp

nodejsおよびwebserverサービスのState列にUp以外のものが表示された場合、またはcertbot0以外の終了ステータスが表示された場合)sコンテナの場合は、必ずdocker-compose logsコマンドでサービスログを確認してください。

docker-compose logs service_name

これで、資格情報がdocker-compose execを使用してwebserverコンテナにマウントされていることを確認できます。

docker-compose exec webserver ls -la /etc/letsencrypt/live

リクエストが成功した場合、次のような出力が表示されます。

Outputtotal 16
drwx------ 3 root root 4096 Dec 23 16:48 .
drwxr-xr-x 9 root root 4096 Dec 23 16:48 ..
-rw-r--r-- 1 root root  740 Dec 23 16:48 README
drwxr-xr-x 2 root root 4096 Dec 23 16:48 example.com

リクエストが成功することがわかったので、certbotサービス定義を編集して、--stagingフラグを削除できます。

docker-compose.ymlを開きます:

nano docker-compose.yml

certbotサービス定義を含むファイルのセクションを見つけ、commandオプションの--stagingフラグを--force-renewalフラグに置き換えます。これにより、Certbotに次のことを通知します。既存の証明書と同じドメインを持つ新しい証明書を要求します。 certbotサービス定義は次のようになります。

~/node_project/docker-compose.yml

...
  certbot:
    image: certbot/certbot
    container_name: certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - webserver
    command: certonly --webroot --webroot-path=/var/www/html --email [email protected] --agree-tos --no-eff-email --force-renewal -d example.com -d www.example.com
...

これで、docker-compose upを実行して、certbotコンテナーとそれに関連するボリュームを再作成できます。 また、--no-depsオプションが含まれ、webserverサービスはすでに実行されているため、開始をスキップできることをComposeに通知します。

docker-compose up --force-recreate --no-deps certbot

証明書要求が成功したことを示す出力が表示されます。

Outputcertbot      | IMPORTANT NOTES:
certbot      |  - Congratulations! Your certificate and chain have been saved at:
certbot      |    /etc/letsencrypt/live/example.com/fullchain.pem
certbot      |    Your key file has been saved at:
certbot      |    /etc/letsencrypt/live/example.com/privkey.pem
certbot      |    Your cert will expire on 2019-03-26. To obtain a new or tweaked
certbot      |    version of this certificate in the future, simply run certbot
certbot      |    again. To non-interactively renew *all* of your certificates, run
certbot      |    "certbot renew"
certbot      |  - Your account credentials have been saved in your Certbot
certbot      |    configuration directory at /etc/letsencrypt. You should make a
certbot      |    secure backup of this folder now. This configuration directory will
certbot      |    also contain certificates and private keys obtained by Certbot so
certbot      |    making regular backups of this folder is ideal.
certbot      |  - If you like Certbot, please consider supporting our work by:
certbot      |
certbot      |    Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
certbot      |    Donating to EFF:                    https://eff.org/donate-le
certbot      |
certbot exited with code 0

証明書を配置したら、Nginx構成の変更に進んでSSLを含めることができます。

[[step-5 -—- modifying-the-web-server-configuration-and-service-definition]] ==ステップ5—Webサーバーの構成とサービス定義の変更

Nginx構成でSSLを有効にするには、HTTPリダイレクトをHTTPSに追加し、SSL証明書とキーの場所を指定する必要があります。 また、Perfect Forward Secrecyに使用するDiffie-Hellmanグループの指定も含まれます。

これらの追加を含めるためにwebserverサービスを再作成するので、ここで停止できます。

docker-compose stop webserver

次に、現在のプロジェクトディレクトリにDiffie-Hellmanキー用のディレクトリを作成します。

mkdir dhparam

openssl commandを使用してキーを生成します。

sudo openssl dhparam -out /home/sammy/node_project/dhparam/dhparam-2048.pem 2048

キーを生成するのに少し時間がかかります。

関連するDiffie-HellmanおよびSSL情報をNginx構成に追加するには、最初に以前に作成したNginx構成ファイルを削除します。

rm nginx-conf/nginx.conf

ファイルの別のバージョンを開きます。

nano nginx-conf/nginx.conf

次のコードをファイルに追加して、HTTPをHTTPSにリダイレクトし、SSL資格情報、プロトコル、およびセキュリティヘッダーを追加します。 example.comを独自のドメインに置き換えることを忘れないでください。

~/node_project/nginx-conf/nginx.conf

server {
        listen 80;
        listen [::]:80;
        server_name example.com www.example.com;

        location ~ /.well-known/acme-challenge {
          allow all;
          root /var/www/html;
        }

        location / {
                rewrite ^ https://$host$request_uri? permanent;
        }
}

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name example.com www.example.com;

        server_tokens off;

        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        ssl_buffer_size 8k;

        ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;

        ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
        ssl_prefer_server_ciphers on;

        ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

        ssl_ecdh_curve secp384r1;
        ssl_session_tickets off;

        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8;

        location / {
                try_files $uri @nodejs;
        }

        location @nodejs {
                proxy_pass http://nodejs:8080;
                add_header X-Frame-Options "SAMEORIGIN" always;
                add_header X-XSS-Protection "1; mode=block" always;
                add_header X-Content-Type-Options "nosniff" always;
                add_header Referrer-Policy "no-referrer-when-downgrade" always;
                add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
                # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
                # enable strict transport security only if you understand the implications
        }

        root /var/www/html;
        index index.html index.htm index.nginx-debian.html;
}

HTTPサーバーブロックは、.well-known/acme-challengeディレクトリへのCertbot更新要求のWebルートを指定します。 また、HTTPリクエストをルートディレクトリからHTTPSに転送するrewrite directiveも含まれています。

HTTPSサーバーブロックはsslhttp2を有効にします。 HTTP / 2がHTTPプロトコルを反復処理する方法と、HTTP / 2がWebサイトのパフォーマンスにもたらすメリットの詳細については、How To Set Up Nginx with HTTP/2 Support on Ubuntu 18.04の概要を参照してください。 このブロックには、最新のSSLプロトコルと暗号を使用し、OSCPステープルがオンになっていることを確認するための一連のオプションも含まれています。 OSCPステープリングを使用すると、最初のTLS handshakeの間にcertificate authorityからタイムスタンプ付きの応答を提供できるため、認証プロセスを高速化できます。

このブロックは、SSLおよびDiffie-Hellmanの資格情報とキーの場所も指定します。

最後に、プロキシパス情報をこのブロックに移動しました。これには、try_filesディレクティブを含むロケーションブロック、エイリアスされたNode.jsアプリケーションコンテナーへのリクエストの指定、セキュリティヘッダーを含むそのエイリアスのロケーションブロックが含まれます。これにより、SSL LabsSecurity HeadersサーバーテストサイトなどでAの評価を取得できるようになります。 これらのヘッダーには、X-Frame-OptionsX-Content-Type-OptionsReferrer PolicyContent-Security-Policy、およびX-XSS-Protectionが含まれます。 HTTP Strict Transport Security(HSTS)ヘッダーはコメント化されています—影響を理解し、その“preload” functionalityを評価した場合にのみ、これを有効にしてください。

編集が完了したら、ファイルを保存して閉じます。

webserverサービスを再作成する前に、HTTPSに関連するポート情報や、Diffie-Hellmanボリューム定義など、docker-compose.ymlファイルのサービス定義にいくつか追加する必要があります。

ファイルを開きます。

nano docker-compose.yml

webserverサービス定義で、次のポートマッピングとdhparam名前付きボリュームを追加します。

~/node_project/docker-compose.yml

...
 webserver:
    image: nginx:latest
    container_name: webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - web-root:/var/www/html
      - ./nginx-conf:/etc/nginx/conf.d
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - dhparam:/etc/ssl/certs
    depends_on:
      - nodejs
    networks:
      - app-network

次に、dhparamボリュームをvolumes定義に追加します。

~/node_project/docker-compose.yml

...
volumes:
  ...
  dhparam:
    driver: local
    driver_opts:
      type: none
      device: /home/sammy/node_project/dhparam/
      o: bind

web-rootボリュームと同様に、dhparamボリュームは、ホストに格納されているDiffie-Hellmanキーをwebserverコンテナーにマウントします。

編集が終了したら、ファイルを保存して閉じます。

webserverサービスを再作成します。

docker-compose up -d --force-recreate --no-deps webserver

docker-compose psでサービスを確認してください:

docker-compose ps

nodejsおよびwebserverサービスが実行されていることを示す出力が表示されます。

Output  Name                 Command               State                     Ports
----------------------------------------------------------------------------------------------
certbot     certbot certonly --webroot ...   Exit 0
nodejs      node app.js                      Up       8080/tcp
webserver   nginx -g daemon off;             Up       0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp

最後に、ドメインにアクセスして、すべてが期待どおりに機能していることを確認できます。 ブラウザをhttps://example.comに移動し、必ずexample.comを独自のドメイン名に置き換えてください。 次のランディングページが表示されます。

Application Landing Page

ブラウザのセキュリティインジケータにロックアイコンも表示されます。 必要に応じて、SSL Labs Server Test landing pageまたはSecurity Headers server test landing pageに移動できます。 含まれている構成オプションは、両方でサイトにAの評価を与える必要があります。

[[step-6 -—- renewing-certificates]] ==ステップ6—証明書の更新

Let's Encrypt証明書は90日間有効です。そのため、自動更新プロセスを設定して、失効しないようにしてください。 これを行う1つの方法は、cronスケジューリングユーティリティを使用してジョブを作成することです。 この場合、証明書を更新してNginx構成を再読み込みするスクリプトを使用して、cronジョブをスケジュールします。

プロジェクトディレクトリでssl_renew.shというスクリプトを開きます。

nano ssl_renew.sh

次のコードをスクリプトに追加して、証明書を更新し、Webサーバー構成を再読み込みします。

~/node_project/ssl_renew.sh

#!/bin/bash

/usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml run certbot renew --dry-run \
&& /usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml kill -s SIGHUP webserver

docker-composeバイナリの場所を指定することに加えて、docker-composeコマンドを実行するためにdocker-compose.ymlファイルの場所も指定します。 この場合、docker-compose runを使用してcertbotコンテナーを開始し、サービス定義で提供されるcommandを別のrenewサブコマンドでオーバーライドします。これは証明書を更新します。期限切れに近づいています。 スクリプトをテストするために、ここに--dry-runオプションを含めました。

次に、スクリプトはdocker-compose killを使用してSIGHUP signalwebserverコンテナーに送信し、Nginx構成を再ロードします。 このプロセスを使用してNginx構成を再ロードする方法の詳細については、this Docker blog post on deploying the official Nginx image with Dockerを参照してください。

編集が終了したら、ファイルを閉じます。 実行可能にします。

chmod +x ssl_renew.sh

次に、rootcrontabファイルを開いて、指定した間隔で更新スクリプトを実行します。

sudo crontab -e

このファイルを初めて編集する場合は、エディターを選択するよう求められます。

crontab

no crontab for root - using an empty one
Select an editor.  To change later, run 'select-editor'.
  1. /bin/ed
  2. /bin/nano        <---- easiest
  3. /usr/bin/vim.basic
  4. /usr/bin/vim.tiny
Choose 1-4 [2]:
...

ファイルの最後に、次の行を追加します。

crontab

...
*/5 * * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

これにより、ジョブ間隔が5分ごとに設定されるため、更新リクエストが意図したとおりに機能したかどうかをテストできます。 また、ジョブからの関連する出力を記録するために、ログファイルcron.logを作成しました。

5分後、cron.logをチェックして、更新要求が成功したかどうかを確認します。

tail -f /var/log/cron.log

更新が成功したことを確認する出力が表示されます。

Output- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
**          (The test certificates above have not been saved.)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Killing webserver ... done

crontabファイルを変更して、1日の間隔を設定できるようになりました。 たとえば、毎日正午にスクリプトを実行するには、ファイルの最終行を次のように変更します。

crontab

...
0 12 * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1

また、ssl_renew.shスクリプトから--dry-runオプションを削除することもできます。

~/node_project/ssl_renew.sh

#!/bin/bash

/usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml run certbot renew \
&& /usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml kill -s SIGHUP webserver

cronジョブは、Let’s Encrypt証明書が適格なときに更新することで、証明書が失効しないようにします。 set up log rotation with the Logrotate utilityを使用して、ログファイルを回転および圧縮することもできます。

結論

コンテナを使用して、NginxリバースプロキシでNodeアプリケーションをセットアップおよび実行しました。 また、アプリケーションのドメインのSSL証明書を保護し、必要に応じてこれらの証明書を更新するためにcronジョブを設定しました。

Let’s Encryptプラグインの詳細に興味がある場合は、Nginx pluginまたはstandalone pluginの使用に関する記事を参照してください。

次のリソースを参照して、Docker Composeの詳細を確認することもできます。

Compose documentationは、マルチコンテナアプリケーションについてさらに学ぶための優れたリソースでもあります。