テスト用のCA認証局をDockerで作ってテストしてみたいです。手順を教えて下さい
了解しました。修正後(server.conf
を /etc/nginx/conf.d/default.conf
にマウント)の前提で、最初から通しで動く手順をまとめ直します。最小構成の“OpenSSL製ローカルCA(Docker)”でサーバ証明書・クライアント証明書を発行し、Nginx で TLS と(任意の)mTLS を確認します。
0) 前提
- ホスト:Linux/WSL/PowerShell いずれでも可(以下は Linux/WSL 体裁)
- 作業ディレクトリ:
~/ca-docker
mkdir -p ~/ca-docker && cd ~/ca-docker
1) ファイル一式の作成
1-1) Dockerfile(CA用コンテナ)
# ./Dockerfile
FROM alpine:3.20
RUN apk add --no-cache bash openssl coreutils
WORKDIR /ca
1-2) docker-compose.yml
# ./docker-compose.yml
services:
ca:
build: .
container_name: local-openssl-ca
working_dir: /ca
command: bash -lc "sleep infinity"
volumes:
- ./ca:/ca # 生成物をホストへ
nginx:
image: nginx:1.27-alpine
container_name: mtls-nginx
depends_on: []
ports:
- "8443:8443"
volumes:
- ./server.conf:/etc/nginx/conf.d/default.conf:ro
- ./ca/certs:/certs:ro
- ./ca/private:/private:ro
1-3) Nginx サーバ設定(conf.d 用に server ブロックのみ)
# ./server.conf
server {
listen 8443 ssl;
ssl_certificate /certs/server.local.cert.pem;
ssl_certificate_key /private/server.local.key.pem;
# mTLS(クライアント証明書)検証:optional(= 任意)。必須化は 'on'
ssl_client_certificate /certs/ca.cert.pem;
ssl_verify_client optional;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
add_header Content-Type text/plain;
return 200 "OK (mTLS optional)\n";
}
}
1-4) OpenSSL 設定(CA用)
# ./ca/openssl.cnf
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /ca
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
crlnumber = $dir/crlnumber
RANDFILE = $dir/private/.rand
private_key = $dir/private/ca.key.pem
certificate = $dir/certs/ca.cert.pem
default_md = sha256
default_days = 825
policy = policy_loose
email_in_dn = no
unique_subject = no
copy_extensions = copy
crl = $dir/crl/ca.crl.pem
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256
x509_extensions = v3_ca
[ req_distinguished_name ]
commonName = Common Name (eg, your name)
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, keyCertSign, cRLSign
[ v3_server ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[ v3_client ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
1-5) サーバ証明書の CSR 用設定(SAN はここで指定)
# ./ca/server.req.cnf
[ req ]
distinguished_name = dn
req_extensions = v3_req
prompt = no
[ dn ]
CN = localhost
[ v3_req ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
IP.1 = 127.0.0.1
メモ:
openssl.cnf
の[CA_default] copy_extensions = copy
により、CSR 側(server.req.cnf
)の SAN が証明書へコピーされます。SAN を変える場合はこのファイルを編集します。
2) CA コンテナ起動&CA 初期化
docker compose up -d ca
docker compose exec ca bash
(以降、CA コンテナ内)
set -eux
mkdir -p private certs csr newcerts crl
chmod 700 private
: > index.txt
echo 1000 > serial
echo 1000 > crlnumber
3) ルートCA 作成(自己署名)
# ルートCA鍵
openssl genrsa -out private/ca.key.pem 4096
chmod 600 private/ca.key.pem
# ルートCA証明書(10年)
openssl req -config openssl.cnf \
-key private/ca.key.pem \
-new -x509 -days 3650 -sha256 \
-subj "/CN=Local Test Root CA" \
-extensions v3_ca \
-out certs/ca.cert.pem
# 確認
openssl x509 -in certs/ca.cert.pem -noout -text | head -n 20
4) サーバ証明書の発行(SAN= localhost / 127.0.0.1)
# 秘密鍵
openssl genrsa -out private/server.local.key.pem 2048
chmod 600 private/server.local.key.pem
# CSR(SAN は server.req.cnf)
openssl req -new -key private/server.local.key.pem \
-out csr/server.local.csr.pem \
-config server.req.cnf
# 署名(server 用拡張)
openssl ca -batch -config openssl.cnf \
-extensions v3_server -days 825 -notext -md sha256 \
-in csr/server.local.csr.pem \
-out certs/server.local.cert.pem
# 検証
openssl verify -CAfile certs/ca.cert.pem certs/server.local.cert.pem
5) クライアント証明書の発行(curl/ブラウザ用)
# 秘密鍵
openssl genrsa -out private/client1.key.pem 2048
chmod 600 private/client1.key.pem
# CSR
openssl req -new -key private/client1.key.pem \
-out csr/client1.csr.pem \
-subj "/CN=client1"
# 署名(client 用拡張)
openssl ca -batch -config openssl.cnf \
-extensions v3_client -days 825 -notext -md sha256 \
-in csr/client1.csr.pem \
-out certs/client1.cert.pem
# PKCS#12(ブラウザ取込用。パスワードなし)
openssl pkcs12 -export -inkey private/client1.key.pem \
-in certs/client1.cert.pem -certfile certs/ca.cert.pem \
-out certs/client1.p12 -password pass:
CA コンテナから抜ける:
exit
6) Nginx 起動(TLS / mTLS optional)
docker compose up -d nginx
docker logs mtls-nginx --tail=50
7) 動作確認(ホスト側)
- CA を信頼してアクセス(クライアント証明書なし)
curl -v --cacert ./ca/certs/ca.cert.pem https://localhost:8443/
- mTLS(クライアント証明書あり)
curl -v --cacert ./ca/certs/ca.cert.pem \
--cert ./ca/certs/client1.cert.pem \
--key ./ca/private/client1.key.pem \
https://localhost:8443/
- クライアント証明書を必須にする場合:
server.conf
のssl_verify_client on;
に変更して
docker compose restart nginx
クライアント証明書なしアクセスは 400/403 になります。
よくあるハマりどころ
- 「Are you trying to mount a directory onto a file?」
ホスト側の./server.conf
はファイルである必要があります。ls -ld server.conf
で確認。./ca/certs
と./ca/private
はディレクトリである必要があります。 openssl ca
の初期化エラー
index.txt
、serial
、crlnumber
を作っていないと失敗します(手順 2 で作成済み)。- ブラウザで警告回避
テスト用途に限って OS/ブラウザの「信頼済みルート」に./ca/certs/ca.cert.pem
をインポートします。
注意
本手順は検証用です。実運用では中間CAの分離、鍵保護(HSM 等)、失効運用(CRL/OCSP)、監査・権限分離などが必須です。
内容が難しいです。もっと簡単に説明して下さい
了解です。いま作った entities/server_web02
と entities/client_alice
の証明書・鍵をそのまま使って、**相互TLS(mTLS)で通信する最小のPythonプログラム(サーバ+クライアント)**を用意します。Nginxは触らず、別ポート 9443
を使います。
※ 実行は ~/ca-docker
をカレントにしてください。
サーバ(server_web02 側)
server_web02_https.py
を作成
# server_web02_https.py from http.server import HTTPServer, BaseHTTPRequestHandler import ssl class Handler(BaseHTTPRequestHandler): def do_GET(self): cert = self.connection.getpeercert() # クライアント証明書のCNを表示(任意) client_cn = None if cert and "subject" in cert: subject = dict(x[0] for x in cert["subject"]) client_cn = subject.get("commonName") self.send_response(200) self.send_header("Content-Type", "text/plain; charset=utf-8") self.end_headers() self.wfile.write(f"Hello from server_web02\nclient_cn={client_cn}\n".encode("utf-8")) if __name__ == "__main__": httpd = HTTPServer(("0.0.0.0", 9443), Handler) # 相互TLS用コンテキスト ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) # サーバ側の証明書/鍵(server_web02) ctx.load_cert_chain( certfile="entities/server_web02/certs/server_web02.cert.pem", keyfile="entities/server_web02/private/server_web02.key.pem", ) # クライアント証明書の検証に使うCA ctx.load_verify_locations(cafile="ca/certs/ca.cert.pem") ctx.verify_mode = ssl.CERT_REQUIRED # クライアント証明書必須 httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True) print("server_web02: https://127.0.0.1:9443 で待機中(mTLS 必須)") httpd.serve_forever()
起動(ターミナル1):cd ~/ca-docker python3 server_web02_https.py
クライアント(client_alice 側)
client_alice_request.py
を作成
# client_alice_request.py
import ssl
import http.client
if __name__ == "__main__":
# サーバ検証用CA(自作CA)+ クライアント証明書/鍵(client_alice)
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile="ca/certs/ca.cert.pem")
ctx.load_cert_chain(
certfile="entities/client_alice/certs/client_alice.cert.pem",
keyfile="entities/client_alice/private/client_alice.key.pem",
)
# 証明書のSANに IP=127.0.0.1 を入れているため、ホスト名は 127.0.0.1 を使う
conn = http.client.HTTPSConnection("127.0.0.1", 9443, context=ctx)
conn.request("GET", "/")
resp = conn.getresponse()
body = resp.read().decode("utf-8", errors="replace")
print(resp.status, resp.reason)
print(body)
conn.close()
実行(ターミナル2):
cd ~/ca-docker
python3 client_alice_request.py
期待結果:
-
クライアント側に
200 OK
と本文
Hello from server_web02
client_cn=client_alice