Skip to content
Alexey Vasyukov edited this page Aug 29, 2022 · 8 revisions

How TLS works

The best comic-article to understand how TLS works.

gRPC connection types

There are three types for gRPC connections you can use

  1. Insecure (all data transmitted without encryption)
  2. Server-Side TLS (browser like encryption, where only the server provides TLS certificate to the client)
  3. Mutual TLS (most secure, both the server and the client provides there certificates to each other)

Creating Self-Signed certificates

Optional, you can use CA

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 usee 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

Implementing Server-Side encryption

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

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
);

const server = new Server();

server.addService(...);

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

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

Implementing Mutual encryption

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

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
);

const server = new Server();

server.addService(...);

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

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

gRPC-Web

Read the grpc.io blog post about The state of gRPC-web

## Compatibility Issues 
Of course, with two different proxies also come compatibility issues.
Fortunately, these have recently been ironed out, so you can expect to use either client with either proxy.

Implementing Server-Side encryption

Start your gRPC service with Server-Side encryption enabled.

Then start envoy proxy with gRPC-Web filters:

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: 0.0.0.0
                    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

Implementing Mutual encryption

Start your gRPC service with Server-Side encryption enabled.

Then start envoy proxy with gRPC-Web filters:
Note here that on envoy side we are enabling client certificate verification.

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: 0.0.0.0
                    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