境界を越えて:NginxによるmTLS導入から6ヶ月間の振り返り

Security tutorial - IT technology blog
Security tutorial - IT technology blog

脆弱な内部ネットワーク:高くつく「神話」

長年、「内部ネットワーク」はゲートで囲まれた高級住宅地のように扱われてきました。私たちは、ファイアウォールやWAF、レート制限といった「ゲート」には多額の費用を投じてきましたが、肝心の家々の「玄関」は開け放したままでした。内部のマイクロサービスは、プレーンなHTTPや基本的な一方向TLSで通信していました。その論理とは、「ハッカーが一度内部に侵入してしまえば、もうおしまいだ」というものです。しかし、この考え方は非常に危険な賭けであり、ゼロトラスト(Zero Trust)アーキテクチャがようやくこれに終止符を打とうとしています。

私が目を覚ますきっかけとなったのは、教科書の中ではなく、ある日の午前3時のことでした。一見「隠されている」はずのステージングサーバーで、4万5000回ものSSHログイン試行の失敗を示す自動アラートが鳴り響いたのです。境界防御は決して保証ではありません。もし一つのサービスが突破されれば、攻撃者はスタック全体を幽霊のように横移動(ラテラルムーブメント)できてしまいます。相互TLS(mTLS)は、この「脆弱な内部(soft interior)」問題を解決します。これは、1バイトのデータをやり取りする前に、すべてのサービスに暗号化された身分証明書の提示を強制するものです。

過去6ヶ月間にわたり24のマイクロサービスでmTLSを運用してきて、セキュリティ面での成果と運用面での苦労(operational scars)を目の当たりにしました。ここでは、NginxとOpenSSLを使用して、いかにして堅牢な認証レイヤーを構築したかを紹介します。

双方向ハンドシェイク:盲目的な信頼からの脱却

標準的なTLSは、用心棒にIDを見せるようなものですが、用心棒が自分のIDをあなたに見せることはありません。ブラウザはサーバーを確認しますが、サーバーはあなたが誰であるかを知りません。マイクロサービスのメッシュにおいて、これは大きな死角となります。サービスAがサービスBにデータを要求しても、サービスBは基本的にサービスAの言葉を鵜呑みにしているに過ぎません。

mTLSがいかにしてセキュリティを強化するか

相互TLS(mTLS)は、双方向のハンドシェイクを要求します。サーバーがクライアントを検証し、クライアントもサーバーを検証します。署名が独自のプライベート認証局(CA)と一致しない場合、接続は即座に切断されます。これにより、ネットワークは暗号化によって強制されたアイデンティティシステムへと変貌します。

これを実現するには、以下の3つの柱が必要です。

  • プライベートCA: すべての証明書に署名する、内部の「信頼の起点(root of trust)」です。
  • サーバー証明書: リクエストを受信するサービスの身元証明です。
  • クライアント証明書: 呼び出しを行うサービスの身元証明です。

実装:OpenSSLとNginxによるmTLS

私たちはNginxをサイドカープロキシとして使用しました。これにより、PythonやGoのコードをクリーンに保つことができます。アプリケーションは暗号化が行われていることすら知りません。Nginxが複雑な計算処理を担い、アプリ側にはローカルなトラフィックとして見えるだけです。

ステップ1:信頼の起点の構築

内部トラフィックには、Let’s EncryptのようなパブリックCAは避けましょう。内部IPをうまく処理できませんし、大規模な運用ではコストもかさみます。代わりに、独自のルート証明書を生成します。

# CAのプライベートキーを作成
openssl genrsa -out internal-ca.key 4096

# ルート証明書を作成(10年間有効)
openssl req -x509 -new -nodes -key internal-ca.key -sha256 -days 3650 -out internal-ca.crt \
    -subj "/CN=Internal-CA/O=DevOps"

この internal-ca.crt が「アンカー」となります。すべてのサービスが、相手を検証するためにこのコピーを必要とします。

ステップ2:サーバー証明書の発行(Order API)

