gRPC通信におけるmTLS(相互TLS)認証の実装ガイド

証明書の発行と設定

gRPCサーバー(想定ドメイン: grpc.local.net)とクライアント間の通信を相互TLS(mTLS)で保護する手順を解説します。まず、認証局(CA)、サーバー用、クライアント用の証明書チェーンを構築します。

1. ルートCA証明書の生成

以下のOpenSSLコマンドを使用して、RSA 4096ビットの秘密鍵と自己署名ルート証明書を作成します。実運用ではパスフレーズ保護を推奨しますが、開発環境では -nodes を付与してパスワードを省略可能です。

openssl req -x509 -newkey rsa:4096 -nodes -keyout root-ca.key -out root-ca.crt -subj "/CN=InternalRootCA" -days 3650

2. 拡張設定ファイルの作成

証明書要求(CSR)発行時にSAN(Subject Alternative Name)を適用するための設定ファイル tls_ext.cnf を用意します。

[req]
req_extensions = v3_req
distinguished_name = req_dn
prompt = no

[req_dn]
C = JP
ST = Tokyo
L = Minato
O = DevOps Team
CN = grpc.local.net

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = grpc.local.net
IP.1 = 127.0.0.1

3. サーバー用鍵と証明書の発行

CSRを作成後、ルートCAで署名を行います。

openssl req -newkey rsa:2048 -nodes -keyout svc-server.key -out svc-server.csr -subj "/CN=grpc.local.net" -config tls_ext.cnf
openssl x509 -req -in svc-server.csr -out svc-server.crt -CA root-ca.crt -CAkey root-ca.key -CAcreateserial -days 365 -extensions v3_req -extfile tls_ext.cnf

4. クライアント用鍵と証明書の発行

クライアント認証用に別ファイル名で同手順を実行します。

openssl req -newkey rsa:2048 -nodes -keyout svc-client.key -out svc-client.csr -subj "/CN=grpc-client-app" -config tls_ext.cnf
openssl x509 -req -in svc-client.csr -out svc-client.crt -CA root-ca.crt -CAkey root-ca.key -CAcreateserial -days 365 -extensions v3_req -extfile tls_ext.cnf

Protobuf定義とGoスタブ生成

RPCのインターフェースを定義します。ping.proto を作成し、以下のように記述します。

syntax = "proto3";
option go_package = "./pb";

service Pinger {
  rpc Check (PingRequest) returns (PingResponse);
}

message PingRequest {
  string target = 1;
}

message PingResponse {
  string status = 1;
}

コンパイラーを使用してGo用のコードを出力します。

protoc --go_out=./pb --go_opt=paths=source_relative \
       --go-grpc_out=./pb --go-grpc_opt=paths=source_relative \
       ping.proto

mTLS対応サーバー実装

Go言語でgRPCサーバーを構築し、クライアント証明書の検証を必須化する設定を適用します。

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"flag"
	"fmt"
	"log"
	"net"
	"os"

	pb "example/ping/pb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

var (
	srvPort    = flag.Int("listen", 50051, "Server listening port")
	certFile   = flag.String("cert", "svc-server.crt", "Server certificate path")
	keyFile    = flag.String("key", "svc-server.key", "Server private key path")
	caCertFile = flag.String("ca", "root-ca.crt", "CA certificate path")
)

type pingerImpl struct {
	pb.UnimplementedPingerServer
}

func (p *pingerImpl) Check(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error) {
	log.Printf("[Server] Received ping for: %s", req.GetTarget())
	return &pb.PingResponse{Status: "ACK: " + req.GetTarget()}, nil
}

