「オレだよオレ、信用してくれよ」とブラウザにいうための自己証明書を作り、自己認証局をOSに登録します。
httpsアクセスするのはもちろん、TCP通信でもTLSを使いたいため作ります。というのも、Traefikでホスト名を使ってリバースプロキシしますが、TCP通信の場合はSSL/TLSで暗号化されていないとホスト名を取れないからです。
本章ではいわゆるオレオレ認証局とオレオレ証明書を作成するワンショットのコンテナを構築します。
セキュリティに関わるところだし、自前でopensslコマンドをたたくことで理解を深める目的で作成しました。
一般的な方法がよければ、mkcertというものもあるので、そちらを使ってもいいでしょう。
こちらのサイトを参考にさせていただきました。
本章はarkbig/devbaseリポジトリのsslcertフォルダの説明になります。
📂 devbase/
├── 📄compose.override.yaml(必要なら作る)
├── 📄compose.yaml
└── 📂sslcert/
├── 📄Dockerfile # opensslを実行するコンテナ作成指示書
├── 📄entrypoint.sh # コンテナ起動時に実行するヘルパースクリプト
├── 📄generatecerts.sh # opensslを使用して証明書を作るスクリプト
└── 📁.certs # ここに証明書を作る(自分で作るフォルダ)
ファイルの関連図
Dockerfile
まずはコンテナのためのイメージを作るDockerfileです。
FROM ubuntu
#---------------(1)--------------#
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends \
gosu \
openssl \
&& apt-get -y clean \
&& rm -rf /var/lib/apt/lists/*
#---------------(2)--------------#
ENV CONTAINER_UID=${CONTAINER_UID:-1000}
ENV CONTAINER_GID=${CONTAINER_GID:-1000}
RUN groupadd -g ${CONTAINER_GID} -o castaff \
&& useradd -g castaff -m -o -u ${CONTAINER_UID} castaff \
&& mkdir /certs \
&& chown $UID:$GID /certs
VOLUME ["/certs"]
WORKDIR /home/castaff/
#---------------(3)--------------#
COPY entrypoint.sh generatecerts.sh ./
RUN chmod +x ./entrypoint.sh ./generatecerts.sh
ENTRYPOINT ["./entrypoint.sh"]
CMD ["./generatecerts.sh"]
大きく3つのパートに分かれています。
- OSのパッケージマネージャーを使って必要なソフト
gosu
、openssl
をインストール - コマンド実行用のアカウント
castaff
を作成 - 実行するコマンドをコンテナへコピー&設定
特筆するとしたら、2番目のアカウント作成部分でしょうか。作成する証明書ファイルはバインドマウントしたホスト側のフォルダを想定しています。そのため、コンテナ内のUIDをホスト側のUIDに合わせる必要があるのでこの処理を行っています。実際のidはentrypoint.shで動的に変更します。
entrypoint.sh
次にコンテナ実行のヘルパースクリプト(ENTRYPOINT)です。
#!/usr/bin/env sh
#====================================================================
# begin of 定型文
# このスクリプトを厳格に実行
set -eu
# set -eux
# 環境に影響を受けないようにしておく
umask 0022
# PATH='/usr/bin:/bin'
IFS=$(printf ' \t\n_')
IFS=${IFS%_}
export IFS LC_ALL=C LANG=C PATH
# end of 定型文
#--------------------------------------------------------------------
#---------------(1)--------------#
# UID,GIDを合わせる
uid=$(stat -c "%u" .)
gid=$(stat -c "%g" .)
ug_name=castaff
if [ "${CONTAINER_GID}" != "${gid}" ]; then
groupmod -g "${CONTAINER_GID}" -o "${ug_name}"
chgrp -R "${CONTAINER_GID}" .
fi
if [ "${CONTAINER_UID}" != "${uid}" ]; then
usermod -g "${ug_name}" -o -u "${CONTAINER_UID}" "${ug_name}"
fi
#---------------(2)--------------#
if [ "$(id -u)" = "${CONTAINER_UID}" ]; then
exec "$@"
else
# ユーザー変更してコマンド実行
exec /usr/sbin/gosu "${ug_name}" "$@"
fi
大きく2つのパートに分かれています。
- UID,GIDをホストに合わせる
- コマンド実行
1番目の処理はホスト側のUID/GIDと合わせるための変更をしています。
2番目のコマンド実行はユーザーを切り替えています。
gosu
はsudo
の代わりで、コンテナ内ではTTY関連で問題が起きるためsudo
の利用は避けたほうがいいようです。
generatecerts.sh
そしてsslcertのメイン処理である証明書の作成スクリプトです。
#!/usr/bin/env sh
#====================================================================
# begin of 定型文
# このスクリプトを厳格に実行
set -eu
# set -eux
# 環境に影響を受けないようにしておく
umask 0022
# PATH='/usr/bin:/bin'
IFS=$(printf ' \t\n_')
IFS=${IFS%_}
export IFS LC_ALL=C LANG=C PATH
# end of 定型文
#--------------------------------------------------------------------
#---------------(1)--------------#
#====================================================================
# begin of ユーザー設定のための環境変数
# 出力先フォルダ - マウントすることで、コンテナ外部に出力される
: "${CERTS_OUT:=/certs}"
# 自己認証局の設定
: "${CA_CN:="My Test"}" # Common Name
ca_cn_filebody=$(echo "ca-${CA_CN}" | sed -e 's@[ \\/:*?"<>|]@-@g')
: "${CA_FILEBODY:=${ca_cn_filebody}}"
: "${CA_CERT:="${CA_FILEBODY}.cer"}" # これをOSに登録する
: "${CA_KEY:="${CA_FILEBODY}.key"}"
: "${CA_PASS="${CA_FILEBODY}.pass"}" # set empty if no password
# CA_SUBJはオプションで、/C=国コード/ST=県/O=組織名/OU=部門名などが指定できます。(/CN=${CA_CN}が自動付与されます)
: "${CA_SUBJ:="/CN=${CA_CN}"}"
case "${CA_SUBJ%/CN=*}" in
"${CA_SUBJ}") CA_SUBJ=${CA_SUBJ}/CN=${CA_CN} ;;
esac
# 自己証明書の設定
: "${SSL_CN:="dev.test"}" # 今の時代あまり意味はない
# 今はSANsが使われる
# 対象のドメインをDNS:〜、対象のIPアドレスをIP:〜で指定する
# DNS:*.exampleのようにTLDだけのワイルドカードは今のブラウザは信用してくれない
: "${SSL_SANS:="subjectAltName=DNS:test,DNS:dev.test,DNS:*.dev.test,DNS:localhost,DNS:dev.localhost,DNS:*.dev.localhost,IP:127.0.0.1"}"
ssl_cn_filebody=$(echo "ssl-${SSL_CN}" | sed -e 's@[ \\/:*?"<>|]@-@g')
: "${SSL_FILEBODY:=${ssl_cn_filebody}}"
: "${SSL_CERT:="${SSL_FILEBODY}.cer"}" # これをサーバーに設定(公開鍵)
: "${SSL_KEY:="${SSL_FILEBODY}.key"}" # これもサーバーに設定(秘密鍵)
: "${SSL_CSR:="${SSL_FILEBODY}.csr"}"
: "${SSL_PASS=''}" # default is no password
: "${SSL_SERIAL:="${SSL_FILEBODY}.srl"}"
# SSL_SUBJはオプションで、/C=国コード/ST=県/O=組織名/OU=部門名などが指定できます。(/CN=${SSL_CN}が自動付与されます)
: "${SSL_SUBJ:="/CN=${SSL_CN}"}"
case "${SSL_SUBJ%/CN=*}" in
"${SSL_SUBJ}") : SSL_SUBJ="${SSL_SUBJ}/CN=${SSL_CN}" ;;
esac
# end of ユーザー設定のための環境変数
#--------------------------------------------------------------------
#---------------(2)--------------#
#====================================================================
# begin of 認証局
# 認証局のパスワード作成
if [ -z "${CA_PASS}" ]; then
echo "<---- Do NOT use CA passphrase ---->"
ca_passout_args=
ca_passin_args=
elif [ -e "${CERTS_OUT}/${CA_PASS}" ]; then
echo "<---- Using existing CA Password ${CA_PASS} ---->"
ca_passout_args="-aes256 -passout file:${CERTS_OUT}/${CA_PASS}"
ca_passin_args="-passin file:${CERTS_OUT}/${CA_PASS}"
else
echo "<==== Generating new CA Password ${CA_PASS} ====>"
ca_passout_args="-aes256 -passout file:${CERTS_OUT}/${CA_PASS}"
ca_passin_args="-passin file:${CERTS_OUT}/${CA_PASS}"
openssl rand -base64 -out "${CERTS_OUT}/${CA_PASS}" 32
chmod 400 "${CERTS_OUT}/${CA_PASS}"
fi
# 認証局の秘密鍵作成
if [ -e "${CERTS_OUT}/${CA_KEY}" ]; then
echo "<---- Using existing CA Key ${CA_KEY} ---->"
else
echo "<==== Generating new CA Key ${CA_KEY} ====>"
# shellcheck disable=SC2086
openssl genrsa -out "${CERTS_OUT}/${CA_KEY}" ${ca_passout_args} 2048
chmod 400 "${CERTS_OUT}/${CA_KEY}"
fi
# 認証局の自己証明書作成 --
if [ -e "${CERTS_OUT}/${CA_CERT}" ]; then
echo "<---- Using existing CA Cert ${CA_CERT} ---->"
else
echo "<==== Generating new CA Cert ${CA_CERT} ====>"
# shellcheck disable=SC2086
openssl req -new -x509 -days 36500 -key "${CERTS_OUT}/${CA_KEY}" ${ca_passin_args} -subj "${CA_SUBJ}" -out "${CERTS_OUT}/${CA_CERT}"
fi
# end of 認証局
#--------------------------------------------------------------------
#---------------(3)--------------#
#====================================================================
# begin of 証明書
# 証明書のパスワード作成
if [ -z "${SSL_PASS}" ]; then
echo "<---- Do NOT use SSL passphrase ---->"
ssl_passout_args=
ssl_passin_args=
elif [ -e "${CERTS_OUT}/${SSL_PASS}" ]; then
echo "<---- Using existing SSL Password ${SSL_PASS} ---->"
ssl_passout_args="-aes256 -passout file:${CERTS_OUT}/${SSL_PASS}"
ssl_passin_args="-passin file:${CERTS_OUT}/${SSL_PASS}"
else
echo "<==== Generating new SSL Password ${SSL_PASS} ====>"
ssl_passout_args="-aes256 -passout file:${CERTS_OUT}/${SSL_PASS}"
ssl_passin_args="-passin file:${CERTS_OUT}/${SSL_PASS}"
openssl rand -base64 -out "${CERTS_OUT}/${SSL_PASS}" 32
chmod 400 "${CERTS_OUT}/${SSL_PASS}"
fi
# 証明書の秘密鍵作成
if [ -e "${CERTS_OUT}/${SSL_KEY}" ]; then
echo "<---- Using existing SSL Key ${SSL_KEY} ---->"
else
echo "<==== Generating new SSL Key ${SSL_KEY} ====>"
# shellcheck disable=SC2086
openssl genrsa -out "${CERTS_OUT}/${SSL_KEY}" ${ssl_passout_args} 2048
chmod 400 "${CERTS_OUT}/${SSL_KEY}"
fi
# 証明書署名要求作成 - 自己証明するので要求は最低限で、証明時に全て指定する
if [ -e "${CERTS_OUT}/${SSL_CSR}" ]; then
echo "<---- Using existing SSL CSR ${SSL_CSR} ---->"
else
# shellcheck disable=SC2086
openssl req -new -key "${CERTS_OUT}/${SSL_KEY}" ${ssl_passin_args} -subj "${SSL_SUBJ}" -out "${CERTS_OUT}/${SSL_CSR}"
fi
# 証明書署名要求のシリアル番号作成 - 再作成時は自動インクリメントされるのを使う
if [ -e "${CERTS_OUT}/${SSL_SERIAL}" ]; then
echo "<---- Using existing SSL Serial ${SSL_SERIAL} ---->"
else
echo "<==== Generating new SSL Serial ${SSL_SERIAL} ====>"
echo 00 >"${CERTS_OUT}/${SSL_SERIAL}"
fi
# CA署名証明書作成
if [ -e "${CERTS_OUT}/${SSL_CERT}" ]; then
echo "<---- Using existing SSL CERT ${SSL_CERT} ---->"
else
echo "<==== Generating new SSL CERT ${SSL_CERT} ====>"
# shellcheck disable=SC2086
echo "${SSL_SANS}" | openssl x509 -days 60 -CA "${CERTS_OUT}/${CA_CERT}" -CAkey "${CERTS_OUT}/${CA_KEY}" ${ca_passin_args} -CAserial "${CERTS_OUT}/${SSL_SERIAL}" -req -in "${CERTS_OUT}/${SSL_CSR}" -out "${CERTS_OUT}/${SSL_CERT}" -extfile -
fi
# 確認 - 余計なのも出力される...-nocert効かない?
openssl x509 -text -in "${CERTS_OUT}/${SSL_CERT}" -noout -nocert -issuer -enddate -subject -ext subjectAltName
# end of 証明書
#--------------------------------------------------------------------
大きく3つのパートに分かれています。
- ユーザー設定のための環境変数
- 認証局作成
- 証明書作成
1番目の環境変数をcompose.override.yamlで設定すると動作を変更できます。
特に重要な環境変数はSSL_ADDEXT
です。ここで指定するsubjectAltName
(いわゆるSANs)と実際のアクセス名が一致していないとブラウザが警告を出します。このSANsにはDNS指定とIP指定が可能です。
初期値ではtest系とlocalhost系を登録しています。本書ではこの後"*.dev.test"のアクセスを使っていきます。
たとえばtest系だと"test"、"dev.test"、"*.dev.test"と3つ指定しますが、.
ドット区切りごとに別ドメインとして扱われるためです。また*.
があるのはワイルドカード指定です。最近のブラウザはこのように3個目以上の場所でワイルドカードを受け付けます。"*.test"のように2個目にワイルドカードを指定した場合は証明書とアクセス名が一致しないと判定されて警告が出ます。(当初"*.test"で登録して、結構な時間を無駄にしました😢)
2番目の認証局作成で3つのファイルを作成します。重要なファイルは証明書(公開鍵)のca-My-Test.cerで、これをOSに登録します。
残りの2つは秘密鍵のca-My-Test.keyとこの秘密鍵を暗号化したパスフレーズが書かれたca-My-Test.passです。環境変数CA_PASS
に空文字を設定すると秘密鍵の暗号化はされず平文で保存されます。
認証局の証明書は有効期限を長く(36500日≒100年)設定しています。
3番目の証明書作成で4つのファイルを作成します。重要なファイルは証明書(公開鍵)のssl-dev.test.cerと秘密鍵のssl-dev.test.keyで、これらをTraefikに登録します。
残り2つは証明書の署名要求ssl-dev.test.csrと証明書の連番ssl-dev.test.srlです。認証局と違って証明書の秘密鍵はデフォルト平文で保存しています。環境変数SSL_PASS
でパスフレーズ保存ファイル名を指定すると暗号化されます。ただしTraefikは秘密鍵の暗号化に対応していません。暗号化してもパスフレーズを指定するならセキュリティは大差ないだろうという判断です。
こちらの証明書は有効期限を短く(60日≒2ヵ月)設定しています。これはセキュリティが厳しくなり、現在では397日を超えるとブラウザが警告を出します。今後90日まで短くなることが検討されているため、余裕を持って2ヵ月としています。
awer
compose.yaml(抜粋)
最後に、dockerで使用するcomposeファイルです。
services:
sslcert:
image: arkbig/sslcert
build:
context: ./sslcert
args:
no_proxy: ${no_proxy-}
http_proxy: ${http_proxy-}
https_proxy: ${https_proxy-}
volumes:
- ./sslcert/.certs/:/certs/
profiles:
- sslcert
volumes:
でオレオレ認証局やオレオレ証明書をsslcert/.certs/フォルダに作るためバインドマウントで指定してます。
profiles:
を指定すると、未指定のdocker compose up
では起動しなくなります。普段は使わないものなのでdocker compose run --rm sslcert
と明示して使用します。
もしくは環境変数COMPOSE_PROFILES=sslcert
を設定すれば、sslcertを指定しなくても自動起動の対象になります。
また、必要に応じてenvironment:
で動作を変更できます。個人環境の設定ですので、Git管理外のcompose.override.yamlで指定します。
下記がデフォルト値での設定例です。
services:
sslcert:
environment:
# 作成される証明書たちのownerを指定
CONTAINER_UID: 1000
CONTAINER_GID: 1000
# 出力先フォルダ(コンテナ内パス)
CERTS_OUT: /certs
# 自己認証局の設定
## 名称
CA_CN: My Test
## 生成するファイルのbasename
CA_FILEBODY: <normalized CA_CN>
## OSに登録する認証局の証明書ファイル名
CA_CERT: $CA_FILEBODY.cer
## 証明書発行時に使用する証明書の秘密鍵
CA_KEY: $CA_FILEBODY.key
## 秘密鍵の保存時の暗号パスワード(空文字なら平文保存)
CA_PASS: $CA_FILEBODY.pass
## 認証局の属性
## /C=国コード/ST=県/O=組織名/OU=部門などが指定できる
## /CN=が未指定なら自動で/CN=$CA_CNが付与される
CA_SUBJ: /CN=$CA_CN
# 自己証明書の設定
## 名称(古いシステム用のドメイン)
SSL_CN: dev.test
## 新しいシステム用のSANなど
SSL_ADDEXT: subjectAltName=DNS:test,DNS,dev.test,DNS:*.dev.test,DNS:localhost,DNS:dev.localhost,DNS:*.dev.localhost,IP:127.0.0.1
## 生成するファイルのbasename
SSL_FILEBODY: <normalized SSL_CN>
## サーバーに設定する自己証明書(公開鍵)
SSL_CERT: $SSL_FILEBODY.cer
## サーバーに設定する自己証明書の秘密鍵
SSL_KEY: $SSL_FILEBODY.key
## 認証局への署名リクエストファイル(手抜きなので本番には使えない)
SSL_CSR: $SSL_FILEBODY.csr
## 自己証明書の保存時の暗号パスワード(空文字なら平文保存)
SSL_PASS: ""
## 自己証明書のシリアル番号保存ファイル
SSL_SERIAL: $SSL_FILEBODY.srl
## 自己証明書の属性
## /C=国コード/ST=県/O=組織名/OU=部門などが指定できる
## /CN=が未指定なら自動で/CN=$SSL_CNが付与される
SSL_SUBJ: /CN=$SSL_CN
特に重要なのが、CONTAINER_UID
とCONTAINER_GID
です。作成されるファイルのパーミッションに影響しますので適切に設定する必要があります。
これらの番号はid
コマンドで確認できます。
sslcert使用方法
ここまでに説明したファイルたちを利用して証明書を作るには次のコマンドを実行します。
mkdir sslcert/.certs
docker compose build sslcert
docker compose run --rm sslcert
これにより初期設定では下記7つのファイルがsslcert/.certs/に作成されます。
ファイル名 | 内容 | |
---|---|---|
認 | ca-My-Test.cer | オレオレ認証局の証明書(公開鍵)で、これをOSに登録する |
証 | ca-My-Test.key | 認証局の秘密鍵で、証明書発行時に使用する |
局 | ca-My-Test.pass | 認証局の秘密鍵を開くためのパスフレーズです |
─ | ──────── | ────────────────────────────── |
俺 | ssl-dev.test.cer | オレオレ証明書(公開鍵)で、これをTraefikに登録する |
証 | ssl-dev.test.csr | 証明書のための署名要求ファイルで、本来なら正規の認証局に提出するもの? |
明 | ssl-dev.test.key | オレオレ証明書の秘密鍵で、これもTraefikに登録する |
書 | ssl-dev.test.srl | 証明書のシリアル番号で、期限切れで作り直すときはインクリメントされる |
SSL証明書の有効期限が切れたら再作成をします。
現在のところ、手動でsslcert/.certs/ssl-dev.test.cerだけ削除して、docker compose run --rm sslcert
で再作成します。
OSに自己認証局を信頼させる
作ったオレオレ認証局の証明書sslcert/.certs/ca-My-Test.cerをOSに信頼できる機関として登録します。
🍎Macのキーチェーンへ登録
【クリックで展開】Macのキーチェーンへ登録手順。
- Finderからsslcert/.certs/ca-My-Test.cerを開くと、キーチェーンが起動します(ログイン項目にインストール)
- キーチェーンに追加した証明書の情報を見る
- 「信頼」の項目を「常に信頼」に設定する
- ウィンドウを閉じるとユーザーパスワードが求められ、登録完了
- 赤色の×マークから青色の+マークに変わればOK
- 赤色の×マークから青色の+マークに変わればOK
🪟Windowsのcertmgr.mscへ登録
【クリックで展開】Windowsのcertmgr.mscへ登録手順。
- WSL2側で
explorer.exe sslcert/.certs
を実行するとExplorerが起動します- Explorerからsslcert/.certs/ca-My-Test.cerを開く
- 「証明書のインストール」をクリック
- 「次へ」をクリック(保存場所はどちらでも)
- 証明書のインポートウィザードが開きます
- 「証明書をすべて次のストアに配置する」を選んで、参照から「信頼されたルート証明機関」を選択
- 「次へ」をクリックすると確認ダイアログが出るので「OK」で進めると登録完了
- 登録できたかは「ユーザー証明書の管理」か「システム証明書の管理」(登録したほう)から確認できます
WSL2でも登録します。
sudo mkdir /usr/share/ca-certificates/self
sudo cp ./sslcert/.certs/ca-My-Test.cer /usr/share/ca-certificates/self/
sudo echo "self/ca-My-Test.cer" >> /etc/ca-certificates.conf
sudo update-ca-certificates
🐧Linuxのca-certificatesへ登録
【クリックで展開】Linuxのca-certificatesへ登録手順です。
動作確認したのはWSL2上のUbuntuです。
sudo mkdir /usr/share/ca-certificates/self
sudo cp ./sslcert/.certs/ca-My-Test.cer /usr/share/ca-certificates/self/
sudo echo "self/ca-My-Test.cer" >> /etc/ca-certificates.conf
sudo update-ca-certificates
✔️本章の作業チェックリスト
- ✔️sslcertをrunして証明書を作成
- OSに自己認証局を信頼させる
- 🍎Mac
- 🪟Windows
- 🐧Linux