サーバ証明書によるTLS実装の原理と実践 - Nginxを用いたOCPP通信のセキュア化

OCPPTLSNginx

Nginxを使いOCPP通信をTLS化する際のサーバ証明書原理と実装方法を図解付きで解説

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点です:

  1. 機密性 (Confidentiality): 通信内容を暗号化し、盗聴を防ぐ。
  2. 完全性 (Integrity): 通信内容が改ざんされていないことを保証する。
  3. 認証 (Authentication): 通信相手が正当なサーバー(またはクライアント)であることを証明する。

2.2 サーバー証明書の役割

サーバー証明書は、**「接続先サーバーが信頼できる存在であること」**を証明するデジタル身分証明書です。

  1. 公開鍵暗号方式: サーバーは「秘密鍵」と「公開鍵」のペアを持ちます。公開鍵は証明書に含まれ、クライアントに渡されます。
  2. 署名: 公開的な認証局(CA)がサーバーの公開鍵に対して電子署名を行うことで、その正当性を保証します(自己署名証明書の場合は自ら署名します)。
  3. ハンドシェイク: クライアントは証明書を検証し、公開鍵を使って共通鍵(セッションキー)を安全に交換します。以降の通信はこの共通鍵で高速に暗号化されます。

3. アーキテクチャ設計

アプリケーションサーバー(FastAPI/Uvicorn)自体にTLS機能を持たせることも技術的には可能ですが、本プロジェクトでは前段に Nginx を配置してTLS終端(TLS Termination)を行う「サイドカー構成(またはリバースプロキシ構成)」を採用しました。

このアーキテクチャを採用する主な理由は以下の通りです:

  1. パフォーマンスと効率:
    Python製のサーバー(Uvicorn)でTLS暗号化・復号化を行うよりも、C言語で最適化されたNginxで行う方がCPU効率が良く、高速です。特にOCPPのような常時接続(WebSocket)が多数発生するシステムでは、ハンドシェイクの負荷分散が重要になります。

  2. セキュリティ(権限分離):
    443番ポート(特権ポート)をリッスンするには通常root権限が必要ですが、アプリケーション自体をrootで動かすことはセキュリティリスクとなります。Nginxのみを特権ポートで動かし、アプリケーションは一般ユーザー権限のポート(8000番など)で動かすことで、万が一アプリに脆弱性があっても影響を最小限に抑えられます。

  3. 運用の柔軟性(ゼロダウンタイム):
    証明書の更新時、Nginxなら nginx -s reload で接続を維持したまま設定を反映できますが、アプリケーションサーバーに組み込んでいる場合、プロセスの再起動(=全WebSocket切断)が必要になることが多いです。

3.1 構成図

本構成のアーキテクチャ図(SVG形式)を以下に示します。

TLS Architecture

3.2 通信フロー

CSからCSMSへの接続確立とメッセージ転送の流れをシーケンス図で示します。NginxがTLS終端(復号)を行い、バックエンドには平文で転送している様子がわかります。

4. 実装詳細

具体的な実装内容を見ていきます。

4.1 証明書の生成

本プロジェクトでは、環境に応じて適切な証明書生成アプローチをとっています。

ローカル開発環境 (scripts/generate_certs.sh)

ローカル環境では固定の設定ファイルを使用し、OpenSSLで自己署名証明書を生成します。

bash
# 秘密鍵の生成
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設定ファイルを動的生成する工夫を行っています。

bash
# 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へ転送します。

nginx
    # アップストリーム(転送先)の定義
    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;
        }
    }

とくに UpgradeConnection ヘッダーの設定は、HTTP通信をWebSocketプロトコルにアップグレードするために不可欠です。

4.3 Docker Compose (docker-compose.yml)

docker-compose で全体の構成を定義します。

yaml
  # 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)では、デフォルトでサーバー証明書の検証を行うセキュアな実装になっています。

  1. デフォルト動作(セキュア):
    ssl.create_default_context() を使用し、サーバー証明書が信頼されたCAによって署名されているか、ホスト名が一致するかを厳格に検証します。

  2. 開発用オプション(検証スキップ):
    自己署名証明書を使用する開発環境向けに、--no-verify-ssl フラグを提供しています。このフラグが有効な場合のみ、例外的に検証を無効化します。

  3. カスタムCAの対応:
    --ca-cert オプションにより、自己署名証明書を信頼するためのルートCAファイルを明示的に指定して検証を通すことも可能です。

5.3 Python実装例

以下は、シミュレーターでの実装ロジックの抜粋です。デフォルトでは厳格な検証を行い、オプション指定時のみ柔軟に対応する設計となっています。

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の理解と適切な実装は必須のスキルです。 この構成をベースに、さらに強固なセキュリティ(クライアント証明書認証など)へ発展させることも可能です。

この記事は役に立ちましたか?

フィードバックはブログの改善に活用させていただきます