func main() {
	flag.Parse()

	cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
	if err != nil {
		log.Fatalf("Failed to load server TLS pair: %v", err)
	}

	caBytes, err := os.ReadFile(*caCertFile)
	if err != nil {
		log.Fatalf("Failed to read CA certificate: %v", err)
	}

	certPool := x509.NewCertPool()
	if !certPool.AppendCertsFromPEM(caBytes) {
		log.Fatal("Failed to populate CA certificate pool")
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{cert},
		ClientCAs:    certPool,
		ClientAuth:   tls.RequireAndVerifyClientCert,
	}

	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *srvPort))
	if err != nil {
		log.Fatalf("Network bind failed: %v", err)
	}

	srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
	pb.RegisterPingerServer(srv, &pingerImpl{})

	log.Printf("mTLS gRPC server listening on :%d", *srvPort)
	if err := srv.Serve(lis); err != nil {
		log.Fatalf("Server execution terminated: %v", err)
	}
}

ビルドおよび起動:

go build -o server.out
./server.out -cert svc-server.crt -key svc-server.key -ca root-ca.crt -listen 50051

mTLS対応クライアント実装

クライアント側でも証明書を提示し、サーバーのホスト名検証を有効化します。

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"flag"
	"log"
	"os"
	"time"

	pb "example/ping/pb"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

var (
	targetHost = flag.String("addr", "grpc.local.net:50051", "gRPC endpoint")
	verifyHost = flag.String("host", "grpc.local.net", "TLS hostname verification")
	cliCert    = flag.String("clicert", "svc-client.crt", "Client certificate file")
	cliKey     = flag.String("clikey", "svc-client.key", "Client private key file")
	caCert     = flag.String("cacert", "root-ca.crt", "Root CA certificate file")
	pingVal    = flag.String("name", "test-node", "Request identifier")
)

func main() {
	flag.Parse()

	cliPair, err := tls.LoadX509KeyPair(*cliCert, *cliKey)
	if err != nil {
		log.Fatalf("Client certificate load failed: %v", err)
	}

	caData, err := os.ReadFile(*caCert)
	if err != nil {
		log.Fatalf("CA read error: %v", err)
	}

	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(caData) {
		log.Fatal("CA pool initialization failed")
	}

	tlsConf := &tls.Config{
		ServerName:   *verifyHost,
		Certificates: []tls.Certificate{cliPair},
		RootCAs:      pool,
	}

	opts := []grpc.DialOption{
		grpc.WithTransportCredentials(credentials.NewTLS(tlsConf)),
	}

	conn, err := grpc.Dial(*targetHost, opts...)
	if err != nil {
		log.Fatalf("Connection failed: %v", err)
	}
	defer conn.Close()

	client := pb.NewPingerClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	resp, err := client.Check(ctx, &pb.PingRequest{Target: *pingVal})
	if err != nil {
		log.Fatalf("RPC invocation failed: %v", err)
	}
	log.Printf("Server response: %s", resp.GetStatus())
}

ビルドおよび実行:

go build -o client.out
./client.out -addr grpc.local.net:50051 -host grpc.local.net -clicert svc-client.crt -clikey svc-client.key -cacert root-ca.crt -name "app-01"

Nginxリバースプロキシ構成

ネットワーク構成上、クライアントとgRPCサーバーが直接通信できない場合、Nginxを中間プロキシとして配置します。このアーキテクチャでは、クライアントとNginx間でmTLSを終端し、Nginxからバックエンドサーバーへは内部ネットワーク経由で転送します。

バックエンドのgRPCサーバー実装からはTLS関連コードを削除し、標準の grpc.NewServer() を使用するように修正します。

Nginxの設定例は以下の通りです。

server {
    listen       443 ssl http2;
    server_name  grpc.local.net;

    ssl_certificate         /etc/nginx/ssl/svc-server.crt;
    ssl_certificate_key     /etc/nginx/ssl/svc-server.key;
    ssl_client_certificate  /etc/nginx/ssl/root-ca.crt;
    ssl_verify_client       on;

    location / {
        grpc_pass grpc://10.0.0.10:50051;
        grpc_read_timeout 30s;
        grpc_connect_timeout 5s;
    }
}

クライアント側のコード変更は不要です。Nginxが ssl_verify_client on によりクライアント証明書の検証を処理し、認証成功後に grpc_pass 経由でリクエストをバックエンドへ中継します。

タグ: gRPC mTLS OpenSSL Go nginx

9月8日 13:53 投稿