t.marcusの外部記憶装置

忘備録とかちょっとした考えとかをつらつらと...

gRPC over SSL/TLS with ClientCertification

gRPCを利用したサービスを作ることになったので、その際に調べた認証・認可周りのメモ

キーワード:grpc, grpcs, client認証

gRPCにおける認証方法

gRPCの認証は公式サイトによると以下の方式がビルトインされてるらしい

また、認証情報は以下の方式で設定できる

  • Channel credential
    • SSL証明書などのように gRPC Channel に紐付ける方式
  • Call credential
    • Request時にClientContextに対して設定する方式

今回はgRPCServiceをグローバルに公開する予定があったのと通信を暗号化したかったので、クライアントに自己署名証明書(通称、オレオレ証明書)を払い出して、その証明書のCommonNameで認可を行う SSL/TLS + Channel credential の方式で試してみた。

[利用方法のイメージ]
               +------- Internet ---------+               
+---------+    |                          |
| Client  |  -------(gRPC over SSL/TLS)---------> [gRPCService]
+---------+    |    ( with ClientCert )   | :443
               +--------------------------+

準備

オレオレ認証局

クライアントに払い出す証明書を生成する必要があるので、オレオレ認証局オレオレ証明書の発行はこちらのQiita記事を参考にキーの出力フォーマットをなどを調整したものをGithubに上げた

https://github.com/tmarcus87/grpc-training-self-signed-ca

クライアントコード

Serverが1台の場合

以下のような感じでSSLで待ち受けるサーバを実装して起動すると動くはず

Serverが複数台の場合

公式のドキュメントによるとLoadBalancingは以下の3種の方法が提案されている

  • Proxy方式
  • 負荷分散に対応したClientを利用する方式 GoとJavaだとそういう対応クライアントがあるらしい(未検証)
  • 外部のLoadBalcningServiceを利用する方式
    • ちゃんと読んでないけど、EtcdとかZooKeeperとかEurekaとか使ってDNS応答を変化させてやる方式?

Proxy以外の方式では1IP:1Serverである必要があるっぽい?

Proxy方式でnginxを利用する場合以下の選択肢がある

  • L4 load balancing
    • TCPセッション単位のバランシング
    • TCPセッション単位のバランシングになるので、クライアント数が少ないと負荷分散が難しそう
    • SSL終端とクライアント認証はgRPCServerで行う
  • L7 load balancing
    • gRPCリクエスト単位のバランシングになる(設定次第?)
    • SSL終端とクライアント認証はnginxで行う
    • nginxのconfigでクライアント証明書の情報をアプリ側にmetadata(http header)として流す
    • nginx <--> gRPCサーバ間も暗号化しようと思えば可能ぽっい(あくまでnginx<--> grpcServer間の暗号化でclient<-->nginx間をのデータをそのままバイパスできないっぽい)

// todo Plain接続の場合のアプリ側のコードを書く

// todo L4 Load balancingの場合のnginx.confを貼り付ける

L7 Load balancingの場合のnginx.confは以下の通り

[nginxの設定]
$ cat /etc/nginx/nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log debug;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    upstream grpc_servers {
        server grpc_server1:5000;
        server grpc_server2:5000;
    }

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''        close;
    }

    server {
        listen 443 ssl http2;

        ssl_certificate /opt/server/dev-gateway-api.example.com.crt;
        ssl_certificate_key /opt/server/dev-gateway-api.example.com.pem;

        ssl_client_certificate /opt/ca/ca.crt;
        ssl_verify_client on;

        proxy_set_header ssl-client-cert        $ssl_client_escaped_cert;
        proxy_set_header ssl-client-verify      $ssl_client_verify;
        proxy_set_header ssl-client-subject-dn  $ssl_client_s_dn;
        proxy_set_header ssl-client-issuer-dn   $ssl_client_i_dn;
        grpc_set_header  ssl-client-cert        $ssl_client_escaped_cert;
        grpc_set_header  ssl-client-verify      $ssl_client_verify;
        grpc_set_header  ssl-client-subject-dn  $ssl_client_s_dn;
        grpc_set_header  ssl-client-issuer-dn   $ssl_client_i_dn;

        location /error502grpc {
            internal;
            default_type application/grpc;
            add_header grpc-status 14;
            add_header grpc-message "unavailable";
            return 204;
        }

        location / {
            grpc_pass grpc://grpc_servers;
            
            error_page 502 = /error502grpc;
        }
    }
}

サーバ側で以下のデータがmetadataから取れる

こんな感じの関数をUnaryServerInterceptorに差し込めば
func getMetaFromHeader(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    if md, ok := metadata.FromIncomingContext(ctx); ok {
        json, _ := json.Marshal(md)
        fmt.Println(string(json))
    }
    return handler(ctx, req)
}

こんなデータが取れるはず

{
  ":authority": [
    "grpc_servers"
  ],
  "content-type": [
    "application/grpc"
  ],
  "ssl-client-cert": [
    "-----BEGIN%20CERTIFICATE-----なんやかんや証明書の中身%0A-----END%20CERTIFICATE-----%0A"
  ],
  "ssl-client-issuer-dn": [
    "emailAddress=grpc@example.com,CN=gateway-gateway.example.com,OU=DevelopmentHQ,O=Hoge\\,Inc.,L=Sumida-ku,ST=Tokyo,C=JP"
  ],
  "ssl-client-subject-dn": [
    "emailAddress=service1@example.com,CN=service1,OU=DevelopmentHQ,O=Hoge\\,Inc.,L=Sumida-ku,ST=Tokyo,C=JP"
  ],
  "ssl-client-verify": [
    "SUCCESS"
  ],
  "user-agent": [
    "grpc-go/1.15.0"
  ]
}