私たちの「Order API」のために、鍵と署名リクエスト(CSR)を生成し、自前のCAで署名します。

# 鍵とCSRを生成
openssl genrsa -out order-api.key 2048
openssl req -new -key order-api.key -out order-api.csr -subj "/CN=order-api.internal"

# 内部CAで署名
openssl x509 -req -in order-api.csr -CA internal-ca.crt -CAkey internal-ca.key \
    -CAcreateserial -out order-api.crt -days 365 -sha256

ステップ3:クライアント証明書の発行(Frontendアプリ)

「Frontend」がOrder APIとの通信を許可されていることを証明するために、独自の認証情報が必要になります。

# クライアントキーとCSRを生成
openssl genrsa -out frontend.key 2048
openssl req -new -key frontend.key -out frontend.csr -subj "/CN=frontend-app"

# 有効なクライアントとして署名
openssl x509 -req -in frontend.csr -CA internal-ca.crt -CAkey internal-ca.key \
    -CAcreateserial -out frontend.crt -days 365 -sha256

ステップ4:Nginxによる強制レイヤー

ここでルールを設定します。Order APIサーバーにおいて、接続を試みるすべての人に証明書を要求するようNginxに指示します。

server {
    listen 443 ssl;
    server_name order-api.internal;

    ssl_certificate /etc/nginx/certs/order-api.crt;
    ssl_certificate_key /etc/nginx/certs/order-api.key;

    # クライアントを検証するためのCA
    ssl_client_certificate /etc/nginx/certs/internal-ca.crt;
    
    # 双方向認証を必須にする
    ssl_verify_client on;

    location / {
        proxy_set_header X-Client-ID $ssl_client_s_dn;
        proxy_pass http://localhost:8080;
    }
}

ssl_verify_client on; を設定することで、NginxはCAによって署名された有効な証明書を持たないリクエストをすべてブロックします。呼び出し側は、アプリケーションにリクエストが届く前に 400 Bad Request (SSL Certificate Required) エラーを受け取ることになります。

ステップ5:cURLによるテスト

標準的なリクエストは失敗するようになります。身元証明のためのファイルを提供する必要があります。

# これはSSLエラーで失敗します
curl https://order-api.internal

# これは成功します
curl --cacert internal-ca.crt \
     --cert frontend.crt \
     --key frontend.key \
     https://order-api.internal

現場からの厳しい現実

mTLSは驚異的なセキュリティを提供しますが、「一度設定すれば終わり」というものではありません。本番環境での6ヶ月間で私たちが学んだことを紹介します。

1. 自動化は必須

当初は1年有効の証明書から始めました。しかし3ヶ月目には、24のサービスにわたって証明書を手動で更新するのが悪夢となりました。最終的には HashiCorp Vault を統合して、発行を自動化しました。5つ以上のサービスがあるなら、手動で行うのはやめましょう。有効期限を必ず忘れ、午前2時に障害を発生させることになります。

2. オブザーバビリティ(観測可能性)の代償

すべてが暗号化されると、標準の tcpdump は役に立ちません。ペイロードが見えなくなるからです。私たちはNginxのロギングを改善し、特に $ssl_client_verify 変数を記録するようにしました。これにより、接続失敗の原因が証明書の期限切れなのか、設定ミスなのかを素早く判断できるようになりました。

3. レイテンシとCPU

新しい接続ごとに平均12msから18msのハンドシェイクレイテンシが発生しました。秒間5,000リクエストを処理する高負荷なサービスでは、これによりCPU使用率が15%上昇しました。パフォーマンスを軽快に保つためには、ssl_session_cache の調整と接続の維持(HTTP Keep-Alive)が極めて重要です。

最後に

mTLSへの移行は、最も効果的なセキュリティ対策の一つです。盗まれたAPIキーを無効化し、攻撃者が被害を与えるには物理的な証明書とプライベートキーの両方が必要になります。セキュリティの負担を開発者からインフラへと移すことができます。スイッチを入れる前に、証明書のライフサイクル管理の計画だけはしっかり立てておきましょう。

Share: