如何在 gRPC/gRPC-Web 中设置和测试 TLS

2023-01-04   出处: ITNEXT  作/译者:Alexey Vasyukov/Yilia


本文包含 Node.JS 示例,但对其他语言也有帮助,让我们开始吧。
gRPC 连接类型,可以使用三种类型的 gRPC 连接:
1.不安全——所有数据未经加密传输。
2.服务器端 TLS — 类似浏览器的加密,其中只有服务器向客户端提供 TLS 证书。
3.相互 TLS — 最安全,服务器和客户端都相互提供证书。

创建自签名证书

  如果没有CA 的证书或想在本地主机上使用 TLS,则这是一个可选步骤。
  请注意,在生产环境中,大多数建议选择 CA 的证书以提高安全性,例如,可以从 Let’s Encrypt 获取免费证书。

rm *.pem
rm *.srl
rm *.cnf

# 1. Generate CA's private key and self-signed certificate
openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Test Org/OU=Test/CN=*.test/emailAddress=test@gmail.com"

echo "CA's self-signed certificate"
openssl x509 -in ca-cert.pem -noout -text

# 2. Generate web server's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=FR/ST=Ile de France/L=Paris/O=Server TLS/OU=Server/CN=*.tls/emailAddress=tls@gmail.com"

# Remember that when we develop on localhost, It’s important to add the IP:0.0.0.0 as an Subject Alternative Name (SAN) extension to the certificate.
echo "subjectAltName=DNS:*.tls,DNS:localhost,IP:0.0.0.0" > server-ext.cnf
# Or you can use localhost DNS and grpc.ssl_target_name_override variable
# echo "subjectAltName=DNS:localhost" > server-ext.cnf

# 3. Use CA's private key to sign web server's CSR and get back the signed certificate
openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf

echo "Server's signed certificate"
openssl x509 -in server-cert.pem -noout -text

# 4. Generate client's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=FR/ST=Alsace/L=Strasbourg/O=PC Client/OU=Computer/CN=*.client.com/emailAddress=client@gmail.com"

# Remember that when we develop on localhost, It’s important to add the IP:0.0.0.0 as an Subject Alternative Name (SAN) extension to the certificate.
echo "subjectAltName=DNS:*.client.com,IP:0.0.0.0" > client-ext.cnf

# 5. Use CA's private key to sign client's CSR and get back the signed certificate
openssl x509 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile client-ext.cnf

echo "Client's signed certificate"
openssl x509 -in client-cert.pem -noout -text

准备工作

  让我们从实现gRPC 服务开始。首先,我们将定义服务协议缓冲区文件。

syntax = "proto3";

package tls_service.v1;

message SimpleMessage {
  string id = 1;
}

service TLSService {
  rpc Unary(SimpleMessage) returns (SimpleMessage);
}

  其次,我们需要生成类型、服务和客户端定义。对于 TypeScript,我更喜欢使用 ts-proto,但你可以根据语言选择任何你喜欢的工具。

protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=env=node,outputServices=grpc-js --ts_proto_out=./src/generated ./proto/tls_service.proto

现在可以在Node.js上实现server程序了。

gRPC

我们将为服务器端和客户端使用官方的@grpc/grpc-js包。

Server-Side TLS

服务器端 TLS 只需要服务器证书及其私钥。

Server

import { Server, ServerCredentials } from '@grpc/grpc-js';
import * as fs from 'fs';
import * as path from 'path';

import { TLSServiceServer, TLSServiceService } from './generated/proto/tls_service';

const TLSService: TLSServiceServer = {
  unary(call, callback) {
    callback(null, call.request);
  },
};

function getServerCredentials(): ServerCredentials {
  const serverCert = fs.readFileSync(path.resolve(__dirname, '../certs/server-cert.pem'));
  const serverKey = fs.readFileSync(path.resolve(__dirname, '../certs/server-key.pem'));

  const serverCredentials = ServerCredentials.createSsl(
    null,
    [
      {
        cert_chain: serverCert,
        private_key: serverKey,
      },
    ],
    false
  );

  return serverCredentials;
}

function main() {
  const server = new Server();

  const serverCredentials = getServerCredentials();

  server.addService(TLSServiceService, TLSService);

  server.bindAsync('0.0.0.0:4000', serverCredentials, () => {
    server.start();

    console.log('gRPC server started on 0.0.0.0:4000');
  });
}

main();

Client

  现在我们可以实现客户端。这里要注意,如果你的TLS证书是用CA签发的(不是Self-Signed),就不需要在客户端提供这个证书; 它应该自动工作。

import { ChannelCredentials } from '@grpc/grpc-js';
import * as fs from 'fs';
import * as path from 'path';

import { TLSServiceClient } from './generated/proto/tls_service';

function getChannelCredentials(): ChannelCredentials {
  const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem'));

  // If you use CA root certificate
  // const channelCredentials = ChannelCredentials.createSsl();

  // If you use Self-Signed root certificate you need to provide it
  const channelCredentials = ChannelCredentials.createSsl(rootCert);

  return channelCredentials;
}

