Chapter 03無料公開

オレオレ証明書の作成とOSへの登録

ArkBig
ArkBig
2022.11.03に更新

「オレだよオレ、信用してくれよ」とブラウザにいうための自己証明書を作り、自己認証局をOSに登録します。

httpsアクセスするのはもちろん、TCP通信でもTLSを使いたいため作ります。というのも、Traefikでホスト名を使ってリバースプロキシしますが、TCP通信の場合はSSL/TLSで暗号化されていないとホスト名を取れないからです。

本章ではいわゆるオレオレ認証局とオレオレ証明書を作成するワンショットのコンテナを構築します。
セキュリティに関わるところだし、自前でopensslコマンドをたたくことで理解を深める目的で作成しました。
一般的な方法がよければ、mkcertというものもあるので、そちらを使ってもいいでしょう。

こちらのサイトを参考にさせていただきました。

https://qiita.com/ll_kuma_ll/items/13c962a6a74874af39c6

https://qiita.com/3244/items/780469306a3c3051c9fe

https://tech-mmmm.blogspot.com/2021/05/linux-esxissl.html

本章はarkbig/devbaseリポジトリのsslcertフォルダの説明になります。

https://github.com/arkbig/devbase

本章で説明するファイル
📂 devbase/
├── 📄compose.override.yaml(必要なら作る)
├── 📄compose.yaml
└── 📂sslcert/
    ├── 📄Dockerfile        # opensslを実行するコンテナ作成指示書
    ├── 📄entrypoint.sh     # コンテナ起動時に実行するヘルパースクリプト
    ├── 📄generatecerts.sh  # opensslを使用して証明書を作るスクリプト
    └── 📁.certs            # ここに証明書を作る(自分で作るフォルダ)

ファイルの関連図

Dockerfile

まずはコンテナのためのイメージを作るDockerfileです。

sslcert/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つのパートに分かれています。

  1. OSのパッケージマネージャーを使って必要なソフトgosuopensslをインストール
  2. コマンド実行用のアカウントcastaffを作成
  3. 実行するコマンドをコンテナへコピー&設定

特筆するとしたら、2番目のアカウント作成部分でしょうか。作成する証明書ファイルはバインドマウントしたホスト側のフォルダを想定しています。そのため、コンテナ内のUIDをホスト側のUIDに合わせる必要があるのでこの処理を行っています。実際のidはentrypoint.shで動的に変更します。

https://qiita.com/yohm/items/047b2e68d008ebb0f001

entrypoint.sh

次にコンテナ実行のヘルパースクリプト(ENTRYPOINT)です。

sslcert/entrypoint.sh
#!/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つのパートに分かれています。

  1. UID,GIDをホストに合わせる
  2. コマンド実行

1番目の処理はホスト側のUID/GIDと合わせるための変更をしています。

2番目のコマンド実行はユーザーを切り替えています。
gosusudoの代わりで、コンテナ内ではTTY関連で問題が起きるためsudoの利用は避けたほうがいいようです。

https://docs.docker.jp/engine/articles/dockerfile_best-practice.html

generatecerts.sh

そしてsslcertのメイン処理である証明書の作成スクリプトです。

sslcert/generatecerts.sh ※Shift+マウスホイールで横スクロール
#!/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. ユーザー設定のための環境変数
  2. 認証局作成
  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は秘密鍵の暗号化に対応していません。暗号化してもパスフレーズを指定するならセキュリティは大差ないだろうという判断です。

https://github.com/traefik/traefik/pull/6518

こちらの証明書は有効期限を短く(60日≒2ヵ月)設定しています。これはセキュリティが厳しくなり、現在では397日を超えるとブラウザが警告を出します。今後90日まで短くなることが検討されているため、余裕を持って2ヵ月としています。
awer

compose.yaml(抜粋)

最後に、dockerで使用するcomposeファイルです。

compose.yaml
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で指定します。
下記がデフォルト値での設定例です。

compose.override.yaml ※Shift+マウスホイールで横スクロール
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_UIDCONTAINER_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のキーチェーンへ登録手順。
  1. Finderからsslcert/.certs/ca-My-Test.cerを開くと、キーチェーンが起動します(ログイン項目にインストール)
    • キーチェーンに追加した証明書の情報を見る
    • 「信頼」の項目を「常に信頼」に設定する
      キーチェーンへ追加
  2. ウィンドウを閉じるとユーザーパスワードが求められ、登録完了
    • 赤色の×マークから青色の+マークに変わればOK 登録完了

🪟Windowsのcertmgr.mscへ登録

【クリックで展開】Windowsのcertmgr.mscへ登録手順。
  1. WSL2側でexplorer.exe sslcert/.certsを実行するとExplorerが起動します
    • Explorerからsslcert/.certs/ca-My-Test.cerを開く
    • 「証明書のインストール」をクリック
    • 「次へ」をクリック(保存場所はどちらでも)
      証明書のインストール
  2. 証明書のインポートウィザードが開きます
    • 「証明書をすべて次のストアに配置する」を選んで、参照から「信頼されたルート証明機関」を選択
    • 「次へ」をクリックすると確認ダイアログが出るので「OK」で進めると登録完了
      証明書のインポートウィザード
  3. 登録できたかは「ユーザー証明書の管理」か「システム証明書の管理」(登録したほう)から確認できます
    登録確認

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

✔️本章の作業チェックリスト