コンテンツにスキップ
日本語

TLS と証明書

本番構成は立ち上げた時点で https です。docker compose up -d のあと、ウェブ画面は 443 番にあり、 80 番に来た平文のリクエストはそちらへ転送されます。 証明書は既定ではシステム自身が発行し、自前のものに差し替えることもできます。 このページでは証明書の出どころの選び方、CA を接続元のマシンへ配る手順、 すでに自社の ingress がある場合のつなぎ方、そして TLS と接続チャネルの wss が効いているかの確かめ方を扱います。

  • 外向きのホスト名ひとつ。.env の TLS_DOMAIN に書きます。社内 DNS かクライアントの hosts で引ければ十分で、公開登録されたドメインである必要はありません。
  • .env の PUBLIC_BASE_URL。同じホスト名の https の URL を指します。シングルサインオンのコールバックアドレスはこの値から組み立てます。
  • 自前の証明書を使うときは、証明書チェーンと秘密鍵(fullchain.pem と privkey.pem)。
  • 公開するポートは 443(https)と 80(http、転送のみ)の二つで、URL にポートは付きません。ホスト側で既にそれらのポートを使っている場合は .env の TLS_HTTPS_PORT と TLS_HTTP_PORT を別の組(よく使うのは 8443 と 8088)に変更し、そのときは URL にポートを付けます。

内蔵のプロキシは TLS 1.2 以上、HSTS、そしてターミナルとグラフィック接続に必要な WebSocket の upgrade 転送を提供します。 設定ファイル内のホスト名は起動時に TLS_DOMAIN から差し込まれるので、ファイル自体を編集する必要はありません。 アプリケーション側の受け入れも済んでいます。フロントエンドはページのプロトコルに合わせて ws と wss を切り替え、 アクセストークンは URL ではなく Authorization ヘッダーで渡します。

既定:システムが署名するローカル CA

Section titled “既定:システムが署名するローカル CA”

.env の TLS_MODE は出荷時 selfsigned です。初回起動時に tls/ へローカルの認証局とサーバー証明書を生成し、 以後の起動ではそれを使い続けます。証明書は TLS_DOMAIN のホスト名と、TLS_IP_SAN に並べたアドレスを含みます。 CA の秘密鍵はホストの tls/ca-private/ に残り、プロキシのコンテナには入りません。

.env
TLS_DOMAIN=jumper.example.internal
TLS_IP_SAN=10.0.0.5 # アドレスでも到達させるとき。カンマ区切り
PUBLIC_BASE_URL=https://jumper.example.internal

この三つが空のときは、bash scripts/quickstart.sh がマシンのホスト名とアドレスから埋め、 最後に開くべき URL と CA 証明書のダウンロード先を表示します。すでに入っている値はそのまま残ります。

Terminal window
docker compose up -d

続けて、CA 証明書を接続元のマシンへ配ります。

Terminal window
curl -sk https://jumper.example.internal/custodexa-ca.crt -o custodexa-ca.crt

Windows ドメインではグループポリシー(コンピューターの構成 → Windows の設定 → セキュリティの設定 → 公開キーのポリシー → 信頼されたルート証明機関)で配布し、macOS とモバイル端末では MDM の構成プロファイル、 単体のマシンでは手動でシステムの信頼されたルート証明書ストアに取り込みます。 配り終えれば、ブラウザはこのサイトを信頼された接続として表示します。 CA 証明書は公開してよいデータなので、そのまま利用者に渡せます。

公開 CA や組織の CA が発行した証明書が手元にあるときは、次のようにします。

  1. チェーンを tls/fullchain.pem、秘密鍵を tls/privkey.pem に置きます。
  2. .env で TLS_MODE=provided、TLS_DOMAIN(証明書のホスト名と揃えます)、 PUBLIC_BASE_URL=https://<ホスト名>(外向きのポートが 443 以外ならポートも付けます)を設定します。
  3. docker compose up -d。

どちらかのファイルが無いと、起動は証明書の準備の段階で止まり、足りないファイルの名前を示します。 プロキシが中途半端な設定のまま立ち上がることはありません。

Terminal window
docker compose logs tls-init

tls/ にすでにある証明書は、以後の起動でもそのまま使われます。入れ替えるときはファイルを消してから起動します。

ホスト側で秘密鍵と署名要求を作り、SAN にホスト名を入れて提出します。

Terminal window
openssl req -newkey rsa:2048 -nodes -keyout tls/privkey.pem \
-out custodexa.csr -subj "/CN=jumper.bnc.prod" \
-addext "subjectAltName=DNS:jumper.bnc.prod"

署名されたら、サーバー証明書と中間証明書を順に(リーフを先頭に)tls/fullchain.pem へつなぎ、 TLS_MODE を provided にして再起動します。 その CA に対するクライアント側の信頼は、通常 AD のグループポリシーか MDM で配ります。 社内の ID プロバイダーが同じ CA で https を話す場合は、CA 証明書をバックエンドのコンテナの信頼ストアに追加します。

すでに自社の ingress がある場合