function main() {
  const credentials = getChannelCredentials();

  const client = new TLSServiceClient('0.0.0.0:4000', credentials);

  client.unary({ id: 'test' }, (error, response) => {
    // eslint-disable-next-line no-console
    console.log('response: ', response);
  });
}

main();

Mutual TLS

  Mutual TLS 需要根证书、服务器证书及其私钥。根证书将在此处用于检查客户端证书是否已签名,并且服务器可以信任客户端。

Server

  与Server-Side部分一样,仅更改了 getServerCredentials 功能。

function getServerCredentials(): ServerCredentials {
  const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem'));
  const serverCert = fs.readFileSync(path.resolve(__dirname, '../certs/server-cert.pem'));
  const serverKey = fs.readFileSync(path.resolve(__dirname, '../certs/server-key.pem'));

  const serverCredentials = ServerCredentials.createSsl(
    rootCert,
    [
      {
        cert_chain: serverCert,
        private_key: serverKey,
      },
    ],
    true
  );

  return serverCredentials;
}

Client

  与Server-Side部分一样,仅更改了 getChannelCredentials 函数。

function getChannelCredentials(): ChannelCredentials {
  const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem'));
  const clientCert = fs.readFileSync(path.resolve(__dirname, '../certs/client-cert.pem'));
  const clientKey = fs.readFileSync(path.resolve(__dirname, '../certs/client-key.pem'));

  const channelCredentials = ChannelCredentials.createSsl(rootCert, clientKey, clientCert);

  return channelCredentials;
}

覆盖 SSL 目标名称

   @grpc/grpc-js包还提供了一些可以设置的有用选项。grpc.ssl_target_name_override — 当代理后面的实际服务器和 CN 不匹配时,这很有帮助。要设置通道选项,需要将它们传递给客户端构造函数的第三个参数。

gRPC-Web

言而总之

你应该知道的事情:
  1.工作方案:Client ↔ Proxy [HTTP(S) gRPC-Web] ↔ Server (gRPC)
  2.有两种实现——官方 gRPC-Web 和@improbable-eng/grpc-web。
  3.目前 gRPC-Web 仅支持通过 HTTP(S) 的一元请求和服务器流请求。
  此外,@improbable-eng/grpc-web 支持客户端和双向流式传输,带有实验性的 websocket 传输。 这不是 gRPC-Web 规范的一部分,不建议用于生产。
  4.有两个代理——来自官方 gRPC-Web 的带有 gRPC-Web 过滤器的 Envoy 和来自 @improbable-eng 的 grpcwebproxy。
  5.可以将任一客户端与任一代理一起使用。
  6.客户端有不同的通信传输。官方 gRPC-web 仅支持 XMLHttpRequest。@improbable-eng/grpc-web 还支持 Fetch(如果可用就使用它)并且可以使用自定义传输进行扩展,例如 Node.js。
@improbable-eng/grpc-web 包在过去 10 个月没有更新,所以如果你没有计划使用 gRPC-Web 自定义传输或在 Node.js 环境中,我认为你应该选择官方 gRPC-Web client。我在官方包中创建了功能请求以添加 Node.js support 链接
在这个例子中,我们将使用在 docker 中运行的 Envoy 代理。

Server-Side TLS

Server

  我们需要在代理后面启动我们的 gRPC 服务。 因此无需更改,只需使用之前讨论的服务器端 TLS 启动服务即可。之后,需要设置 envoy 并启动它。

services:
  envoy-server:
    image: envoyproxy/envoy:v1.22.0
    ports:
      - 8080:8080
    volumes:
      - ./envoy-server.yaml:/etc/envoy/envoy.yaml:ro
      - ./certs/server-cert.pem:/etc/server-cert.pem
      - ./certs/server-key.pem:/etc/server-key.pem
docker-compose.yaml
static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 8080 }
      filter_chains:
        - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              codec_type: auto
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains: ["*"]
                    routes:
                      - match: { prefix: "/" }
                        route:
                          cluster: simple_service
                          timeout: 0s
                          max_stream_duration:
                            grpc_timeout_header_max: 0s
                    cors:
                      allow_origin_string_match:
                        - prefix: "*"
                      allow_methods: GET, PUT, DELETE, POST, OPTIONS
                      allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                      max_age: "1728000"
                      expose_headers: custom-header-1,grpc-status,grpc-message
              http_filters:
                - name: envoy.filters.http.grpc_web
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
                - name: envoy.filters.http.cors
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
                - name: envoy.filters.http.router
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              # https://www.envoyproxy.io/docs/envoy/v1.15.0/api-v3/extensions/transport_sockets/tls/v3/tls.proto#extensions-transport-sockets-tls-v3-downstreamtlscontext
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      # Certificate must be PEM-encoded
                      filename: /etc/server-cert.pem
                    private_key:
                      filename: /etc/server-key.pem
  clusters:
    - name: simple_service
      connect_timeout: 0.25s
      type: logical_dns
      http2_protocol_options: {}
      lb_policy: round_robin
      load_assignment:
        cluster_name: cluster_0
        endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    # address: host.docker.internal - for macOS
                    # address: 0.0.0.0 - for others
                    address: host.docker.internal
                    port_value: 4000
      # http2_protocol_options: {} # Force HTTP/2
      # Your grpc server communicates over TLS. You must configure the transport
      # socket. If you care about the overhead, you should configure the grpc
      # server to listen without TLS. If you need to listen to grpc-web and grpc
      # over HTTP/2 both you can also proxy your TCP traffic with the envoy.
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
envoy-server.yaml

