0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenSSLでプライベートCAを作る ― スクリプト全文

0
Last updated at Posted at 2026-09-24

このシリーズについて
この記事は「OpenSSLでプライベートCAを作る(全15章)」の一部である。章番号はシリーズ全体で通し番号になっている。はじめに・開発環境・まとめ・参考文献は目次記事にある。

12. スクリプト全文

ここまでの手順を1本にまとめたものである。

#!/usr/bin/env bash
#
# mTLS 検証用の証明書一式を生成する。
#
#   ルートCA ──▶ 中間CA(mTLS専用)──┬─▶ サーバ証明書(nginx用)
#                                     └─▶ クライアント証明書(端末用・検証パターン別)
#
# 生成物はすべて開発・検証専用。本番環境では使用しない。
#
# 使い方:
#   ./gen-certs.sh              # 生成(既に certs/ があれば中断)
#   ./gen-certs.sh --force      # certs/ を作り直す
#
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_DIR="${OUT_DIR:-${SCRIPT_DIR}/certs}"
OPENSSL="${OPENSSL:-openssl}"

SERVER_HOSTNAME="${SERVER_HOSTNAME:-$(hostname -s).local}"
SERVER_IP="${SERVER_IP:-127.0.0.1}"
P12_PASSWORD="${P12_PASSWORD:-mtls-dev}"

ROOT_DAYS=3650          # ルートCA: 10年
INTERMEDIATE_DAYS=1825  # 中間CA: 5年
LEAF_DAYS=825           # サーバ・クライアント証明書: 約2年

ROOT_DIR="${OUT_DIR}/root"
INT_DIR="${OUT_DIR}/intermediate"
SRV_DIR="${OUT_DIR}/server"
CLI_DIR="${OUT_DIR}/clients"
CRL_DIR="${OUT_DIR}/crl"

CA_CHAIN="${OUT_DIR}/ca-chain.cert.pem"          # 中間 + ルート(ssl_client_certificate 用)
SRV_FULLCHAIN="${SRV_DIR}/server-fullchain.pem"  # サーバ + 中間(ssl_certificate 用)

