Kafkaをメッセージキューとして、Trillianを追記専用のMerkleツリーとして使用し、内部PKIのための証明書トランスペアレンシーログシステムを構築します。このセットアップにより、証明書の発行を効率的に監査し、不正発行された証明書を検出し、ゴシップベースの一貫性チェックを行うことができます。
なぜ内部CTログが必要なのか?
技術的な詳細に入る前に、なぜ内部PKIのためにCTログを実装する必要があるのかを考えてみましょう。
- 不正な証明書発行の検出
- 内部ポリシーへの準拠の確保
- インシデント対応能力の向上
- 全体的なセキュリティ体制の強化
内部CAに対する「信頼するが検証する」アプローチと考えてください。CAを信頼しつつも、その正直さを保ちたいのです。
構築要素: KafkaとTrillian
内部CTログシステムを実装するために、以下の2つの強力なツールを使用します。
1. Apache Kafka
Kafkaはメッセージキューとして機能し、証明書データの高スループットな取り込みを処理します。これは証明書のためのコンベアベルトのようなもので、順序通りに高信頼で処理されることを保証します。
2. Trillian
Googleが開発したTrillianは、追記専用のMerkleツリーの実装です。これはCTログのバックボーンであり、ログの整合性に対する暗号学的保証を提供し、効率的な包含証明を可能にします。
アーキテクチャ概要
システムアーキテクチャを分解してみましょう。
+----------------+ +--------+ +----------+ +---------+
| Internal CA | --> | Kafka | --> | Trillian | --> | Auditor |
+----------------+ +--------+ +----------+ +---------+
| |
| |
v v
+----------------+ +--------------------+
| Monitor/Alert | | Gossip Participants|
+----------------+ +--------------------+
1. 内部CAが新しく発行した証明書をKafkaに送信します。
2. KafkaはTrillianへの順序通りの信頼性のある配信を保証します。
3. Trillianは証明書をそのMerkleツリーに追加します。
4. 監査者はログの一貫性を確認し、疑わしい証明書をチェックできます。
5. モニタリングシステムは異常を警告します。
6. ゴシップ参加者は複数のインスタンス間でログの一貫性を保証します。
システムの実装
ステップ1: Kafkaのセットアップ
まず、Kafkaクラスターをセットアップします。簡単にするためにDockerを使用します。
version: '3'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- 9092:9092
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
docker-compose up -d
を実行すると、証明書を取り込む準備が整ったKafkaクラスターが起動します。
ステップ2: Trillianの設定
次に、Trillianをセットアップします。ソースからコンパイルする必要があります。
git clone https://github.com/google/trillian.git
cd trillian
go build ./cmd/trillian_log_server
go build ./cmd/trillian_log_signer
Trillian用のMySQLデータベースを作成します。
CREATE DATABASE trillian;
データベーススキーマを初期化します。
mysql -u root -p trillian < storage/mysql/schema/storage.sql
次に、Trillianログサーバーとサイナーを起動します。
./trillian_log_server --logtostderr ...
./trillian_log_signer --logtostderr ...
ステップ3: 証明書サブミッターの実装
内部CAからKafkaに証明書を送信するコンポーネントが必要です。以下は簡単なGoの実装です。
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"github.com/segmentio/kafka-go"
)
func submitCertificate(cert *x509.Certificate) error {
w := kafka.NewWriter(kafka.WriterConfig{
Brokers: []string{"localhost:9092"},
Topic: "ct-log-entries",
})
pemCert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
return w.WriteMessages(context.Background(),
kafka.Message{
Key: []byte(cert.SerialNumber.String()),
Value: pemCert,
},
)
}
ステップ4: Trillianでの証明書処理
次に、Kafkaから証明書を消費し、Trillianに追加する必要があります。
package main
import (
"context"
"github.com/google/trillian"
"github.com/segmentio/kafka-go"
)
func processCertificates(logID int64) {
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "ct-log-entries",
GroupID: "trillian-processor",
})
client, err := trillian.NewTrillianLogClient(...)
if err != nil {
// エラー処理
}
for {
msg, err := r.ReadMessage(context.Background())
if err != nil {
// エラー処理
continue
}
leaf := &trillian.LogLeaf{
LeafValue: msg.Value,
}
_, err = client.QueueLeaf(context.Background(), &trillian.QueueLeafRequest{
LogId: logID,
Leaf: leaf,
})
if err != nil {
// エラー処理
}
}
}
ゴシップベースの一貫性の実装
複数のインスタンス間でCTログの一貫性を確保するために、ゴシッププロトコルを実装します。これにより、異なるログインスタンスがログのビューを比較し、矛盾を検出することができます。
ゴシッププロトコルの概要
- 各ログインスタンスは定期的に最新の署名付きツリーヘッド(STH)を一連のピアに送信します。
- ピアは受信したSTHを自分のものと比較します。
- 差異が検出された場合、ピアは一貫性証明を要求し、検証します。
- 矛盾があれば、さらなる調査のためにアラートが発生します。
以下はゴシッププロトコルの基本的な実装です。
package main
import (
"context"
"github.com/google/trillian"
"github.com/google/trillian/client"
"time"
)
type GossipParticipant struct {
LogID int64
Client trillian.TrillianLogClient
Verifier *client.LogVerifier
Peers []string
}
func (g *GossipParticipant) RunGossip() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
g.gossipRound()
}
}
func (g *GossipParticipant) gossipRound() {
ctx := context.Background()
sth, err := g.Client.GetLatestSignedLogRoot(ctx, &trillian.GetLatestSignedLogRootRequest{LogId: g.LogID})
if err != nil {
// エラー処理
return
}
for _, peer := range g.Peers {
peerSTH := getPeerSTH(peer) // ピアからSTHを取得する関数を実装
if !g.Verifier.VerifyRoot(sth.SignedLogRoot, peerSTH.SignedLogRoot) {
// STHが一致しない場合、一貫性証明を要求
proof, err := g.Client.GetConsistencyProof(ctx, &trillian.GetConsistencyProofRequest{
LogId: g.LogID,
FirstTreeSize: peerSTH.TreeSize,
SecondTreeSize: sth.TreeSize,
})
if err != nil {
// エラー処理
continue
}
// 一貫性証明を検証
if !g.Verifier.VerifyConsistencyProof(proof) {
// 矛盾が検出された場合、アラートを発生
raiseInconsistencyAlert(g.LogID, peer)
}
}
}
}
func raiseInconsistencyAlert(logID int64, peer string) {
// アラートメカニズムを実装(例:メール送信、インシデント対応のトリガー)
}
監査とモニタリング
CTログシステムが整ったら、疑わしい活動や矛盾を検出するために監査とモニタリングを実装する必要があります。
監査者の実装
監査者の役割は、定期的にログをチェックし、ポリシーに違反する証明書や疑わしい証明書を見つけることです。以下は基本的な実装です。
package main
import (
"context"
"crypto/x509"
"encoding/pem"
"github.com/google/trillian"
"time"
)
type Auditor struct {
LogID int64
Client trillian.TrillianLogClient
}
func (a *Auditor) AuditLog() {
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
a.auditRound()
}
}
func (a *Auditor) auditRound() {
ctx := context.Background()
leaves, err := a.Client.GetLeavesByRange(ctx, &trillian.GetLeavesByRangeRequest{
LogId: a.LogID,
StartIndex: 0,
Count: 1000, // 必要に応じて調整
})
if err != nil {
// エラー処理
return
}
for _, leaf := range leaves.Leaves {
block, _ := pem.Decode(leaf.LeafValue)
if block == nil {
// エラー処理
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
// エラー処理
continue
}
if isSuspiciousCertificate(cert) {
raiseSuspiciousCertificateAlert(cert)
}
}
}
func isSuspiciousCertificate(cert *x509.Certificate) bool {
// 疑わしい証明書のチェックを実装
// 例:
// - 予期しない発行者
// - 異常な有効期間
// - 禁止されたキー使用
// - 予期しないSAN
return false
}
func raiseSuspiciousCertificateAlert(cert *x509.Certificate) {
// 疑わしい証明書のアラートメカニズムを実装
}
モニタリングとアラート
CTログシステムの健康状態とパフォーマンスを監視するために、包括的なモニタリングとアラートを実装する必要があります。追跡すべき主要な指標は以下の通りです。
- ログサイズと成長率
- 証明書提出の遅延
- 証明書処理のエラーレート
- ゴシッププロトコルの一貫性チェック
- 監査者の発見とアラート
PrometheusやGrafanaのようなツールを使用して、これらの指標を収集し、可視化することができます。以下は、GoのPrometheusクライアントライブラリを使用して基本的な指標を公開する例です。
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
certificatesProcessed = promauto.NewCounter(prometheus.CounterOpts{
Name: "ct_log_certificates_processed_total",
Help: "処理された証明書の総数",
})
processingLatency = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "ct_log_processing_latency_seconds",
Help: "証明書処理の遅延",
Buckets: prometheus.DefBuckets,
})
gossipInconsistencies = promauto.NewCounter(prometheus.CounterOpts{
Name: "ct_log_gossip_inconsistencies_total",
Help: "検出されたゴシップの不一致の総数",
})
})
// これらの指標をコードで使用:
// certificatesProcessed.Inc()
// processingLatency.Observe(duration.Seconds())
// gossipInconsistencies.Inc()
結論: 信頼するが、検証し、ログを取る
内部PKIのために証明書トランスペアレンシーログを実装することは、一見すると過剰に思えるかもしれません。しかし、サイバーセキュリティの世界では、信頼が最も重要であり、侵害の結果が壊滅的である可能性があるため、安心のための小さな代償です。
高スループットのメッセージ処理のためにKafkaを、暗号学的整合性のためにTrillianを活用することで、以下を実現する堅牢なシステムを構築しました。
- 不正または誤発行された証明書を迅速に検出
- すべての証明書発行の不変の監査証跡を提供
- ゴシッププロトコルを通じて複数のログインスタンス間の一貫性を確保
- 疑わしい活動に対するプロアクティブなモニタリングとアラートを可能にする
PKIの領域では、信頼は良いが、検証はさらに良いです。この内部CTログシステムを実装することで、セキュリティ体制を改善するだけでなく、監査の精査や時間の試練に耐えうる検証可能な信頼の基盤を構築しています。
"神を信じる。我々は他のすべての者にデータを持ってくることを要求する。" - W. Edwards Deming
さあ、証明書をログに記録しましょう!未来の自分(と監査者)が感謝するでしょう。