1. はじめに
OCPP (Open Charge Point Protocol) v2.0.1準拠のCSMS (Charging Station Management System)を開発する際に、EV充電器(Charging Station)とCSMS間の通信セキュリティは重要な課題です。とくにOCPPの仕様では、Security Profile 2以上でTLS (Transport Layer Security) による暗号化が必須となります。
今回は、アプリケーションコード(別で開発、FastAPI)には手を加えずに、リバースプロキシとしてNginxを配置することでTLS通信を実現する方法について、その原理原則と実装例をまとめます。
2. TLSの原理原則
2.1 TLSとは
TLSは、ネットワーク上での通信を暗号化し、第三者による盗聴や改ざんを防ぐためのプロトコルです。旧来のSSL(Secure Sockets Layer)の後継にあたります。
TLSの主な役割は以下の3点です:
- 機密性 (Confidentiality): 通信内容を暗号化し、盗聴を防ぐ。
- 完全性 (Integrity): 通信内容が改ざんされていないことを保証する。
- 認証 (Authentication): 通信相手が正当なサーバー(またはクライアント)であることを証明する。
2.2 サーバー証明書の役割
サーバー証明書は、**「接続先サーバーが信頼できる存在であること」**を証明するデジタル身分証明書です。
- 公開鍵暗号方式: サーバーは「秘密鍵」と「公開鍵」のペアを持ちます。公開鍵は証明書に含まれ、クライアントに渡されます。
- 署名: 公開的な認証局(CA)がサーバーの公開鍵に対して電子署名を行うことで、その正当性を保証します(自己署名証明書の場合は自ら署名します)。
- ハンドシェイク: クライアントは証明書を検証し、公開鍵を使って共通鍵(セッションキー)を安全に交換します。以降の通信はこの共通鍵で高速に暗号化されます。
3. アーキテクチャ設計
アプリケーションサーバー(FastAPI/Uvicorn)自体にTLS機能を持たせることも技術的には可能ですが、本プロジェクトでは前段に Nginx を配置してTLS終端(TLS Termination)を行う「サイドカー構成(またはリバースプロキシ構成)」を採用しました。
このアーキテクチャを採用する主な理由は以下の通りです:
-
パフォーマンスと効率:
Python製のサーバー(Uvicorn)でTLS暗号化・復号化を行うよりも、C言語で最適化されたNginxで行う方がCPU効率が良く、高速です。特にOCPPのような常時接続(WebSocket)が多数発生するシステムでは、ハンドシェイクの負荷分散が重要になります。 -
セキュリティ(権限分離):
443番ポート(特権ポート)をリッスンするには通常root権限が必要ですが、アプリケーション自体をrootで動かすことはセキュリティリスクとなります。Nginxのみを特権ポートで動かし、アプリケーションは一般ユーザー権限のポート(8000番など)で動かすことで、万が一アプリに脆弱性があっても影響を最小限に抑えられます。 -
運用の柔軟性(ゼロダウンタイム):
証明書の更新時、Nginxならnginx -s reloadで接続を維持したまま設定を反映できますが、アプリケーションサーバーに組み込んでいる場合、プロセスの再起動(=全WebSocket切断)が必要になることが多いです。
3.1 構成図
本構成のアーキテクチャ図(SVG形式)を以下に示します。
3.2 通信フロー
CSからCSMSへの接続確立とメッセージ転送の流れをシーケンス図で示します。NginxがTLS終端(復号)を行い、バックエンドには平文で転送している様子がわかります。
4. 実装詳細
具体的な実装内容を見ていきます。
4.1 証明書の生成
本プロジェクトでは、環境に応じて適切な証明書生成アプローチをとっています。
ローカル開発環境 (scripts/generate_certs.sh)
ローカル環境では固定の設定ファイルを使用し、OpenSSLで自己署名証明書を生成します。
# 秘密鍵の生成
openssl genrsa -out certs/server.key 2048
# 証明書署名要求 (CSR) の生成
# SAN (Subject Alternative Name) を設定し、localhostやIPアドレスでも警告が出ないように配慮
openssl req -new -key certs/server.key -out certs/server.csr -config certs/openssl.cnf
# 自己署名証明書の生成
openssl x509 -req -days 365 -in certs/server.csr -signkey certs/server.key -out certs/server.crt ...
クラウド環境での動的なSAN設定 (terraform/user-data.sh)
AWS EC2などのクラウド環境では、サーバーのIPアドレスやDNS名はインスタンス起動時に動的に決定されます。そのため、固定の設定ファイルでは対応しきれない場合があります。
そこで user-data スクリプト内で、インスタンスメタデータから情報を取得し、OpenSSL設定ファイルを動的生成する工夫を行っています。
# 1. インスタンスメタデータ(IMDSv2)からパブリックIP/DNSを取得
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" ...)
PUBLIC_IP=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" ... public-ipv4 ...)
# 2. 取得したIP/DNSを埋め込んだ openssl.cnf を動的に作成
cat > certs/openssl.cnf <<EOF
...
[alt_names]
DNS.1 = $PUBLIC_DNS
DNS.2 = localhost
IP.1 = $PUBLIC_IP <-- 動的に取得したIPを設定
IP.2 = 127.0.0.1
EOF
# 3. 証明書の生成
openssl req -x509 ... -config certs/openssl.cnf ...
Why?(なぜこれが必要か)
TLS通信においてクライアント(充電器)は、接続先URLのホスト名と、サーバー証明書に含まれる SAN (Subject Alternative Name) が一致するかを厳格に検証します。
とくに検証環境などでIPアドレスを直接指定して接続する場合、証明書のSANにそのIPが含まれていなければ接続エラー(Hostname mismatch)となります。IPが変動するクラウド環境において、この動的生成ロジックはスムーズな接続検証のために不可欠です。
4.2 Nginxの設定 (docker/nginx.conf)
Nginxの設定では、443ポートでHTTPS/WSSを受け付け、バックエンドのFastAPIへ転送します。
# アップストリーム(転送先)の定義
upstream ocpp_backend {
server ocpp-csms-test-tool:8000;
}
server {
listen 443 ssl;
http2 on;
server_name localhost;
# 証明書と秘密鍵のパス
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# 推奨されるSSLプロトコルとCipher設定
ssl_protocols TLSv1.2 TLSv1.3;
# OCPP WebSocket のハンドリング
location /ocpp/ {
proxy_pass http://ocpp_backend;
# WebSocket接続のための必須ヘッダー
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# クライアント情報の転送
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
とくに Upgrade と Connection ヘッダーの設定は、HTTP通信をWebSocketプロトコルにアップグレードするために不可欠です。
4.3 Docker Compose (docker-compose.yml)
docker-compose で全体の構成を定義します。
# Nginx reverse proxy with TLS
nginx:
image: nginx:alpine
ports:
- "80:80" # HTTPリダイレクト用
- "443:443" # HTTPS/WSS用
volumes:
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/ssl:ro # 生成した証明書をマウント
depends_on:
- ocpp-csms-test-tool
アプリケーション (ocpp-csms-test-tool) はポートをホストに公開せず(ports をコメントアウト)、Nginx経由でのみアクセス可能にすることで、セキュリティを高めています。
5. 自己署名証明書とWebSocket接続の検証
本開発環境では、正規の認証局(CA)ではなく、OpenSSLで生成した**自己署名証明書(Self-Signed Certificate)**を利用してTLS通信を確立しています。この仕組みについて解説します。
5.1 なぜ自己署名証明書を使うのか?
通常、Webサイトなどで使われる証明書は、信頼された第三者機関(Let's Encrypt や DigiCert など)によって署名されています。これにより、ブラウザは「このサイトは本物だ」と判断できます。
しかし、開発環境(localhost)や閉じたネットワーク内では、正規のCAから証明書を取得することが難しい場合があります。そこで、**「自分で自分の身元を証明する」**形式の自己署名証明書を使用します。
5.2 開発ツールにおける証明書検証の実装
本プロジェクトのシミュレーター(cs_simulator.py)では、デフォルトでサーバー証明書の検証を行うセキュアな実装になっています。
-
デフォルト動作(セキュア):
ssl.create_default_context()を使用し、サーバー証明書が信頼されたCAによって署名されているか、ホスト名が一致するかを厳格に検証します。 -
開発用オプション(検証スキップ):
自己署名証明書を使用する開発環境向けに、--no-verify-sslフラグを提供しています。このフラグが有効な場合のみ、例外的に検証を無効化します。 -
カスタムCAの対応:
--ca-certオプションにより、自己署名証明書を信頼するためのルートCAファイルを明示的に指定して検証を通すことも可能です。
5.3 Python実装例
以下は、シミュレーターでの実装ロジックの抜粋です。デフォルトでは厳格な検証を行い、オプション指定時のみ柔軟に対応する設計となっています。
import ssl
import websockets
import asyncio
async def connect_csms(ssl_verify=True, ca_cert_path=None):
uri = "wss://localhost:443/ocpp/CS001"
# SSLコンテキストの作成(デフォルトでOSの信頼するCAリストを使用)
ssl_context = ssl.create_default_context()
if not ssl_verify:
# 開発用: 明示的に指定された場合のみ検証を無効化
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
print("⚠️ SSL verification disabled")
elif ca_cert_path:
# カスタムCA(自己署名証明書など)を信頼リストに追加
ssl_context.load_verify_locations(cafile=ca_cert_path)
print(f"🔒 Using custom CA: {ca_cert_path}")
# 接続処理(デフォルトでは検証が失敗すると接続できない)
async with websockets.connect(uri, ssl=ssl_context, subprotocols=["ocpp2.0.1"]) as websocket:
print("✅ Connected securely!")
このように、ツール側では「基本は検証する(安全)」、「必要に応じて緩める(開発)」というポリシーで実装されています。
このコードのポイントは ssl.CERT_NONE です。これにより、サーバーが提示した証明書が自己署名であっても、有効期限が切れていても、クライアントはエラーを出さずに暗号化通信を開始します。
5.4 注意点
本番環境では、この設定(check_hostname = False, verify_mode = ssl.CERT_NONE)は絶対に使用してはいけません。
中間者攻撃(Man-in-the-Middle Attack)のリスクがあるため、必ず正規のCAから発行された証明書を使用し、適切な検証を行う必要があります。
6. まとめ
Nginxを活用することで、アプリケーションコードを変更することなくセキュアなTLS通信を実現しました。
これは「関心の分離」というソフトウェア設計の原則にも適っており、本番環境(AWS ELB + ACMなど)への移行もスムーズに行える構成となっています。
OCPPのような重要インフラに関わる通信において、TLSの理解と適切な実装は必須のスキルです。 この構成をベースに、さらに強固なセキュリティ(クライアント証明書認証など)へ発展させることも可能です。
この記事は役に立ちましたか?
フィードバックはブログの改善に活用させていただきます