log()  { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
step() { printf '    - %s\n' "$*"; }

# ---------------------------------------------------------------- 事前チェック
if ! "$OPENSSL" version | grep -q '^OpenSSL 3'; then
  echo "エラー: OpenSSL 3.x が必要です(現在: $("$OPENSSL" version))" >&2
  echo "       macOS 標準の LibreSSL では動作しません。brew install openssl@3 を実行し、" >&2
  echo "       OPENSSL=\"\$(brew --prefix openssl@3)/bin/openssl\" ./gen-certs.sh のように指定してください。" >&2
  exit 1
fi

if [ -e "$OUT_DIR" ]; then
  if [ "${1:-}" = "--force" ]; then
    log "既存の ${OUT_DIR} を削除します"
    rm -rf "$OUT_DIR"
  else
    echo "エラー: ${OUT_DIR} が既に存在します。作り直す場合は --force を付けてください。" >&2
    exit 1
  fi
fi

# ------------------------------------------------------------ CA ディレクトリ
# openssl ca コマンドは「発行済み台帳(index.txt)」「連番(serial)」を前提に動く。
# CRL の生成にもこの台帳が要るため、CA ごとに一式を用意する。
init_ca_dir() {
  local dir="$1"
  mkdir -p "$dir/newcerts" "$dir/private"
  chmod 700 "$dir/private"
  : > "$dir/index.txt"
  echo 1000 > "$dir/serial"
  echo 1000 > "$dir/crlnumber"
}

write_ca_cnf() {
  local dir="$1"
  cat > "$dir/openssl.cnf" <<CNF_EOF
[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = ${dir}
new_certs_dir     = \$dir/newcerts
database          = \$dir/index.txt
serial            = \$dir/serial
crlnumber         = \$dir/crlnumber
private_key       = \$dir/private/ca.key.pem
certificate       = \$dir/ca.cert.pem
default_md        = sha256
default_days      = ${LEAF_DAYS}
default_crl_days  = 30
preserve          = no
email_in_dn       = no
unique_subject    = no
policy            = policy_loose
copy_extensions   = none

[ policy_loose ]
countryName            = optional
stateOrProvinceName    = optional
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = optional

[ req ]
default_bits       = 2048
default_md         = sha256
string_mask        = utf8only
prompt             = no
distinguished_name = req_dn

[ req_dn ]

[ v3_intermediate_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = critical, CA:true, pathlen:0
keyUsage               = critical, digitalSignature, cRLSign, keyCertSign

[ server_cert ]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage               = critical, digitalSignature, keyEncipherment
extendedKeyUsage       = serverAuth

[ client_cert ]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer
keyUsage               = critical, digitalSignature, keyEncipherment
extendedKeyUsage       = clientAuth

[ crl_ext ]
authorityKeyIdentifier = keyid:always
CNF_EOF
}

# ------------------------------------------------------------------ ルートCA
log "ルートCA を作成します"
init_ca_dir "$ROOT_DIR"
write_ca_cnf "$ROOT_DIR"

"$OPENSSL" req -x509 -new -nodes -newkey rsa:4096 -sha256 -days "$ROOT_DAYS" \
  -keyout "$ROOT_DIR/private/ca.key.pem" \
  -out    "$ROOT_DIR/ca.cert.pem" \
  -subj "/C=JP/O=Example Corp/CN=Example Corp Dev Root CA" \
  -addext "basicConstraints=critical,CA:true,pathlen:1" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" >/dev/null 2>&1
chmod 600 "$ROOT_DIR/private/ca.key.pem"
step "$ROOT_DIR/ca.cert.pem"

# ------------------------------------------------------------------ 中間CA
log "中間CA(mTLS専用)を作成します"
init_ca_dir "$INT_DIR"
write_ca_cnf "$INT_DIR"

"$OPENSSL" req -new -nodes -newkey rsa:4096 -sha256 \
  -keyout "$INT_DIR/private/ca.key.pem" \
  -out    "$INT_DIR/ca.csr.pem" \
  -subj "/C=JP/O=Example Corp/CN=Example Corp mTLS Intermediate CA" >/dev/null 2>&1
chmod 600 "$INT_DIR/private/ca.key.pem"

"$OPENSSL" ca -batch -config "$ROOT_DIR/openssl.cnf" \
  -extensions v3_intermediate_ca -days "$INTERMEDIATE_DAYS" -notext -md sha256 \
  -in "$INT_DIR/ca.csr.pem" -out "$INT_DIR/ca.cert.pem" >/dev/null 2>&1
step "$INT_DIR/ca.cert.pem"

cat "$INT_DIR/ca.cert.pem" "$ROOT_DIR/ca.cert.pem" > "$CA_CHAIN"
step "$CA_CHAIN (nginx の ssl_client_certificate に指定する)"

# -------------------------------------------------------------- サーバ証明書
log "サーバ証明書を作成します(${SERVER_HOSTNAME} / ${SERVER_IP})"
mkdir -p "$SRV_DIR"

# -extfile を渡すと -extensions は「このファイル内の節」を指す。
# そのため、サーバ証明書用の拡張一式をここに書ききる。
cat > "$SRV_DIR/san.cnf" <<SAN_EOF
[ server_cert ]
basicConstraints       = CA:FALSE
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage               = critical, digitalSignature, keyEncipherment
extendedKeyUsage       = serverAuth
subjectAltName         = DNS:${SERVER_HOSTNAME}, DNS:localhost, IP:${SERVER_IP}, IP:127.0.0.1
SAN_EOF

"$OPENSSL" req -new -nodes -newkey rsa:2048 -sha256 \
  -keyout "$SRV_DIR/server.key.pem" \
  -out    "$SRV_DIR/server.csr.pem" \
  -subj "/C=JP/O=Example Corp/CN=${SERVER_HOSTNAME}" >/dev/null 2>&1
chmod 600 "$SRV_DIR/server.key.pem"

"$OPENSSL" ca -batch -config "$INT_DIR/openssl.cnf" \
  -extensions server_cert -extfile "$SRV_DIR/san.cnf" -days "$LEAF_DAYS" -notext -md sha256 \
  -in "$SRV_DIR/server.csr.pem" -out "$SRV_DIR/server.cert.pem" >/dev/null 2>&1

cat "$SRV_DIR/server.cert.pem" "$INT_DIR/ca.cert.pem" > "$SRV_FULLCHAIN"
step "$SRV_FULLCHAIN (nginx の ssl_certificate に指定する)"

# ---------------------------------------------------------- クライアント証明書
mkdir -p "$CLI_DIR"

# $1: ファイル名, $2: CN, $3: 説明, 以降: openssl ca への追加引数
issue_client() {
  local name="$1" cn="$2" desc="$3"; shift 3
  "$OPENSSL" req -new -nodes -newkey rsa:2048 -sha256 \
    -keyout "$CLI_DIR/${name}.key.pem" \
    -out    "$CLI_DIR/${name}.csr.pem" \
    -subj "/C=JP/O=Example Corp/OU=Devices/CN=${cn}" >/dev/null 2>&1
  chmod 600 "$CLI_DIR/${name}.key.pem"

  "$OPENSSL" ca -batch -config "$INT_DIR/openssl.cnf" \
    -extensions client_cert -notext -md sha256 "$@" \
    -in "$CLI_DIR/${name}.csr.pem" -out "$CLI_DIR/${name}.cert.pem" >/dev/null 2>&1

  # 端末へ入れるための .p12。鍵・証明書・CAチェーンを1ファイルにまとめる。
  "$OPENSSL" pkcs12 -export \
    -inkey "$CLI_DIR/${name}.key.pem" \
    -in    "$CLI_DIR/${name}.cert.pem" \
    -certfile "$CA_CHAIN" \
    -name "Example Corp ${cn}" \
    -passout "pass:${P12_PASSWORD}" \
    -out "$CLI_DIR/${name}.p12" >/dev/null 2>&1

  local serial
  serial="$("$OPENSSL" x509 -in "$CLI_DIR/${name}.cert.pem" -noout -serial | cut -d= -f2)"
  step "$(printf '%-24s CN=%-12s serial=%s  %s' "${name}.p12" "$cn" "$serial" "$desc")"
}

log "クライアント証明書を作成します(検証パターン別)"
issue_client "device-001"         "device-001" "正常系" -days "$LEAF_DAYS"
issue_client "device-001-renewed" "device-001" "更新後(CN は同じ・連番のみ変わる)" -days "$LEAF_DAYS"
issue_client "device-002"         "device-002" "許可リストから外して遮断する確認用" -days "$LEAF_DAYS"
issue_client "device-003"         "device-003" "CA 側で失効させる確認用" -days "$LEAF_DAYS"
issue_client "device-999"         "device-999" "有効期限切れ" -startdate 20250101000000Z -enddate 20250102000000Z

# ------------------------------------------------------------------ 失効リスト
log "device-003 を失効させ、失効リスト(CRL)を作成します"
mkdir -p "$CRL_DIR"
"$OPENSSL" ca -batch -config "$INT_DIR/openssl.cnf" \
  -revoke "$CLI_DIR/device-003.cert.pem" -crl_reason keyCompromise >/dev/null 2>&1
"$OPENSSL" ca -batch -config "$INT_DIR/openssl.cnf" \
  -gencrl -crlexts crl_ext -out "$CRL_DIR/intermediate.crl.pem" >/dev/null 2>&1
step "$CRL_DIR/intermediate.crl.pem"

# nginx は ssl_crl を設定するとチェーンの全段について CRL を確認する。
# そのためルートCA の CRL も要る。2つを 1 ファイルに連結して nginx に渡す。
"$OPENSSL" ca -batch -config "$ROOT_DIR/openssl.cnf" \
  -gencrl -crlexts crl_ext -out "$CRL_DIR/root.crl.pem" >/dev/null 2>&1
step "$CRL_DIR/root.crl.pem"
cat "$CRL_DIR/intermediate.crl.pem" "$CRL_DIR/root.crl.pem" > "$CRL_DIR/ca-chain.crl.pem"
step "$CRL_DIR/ca-chain.crl.pem"

# ---------------------------------------------------------------------- 検証
log "生成物を検証します"
"$OPENSSL" verify -CAfile "$ROOT_DIR/ca.cert.pem" -untrusted "$INT_DIR/ca.cert.pem" \
  "$SRV_DIR/server.cert.pem" | sed 's/^/    /'
for f in "$CLI_DIR"/*.cert.pem; do
  if "$OPENSSL" verify -CAfile "$ROOT_DIR/ca.cert.pem" -untrusted "$INT_DIR/ca.cert.pem" "$f" 2>/dev/null | grep -q OK; then
    printf '    %s: OK\n' "$(basename "$f")"
  else
    # 期限切れの1枚はここで OK にならないのが正しい挙動
    printf '    %s: %s\n' "$(basename "$f")" \
      "$("$OPENSSL" verify -CAfile "$ROOT_DIR/ca.cert.pem" -untrusted "$INT_DIR/ca.cert.pem" "$f" 2>&1 | grep 'lookup:' | head -1 | sed 's/^.*lookup: *//')"
  fi
done

# 中間CA を外すと検証が失敗すること
if "$OPENSSL" verify -CAfile "$ROOT_DIR/ca.cert.pem" "$CLI_DIR/device-001.cert.pem" >/dev/null 2>&1; then
  printf '    \033[1;31m注意: 中間CA 無しでも検証が通っています(想定外)\033[0m\n'
else
  printf '    中間CA を外すと検証は失敗する(想定どおり)\n'
fi

# nginx と同じ条件(チェーンの全段を CRL で確認)でも通ること
if "$OPENSSL" verify -crl_check_all -CAfile "$CA_CHAIN" \
     -CRLfile "$CRL_DIR/ca-chain.crl.pem" "$CLI_DIR/device-001.cert.pem" >/dev/null 2>&1; then
  printf '    nginx と同じ条件でも検証は通る(想定どおり)\n'
else
  printf '    \033[1;31m注意: nginx と同じ条件で検証が失敗しています(CRL 不足)\033[0m\n'
fi

cat <<SUMMARY_EOF

生成が完了しました。

  出力先          ${OUT_DIR}
  サーバ名        ${SERVER_HOSTNAME} (IP: ${SERVER_IP})
  .p12 パスワード ${P12_PASSWORD}

nginx に渡すファイル
  ssl_certificate         ${SRV_FULLCHAIN}
  ssl_certificate_key     ${SRV_DIR}/server.key.pem
  ssl_client_certificate  ${CA_CHAIN}
  ssl_crl                 ${CRL_DIR}/ca-chain.crl.pem

端末に入れるファイル
  発行元(信頼設定が必要) ${ROOT_DIR}/ca.cert.pem
  端末証明書               ${CLI_DIR}/device-001.p12 ほか
SUMMARY_EOF

12.1. 実行結果

上のスクリプトを~/mtls-demo/gen-certs.shとして保存し、実行権限を付けてから実行する。

実行コマンド
chmod +x gen-certs.sh
OPENSSL="$(brew --prefix openssl@3)/bin/openssl" ./gen-certs.sh
実行結果
==> ルートCA を作成します
    - ~/mtls-demo/certs/root/ca.cert.pem
==> 中間CA(mTLS専用)を作成します
    - ~/mtls-demo/certs/intermediate/ca.cert.pem
    - ~/mtls-demo/certs/ca-chain.cert.pem (nginx の ssl_client_certificate に指定する)
==> サーバ証明書を作成します(gw.example.internal / 192.0.2.10)
    - ~/mtls-demo/certs/server/server-fullchain.pem (nginx の ssl_certificate に指定する)
==> クライアント証明書を作成します(検証パターン別)
    - device-001.p12           CN=device-001   serial=1001  正常系
    - device-001-renewed.p12   CN=device-001   serial=1002  更新後(CN は同じ・連番のみ変わる)
    - device-002.p12           CN=device-002   serial=1003  許可リストから外して遮断する確認用
    - device-003.p12           CN=device-003   serial=1004  CA 側で失効させる確認用
    - device-999.p12           CN=device-999   serial=1005  有効期限切れ
==> device-003 を失効させ、失効リスト(CRL)を作成します
    - ~/mtls-demo/certs/crl/intermediate.crl.pem
    - ~/mtls-demo/certs/crl/root.crl.pem
    - ~/mtls-demo/certs/crl/ca-chain.crl.pem
==> 生成物を検証します
    ~/mtls-demo/certs/server/server.cert.pem: OK
    device-001-renewed.cert.pem: OK
    device-001.cert.pem: OK
    device-002.cert.pem: OK
    device-003.cert.pem: OK
    device-999.cert.pem: certificate has expired
    中間CA を外すと検証は失敗する(想定どおり)
    nginx と同じ条件でも検証は通る(想定どおり)

生成が完了しました。

  出力先          ~/mtls-demo/certs
  サーバ名        gw.example.internal (IP: 192.0.2.10)
  .p12 パスワード mtls-dev

nginx に渡すファイル
  ssl_certificate         ~/mtls-demo/certs/server/server-fullchain.pem
  ssl_certificate_key     ~/mtls-demo/certs/server/server.key.pem
  ssl_client_certificate  ~/mtls-demo/certs/ca-chain.cert.pem
  ssl_crl                 ~/mtls-demo/certs/crl/ca-chain.crl.pem

端末に入れるファイル
  発行元(信頼設定が必要) ~/mtls-demo/certs/root/ca.cert.pem
  端末証明書               ~/mtls-demo/certs/clients/device-001.p12 ほか

最後に「生成が完了しました。」が出て、検証の行がすべて想定どおりになっていればよい。

device-999.cert.pem: certificate has expiredは、期限切れの証明書を意図的に作っているためである(10.2参照)。「中間CA を外すと検証は失敗する」「nginx と同じ条件でも検証は通る」の2行は、10.3と10.5に対応する。

certs/が既にある場合は中断する。作り直すときは--forceを付ける。

実行コマンド
OPENSSL="$(brew --prefix openssl@3)/bin/openssl" ./gen-certs.sh --force

次は13. 運用上の注意に戻る。

全体の目次は「OpenSSLでプライベートCAを作る(全15章)」にある。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?