証明書の発行と設定
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 経由でリクエストをバックエンドへ中継します。