cfssl を使って自己認証局による HTTPS 対応:Linux 使い(略)Advent Calendar 2024
はじめに
これは「Linux 使いになりたい人のための Advent Calendar 2024」の記事です。
筆者は、Web エンジニアを志望する人には、セルフホスト Git サービスを稼働させて利用することをオススメしています。もし Git を使ったことがないなら、Git を学ぶときに、セルフホスト Git サービスを稼働させることも視野に含めながら学習するのが効率的だと考えています。
cfssl を使って自己認証局による HTTPS 対応
前回はセルフホスト Git サービスを稼働させるにあたり、GitBucket の CI 用プラグインで CI/CD を実現する方法について説明しました。GitBucket では、あれだけ簡単に実現できた CI/CD も、GitLab + GitLab Runner といった環境で用意しようとなると、なかなか大変なことになります。
HTTP を使うのなら、あまり困らないのです。HTTPS を使うようになると、サーバー証明書の妥当性検証に対する設定が必要になってきます。技術的に正面から対応しようと考えると、自己認証局による HTTPS 対応をするのが妥当なので、ここではそうしてみます。
自己認証局を用意するには、openssl
コマンドを使うのが基本ですが、少し複雑な設定を理解する必要があり使うのが大変です。
ここで、HTTPS 対応は開発時にも必要なことがあるためか、次のように簡易的に用意するためのツールがいくつか世の中にあります。
今回は、機能が豊富な cloudflare/cfssl
を使ってみましょう。このツールは Docker イメージも用意されていますから、試用もしやすいです。
なお、ここではサーバー証明書、自己認証局といったものについての説明は省略させていただきます。HTTPS 対応するにあたって、ここでは「安全な通信を保証するために、そういうものが必要なのだ」程度に思っておいてかまいません。
きちんと理解するには、公開鍵方式での暗号化通信についての基本を理解してから、HTTPS による通信手順について理解するというのが良いでしょう。いずれにせよ、このあたりはきちんと専門的に解説しているものを参照していただいた方が確実です。
cfssl コマンド
Web サーバーの設定をしたことがある人は知っているのですが、HTTPS を利用できるように Web サーバーを設定するには、サーバー用の秘密鍵ファイルとユーザーへ提供するサーバー証明書ファイルが必要です。サーバー証明書ファイルについては、認証局というものによって署名されたものが必要です。
商用の Web サーバーでは、サーバー用の秘密鍵ファイルは、Web サーバー提供者が用意します。また、その人が、認証局へ署名されたサーバー証明書の発行を依頼します。
認証局は発行依頼に応じてサーバー証明書ファイルを Web サーバー提供者へ発行します。
Web サーバー提供者は、認証局からサーバー証明書ファイルを受理したら、それを Web サーバーへ設置して HTTPS 対応した Web サイトの公開をします。
Web サーバー提供者からの視点で一連の作業の流れを整理すると次のようになります。
- サーバー用の秘密鍵ファイル作成
- 認証局へサーバー証明書ファイルの発行依頼
- 認証局からサーバー証明書ファイルを受理
- Web サーバーにサーバー用の秘密鍵ファイルとサーバー証明書を設定
- Web サーバー公開
この作業の中で、cfssl
ツールを使って、次の処理ができます。
- サーバー用の秘密鍵ファイル作成
- サーバー証明書ファイルの発行依頼時に必要なファイルの作成
- 認証局により署名されたサーバー証明書ファイルの作成
ポイントは、ここでは自分が Web サーバー提供者の役割と、認証局の役割の両方を受け持つという点です。どの立場で作業しているかを意識しながら cfssl
ツールを使う必要があります。
cfssl 用 compose.yaml ファイル
さて、cfssl
コマンドを使うには、OS へ直接インストールすることもできますが、そんなにしょっちゅう使うツールというわけでもありません。環境構築のときだけ使えれば良いので、こういったものは Docker で用意するのが手軽です。
また、慣れてくると docker
コマンドで直接作業ができるのですが、慣れないうちは compose.yaml
ファイルを用意して作業するのが確実です。ということで、ここでは 次のディレクトリー構成でファイルを用意します。なお、これ以降、${CFSSL_DIR}
は cfssl ディレクトリーを表すとします。
cfssl/
├── ca-config.json ... CA(自己認証局)用設定ファイル
├── ca-csr.json ... CA 用キーペア作成時に使うファイル
├── compose.yaml ... cfssl コンテナー用
├── server.json ... サーバー証明書用ファイル
├── ca/ ... CA(自己認証局)用ファイル
│ ├── ca-key.pem
│ ├── ca.csr
│ └── ca.pem
└── server/ ... サーバー証明書関連ファイル
├── server-key.pem
├── server.csr
└── server.pem
また、ここでは次の cfssl
コマンド用の compose.yaml
ファイルを用意します。
name: cfssl
services:
cfssl:
image: 'cfssl/cfssl:v1.6.5'
container_name: 'cfssl'
hostname: 'cfssl.local.internal'
tty: true
entrypoint: /bin/bash
ここで使っている Docker イメージはデフォルトだと cfssl
コマンドが実行されるものなのですが、entrypoint: /bin/bash
を指定してコンテナーへアタッチして cfssl
コマンドを実行する方法で使うことにします。
起動は次のようにします。
docker compose up -d
CA 用ファイルの用意
それから、cfssl コンテナーへアタッチします。
docker compose -p cfssl exec cfssl bash
CA 用設定ファイルなどの雛形作成
コンテナー内では、次の手順で設定ファイルを作成します。これを雛形にして、自分が利用する設定ファイルを作成します。なお、cfssl.local.internal#
はコンテナーのプロンプトとします。
cfssl.local.internal# mkdir /cfssl && cd /cfssl
cfssl.local.internal# cfssl print-defaults config > ca-config.json
cfssl.local.internal# cfssl print-defaults csr > ca-csr.json
ファイル編集は Docker ホストでした方が楽なのと、コンテナー終了後も残しておく必要があるので、Docker ホストで次のコマンドを実行してコピーしておきます。
cd ${CFSSL_DIR}
docker compose -p cfssl cp cfssl:/cfssl/ca-config.json ./
docker compose -p cfssl cp cfssl:/cfssl/ca-csr.json ./
ca-config.json の用意
初期値だと有効期間が 8760h (365 日)なので、43800h (5年) へ変更します。ca-config.json
は次のようになります。
{
"signing": {
"default": {
"expiry": "43800h"
},
"profiles": {
"server": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth"
]
},
"client": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
},
"peer": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}
ca-csr.json の用意
CA 用のキーペアを作成するにあたって必要なファイルである ca-csr.json
は次のようにします。ここでは CN (Common Name) が ca.local.internal
の CA を作成することにしています。
{
"CN": "ca.local.internal",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "JP",
"L": "CA",
"O": "local.internal",
"ST": "Tokyo",
"OU": "local"
}
]
}
CA 用ファイルの生成
ファイルの用意ができたら、cfssl コンテナーへコピーしてコンテナー内のファイルを更新します。
docker compose -p cfssl cp ca-config.json cfssl:/cfssl/
docker compose -p cfssl cp ca-csr.json cfssl:/cfssl/
次に cfssl gencert
コマンドを使って CA 用の秘密鍵と公開鍵を作成します。CA 用の初期ファイルを作成する場合は -initca
オプションをつけます。
cfssl.local.internal# cd /cfssl
cfssl.local.internal# cfssl gencert -initca ca-csr.json | cfssljson -bare ca -
これで次のファイルが作成されます。
- ca-key.pem ... サーバー証明書署名時に使用
- ca.csr ... この後使いません
- ca.pem ... 他のマシンで認証局用にインポートするときに使用
ca-key.pem は CA 用の秘密鍵となります。厳重に保管しましょう。また、所有者のみ読み取りができるようにしておきます。コピー時にはファイルモードが変わらないよう注意しましょう。
どれも Docker ホストへコピーして大切に保管しておきます。ここでは Docker ホストで次のコマンドを実行します。まず、${CFSSL_DIR}/ca
ディレクトリーを作成して、そこへ生成したファイルをコピーしています。
cd ${CFSSL_DIR}
if [ ! -e ./ca/ ]; then mkdir ./ca/; fi
for f in ca-key.pem ca.csr ca.pem; do
docker compose -p cfssl cp cfssl:/cfssl/${f} ./ca/
done
サーバー証明書の用意
次にサーバー証明書の用意をします。
sever.json の雛形作成
cfssl コンテナーで次のコマンドを実行して、サーバー証明書用のファイルを生成するときに使う sever.json
の雛形を作成します。
cfssl.local.internal# cd /cfssl
cfssl.local.internal# cfssl print-defaults csr > server.json
これについても Docker ホストで編集するので、Docker ホストで次のコマンドを実行してコピーします。
cd ${CFSSL_DIR}
docker compose -p cfssl cp cfssl:/cfssl/server.json ./
server.json の編集
ここでは、sever.json
を編集して、次の内容としました。
{
"CN": "gitlab-ce-https.local.internal",
"hosts": [
"127.0.0.1",
"gitlab-ce-https.local.internal",
"gitlab-ce.local.internal",
"mailpit.local.internal",
"localhost",
"localhost:8443",
"localhost:8025",
"gitlab-ce-https.local.internal:8443",
"gitlab-ce.local.internal:8443",
"mailpit.local.internal:8025"
],
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "JP",
"ST": "Tokyo",
"L": "CA"
}
]
}
後で、gitlab-ce-https.local.internal
用に使うことを想定しています。また、複数のサーバー証明書を用意するのは面倒なので、https://gitlab-ce-https.local.internal:8443
や https://mailpit.local.internal:8025
などでも使えるようにしてあります。これらは Docker で動かすことを想定しているので localhost
についても対応できるようにしてあります。
サーバー証明書関連ファイルの生成
ファイルを用意したら cfssl コンテナーへコピーします。
cd ${CFSSL_DIR}
docker compose -p cfssl cp ./server.json cfssl:/cfssl/
それから cfssl コンテナーで次のコマンドを実行します。
cfssl.local.internal# cd /cfssl
cfssl.local.internal# cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=server \
server.json \
| cfssljson -bare server
これで次のファイルが生成されます。
- server-key.pem ... サーバー用秘密鍵
- server.csr ... この後使いません。
- server.pem ... サーバー証明書
これらも Docker ホストへ保存するために、次のコマンドを Docker ホストで実行します。
cd ${CFSSL_DIR}
if [ ! -e ./server/ ]; then mkdir ./server/; fi
for f in server-key.pem server.csr server.pem; do
docker compose -p cfssl cp cfssl:/cfssl/${f} ./server/
done
生成されたファイルの確認
なお、生成されたファイルについて内容を確認するには次のように openssl
コマンドを実行します。
cfssl.local.internal# cd /cfssl
cfssl.local.internal# openssl x509 -in ca.pem -text -noout
cfssl.local.internal# openssl x509 -in server.pem -text -noout
作業が終了したら cfssl コンテナーは削除します。Docker ホストで docker compose down
コマンドを使います。
docker compose -p cfssl down
サーバー証明書の更新
サーバー証明書は有効期限を過ぎると更新が必要です。その場合は次のように cfssl コンテナーを起動して、必要なファイルを Docker ホストからコピーします。それから、サーバー証明書の作成時と同じように cfssl
コマンドを実行します。
ここでは、Docker ホストで作業する前提でコマンドを表記しました。
docker compose -p cfssl exec cfssl sh -c "if [ ! -e /cfssl ]; then mkdir /cfssl; fi"
for f in ca/ca.pem ca/ca-key.pem ca-config.json server.json; do
docker compose -p cfssl cp ./${f} cfssl:/cfssl/
done
docker compose -p cfssl exec --workdir=/cfssl cfssl sh -c '\
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=server \
server.json \
| cfssljson -bare server \
'
for f in server-key.pem server.csr server.pem; do
docker compose -p cfssl cp cfssl:/cfssl/${f} ./server/
done
これをスクリプトにしておくと、ワンラインで更新されたサーバー証明書関連のファイルが手に入ります。
おわりに
今回は、cfssl
コマンドを使って簡易的な自己認証局を用意する方法と、それを使ったサーバー証明書関連ファイルの作成方法について説明しました。
最近は Docker や cfssl
プロジェクトのおかげで、以前より、かなり簡単に HTTPS 対応がしやすくなりました。
ただし、HTTPS 対応した Web サーバーへアクセスするクライアント側で自己認証局の証明書を登録するなどの作業が必要となり、Docker を使う場合はそこが少し課題となることがあります。とはいえ、対応が必要なパターンはそれほど多くないので、複雑なことをしなければ難しくありません。
HTTPS 対応については、インターネット環境があって Let's encrypt などの無償のサーバー証明書を取得可能な環境があれば、それを使って対応すれば済むのですが、LAN 環境用の Web サーバーを用意する場合は簡単な対応方法がありません。
LAN 環境で Web ブラウザが警告を出さないように HTTPS 対応の Web サーバーを用意する場合は、自己認証局を用意して環境整備するのが結局の所は確実だと個人的には考えています。
Web サービスの開発をしていたら、ローカルの環境で HTTPS 対応が必要になることがたまにはあります。そういうときに慌てないためにも、セルフホスト Git サービスを稼働させるついでに、関連技術を理解しておくのが良いのではないでしょうか。
Discussion