docker-compose up envoy-server

  这样带有Server-Side TLS的 gRPC-Web 代理在 https://0.0.0.0:8080 就可用了。

Mutual TLS

Server

  这里的 hack 是使用Server-Side TLS启动 gRPC 服务,但在envoy侧需要检查由受信任的 CA 签名的客户端证书。

services:
  envoy-mutual:
    image: envoyproxy/envoy:v1.22.0
    ports:
      - 8080:8080
    volumes:
      - ./envoy-mutual.yaml:/etc/envoy/envoy.yaml:ro
      - ./certs/ca-cert.pem:/etc/ca-cert.pem
      - ./certs/server-cert.pem:/etc/server-cert.pem
      - ./certs/server-key.pem:/etc/server-key.pem
docker-compose.yaml
static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 8080 }
      filter_chains:
        - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              codec_type: auto
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains: ["*"]
                    routes:
                      - match: { prefix: "/" }
                        route:
                          cluster: simple_service
                          timeout: 0s
                          max_stream_duration:
                            grpc_timeout_header_max: 0s
                    cors:
                      allow_origin_string_match:
                        - prefix: "*"
                      allow_methods: GET, PUT, DELETE, POST, OPTIONS
                      allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                      max_age: "1728000"
                      expose_headers: custom-header-1,grpc-status,grpc-message
              http_filters:
                - name: envoy.filters.http.grpc_web
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
                - name: envoy.filters.http.cors
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
                - name: envoy.filters.http.router
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              # https://www.envoyproxy.io/docs/envoy/v1.15.0/api-v3/extensions/transport_sockets/tls/v3/tls.proto#extensions-transport-sockets-tls-v3-downstreamtlscontext
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              require_client_certificate: true
              common_tls_context:
                tls_certificates:
                  - certificate_chain:
                      # Certificate must be PEM-encoded
                      filename: /etc/server-cert.pem
                    private_key:
                      filename: /etc/server-key.pem
                validation_context:
                  only_verify_leaf_cert_crl: true
                  trusted_ca:
                    filename: /etc/ca-cert.pem
  clusters:
    - name: simple_service
      connect_timeout: 0.25s
      type: logical_dns
      http2_protocol_options: {}
      lb_policy: round_robin
      load_assignment:
        cluster_name: cluster_0
        endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    # address: host.docker.internal - for macOS
                    # address: 0.0.0.0 - for others
                    address: host.docker.internal
                    port_value: 4000
      # http2_protocol_options: {} # Force HTTP/2
      # Your grpc server communicates over TLS. You must configure the transport
      # socket. If you care about the overhead, you should configure the grpc
      # server to listen without TLS. If you need to listen to grpc-web and grpc
      # over HTTP/2 both you can also proxy your TCP traffic with the envoy.
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
          common_tls_context:
            validation_context:
              trusted_ca:
                filename: /etc/ca-cert.pem
envoy-mutual.yaml

docker-compose up envoy-mutual

这样带有 Mutual TLS 的 gRPC-Web 代理在 https://0.0.0.0:8080 就可用了。

测试 gRPC 和 gRPC-Web 请求


  最近我发布了一个名为 ezy 的多平台桌面 gRPC / gRPC-Web 客户端。 我每天都在使用 gRPC,但没有用于 gRPC 测试的功能齐全的 UI/UX 完美客户端,所以我尝试创建一个。
  该客户端具有对 gRPC / gRPC-Web 的全面支持。你可以尝试一下。


声明:本文为本站编辑转载,文章版权归原作者所有。文章内容为作者个人观点,本站只提供转载参考(依行业惯例严格标明出处和作译者),目的在于传递更多专业信息,普惠测试相关从业者,开源分享,推动行业交流和进步。 如涉及作品内容、版权和其它问题,请原作者及时与本站联系(QQ:1017718740),我们将第一时间进行处理。本站拥有对此声明的最终解释权!欢迎大家通过新浪微博(@测试窝)或微信公众号(测试窝)关注我们,与我们的编辑和其他窝友交流。
223° /2230 人阅读/0 条评论 发表评论

登录 后发表评论