Section titled “すでに自社の ingress がある場合”

前段にクラウドのロードバランサーや既存の nginx、Traefik がある構成では、overlay で内蔵プロキシを退かせ、 フロントエンドが自分で http のポートを公開して ingress から受けられるようにします。

Terminal window
docker compose -f docker-compose.yml -f docker-compose.external-ingress.yml up -d

.env に COMPOSE_FILE=docker-compose.yml:docker-compose.external-ingress.yml を書いておけば、 以後の up -d、ps、down に毎回 -f を付ける必要はありません。 この形ではフロントエンドが 80 を公開し(.env の HTTP_PORT で変更できます)、外向きの TLS は ingress が担います。 条件は TLS 1.2 以上、信頼された証明書、HTTP から HTTPS への転送、HSTS、そして WebSocket の upgrade 転送です。 ingress からこのホストまでの区間が信頼できないネットワークをまたぐなら、その区間も暗号化します。 PUBLIC_BASE_URL には利用者が実際に見る https の URL を入れます。 フロントエンドへ転送する Host ヘッダーには、利用者が実際に接続する外向きのポートを含めます(443 以外なら付けます)。 CORS_ALLOWED_ORIGINS を設定していないとき、バックエンドは同一オリジンのリクエストだけを受け付け、 その判定はリクエストの Origin と Host の比較で行うため、ポートの欠けた Host では資格情報付きのリクエストがクロスオリジンと見なされます。 TRUSTED_PROXIES には自社の ingress のアドレス、またはそれが属するネットワークを書きます。 これは各リクエストをどの送信元アドレスに帰属させるかを決め、監査記録に残るアドレスと、ログインのレート制限が数える鍵を決めます。 (内蔵プロキシを使う既定の形では bash scripts/quickstart.sh が Docker のサブネットを書き込みます。)

同梱のテンプレートは一般的な用途を賄います。変えたいときは複製し、.env から複製のほうを指し、元のファイルは触りません。

.env
cp docker/reverse-proxy/nginx-tls.conf.template tls/custodexa.conf.template
TLS_NGINX_TEMPLATE=./tls/custodexa.conf.template

変えられるのは server_name(既定では TLS_DOMAIN から入ります)、証明書のパス、追加のヘッダー (HSTS の max-age、includeSubDomains、preload は自社のドメイン方針に合わせます)、 そして upstream(同じ Docker ネットワークならサービス名 frontend:80)です。 docker compose up -d tls-proxy で反映します。

プロキシのコンテナを動かさず、ホストにある既存の nginx を使う場合は、上の ingress overlay でフロントエンドに http のポートを公開させ (マッピングを 127.0.0.1:8088:80 に変えて loopback に縛るのがおすすめです)、設定は同じテンプレートを流用し、 upstream を 127.0.0.1:8088 に向け、server_name と証明書のパスを自分のものに置き換えます。 nginx 1.25.1 より前のバージョンでは http2 on; の代わりに listen 443 ssl http2; と書きます。 ほかのプロキシ(Caddy、Traefik、クラウドのロードバランサー)も前節の条件を満たせば使えます。 docker/reverse-proxy/Caddyfile.example が同等の Caddy 設定です。

Terminal window
docker compose ps # tls-proxy が Up。tls-init は証明書を用意し終えると終了します
curl -sI http://localhost/ | head -1 # 301。https へ転送されます
curl -skI https://localhost/ | head -1 # 200
curl --cacert tls/ca-public/custodexa-ca.crt -sI https://<TLS_DOMAIN>/ | head -1 # 自己署名モードでは 200

四つめは CA 証明書で信頼の連鎖を検証します。200 が返れば証明書とホスト名が一致しています。 そのあとログインして接続をひとつ張ると、ブラウザの開発者ツールの Network タブに wss:// のストリームが見えます。

ログイン状態の再発行に使うトークンは HttpOnly cookie で運びます。ポリシーページの「接続とアカウント」の節に 「ログイン状態は https 接続でのみ保持する」というスイッチがあり、出荷時は有効です。有効なら cookie は暗号化された接続でしか送られず、 保存すれば次に発行する cookie から新しい値が使われます。再起動は要りません。

ポリシーにまだ値が無いときは、.env の AUTH_REFRESH_COOKIE_SECURE、PUBLIC_BASE_URL のプロトコル、出荷時の既定という順で初期値を取ります。 起動ログには効いている値とその出どころが出ます。

Terminal window
docker compose logs backend | grep "refresh cookie"

監査記録のタイムスタンプはホストの時計から取ります。本番ではホストで時刻同期サービスを有効にし、時刻源は UTC にしてください。

  • 通信セキュリティのページにあるチャネル台帳が、各チャネルの現在の暗号化状態を並べます。ウェブ層は導入側の管理と示し、基準からのずれも表示します。
  • 台帳はスナップショットとして書き出せます。スナップショットには生成の時刻と生成者が入り、書き出し自体も監査に残ります。
  • ポリシーのスイッチを変えると一件の記録が残り、変更者と変更の前後の値が入ります。