grpc / grpc-node

gRPC for Node.js
https://grpc.io
Apache License 2.0
4.5k stars 651 forks source link

Support gRPC-over-HTTPS using HTTPS proxy. #1295

Open zamnuts opened 4 years ago

zamnuts commented 4 years ago

PR #1243 introduced proxy support allowing for gRPC-over-HTTP, i.e. an unencrypted proxy. While gRPC is encrypted by means of HTTP/2, and remains so when tunneled, the initial connection to the proxy is not. See https://github.com/grpc/grpc-node/blob/%40grpc/grpc-js%400.7.0/packages/grpc-js/src/http_proxy.ts#L133-L145

The initial HTTP CONNECT call contains two pieces of potentially private/secret data:

For these reasons it may be desirable to encrypt the CONNECT traffic via HTTPS.

Additionally, the current implementation ignores the protocol defined in the proxy configuration (only PROXY_INFO.address is used). When https is defined, this client will still attempt to connect over http. If the proxy is expecting to negotiate TLS on the defined port (URL.host), it will error unexpectedly.

Solution

Detect the presence of HTTP or HTTPS via URL.protocol and make the CONNECT request accordingly:

// pseudo-code
import * as http from 'http';
import * as https from 'https';

interface ProxyInfo {
  address?: string;
  creds?: string;
  protocol?: string;
}

const schemeLib = PROXY_INFO.protocol === 'http' ? http : https; // secure by default
const request = schemeLib.request(options);
request.once('connect', (res, socket, head) => {/*...*/});

Alternative

Be explicit about the lack of HTTPS support by both documenting, and detecting the usage of HTTPS and subsequently throwing an informative error.

murgatroid99 commented 4 years ago

Most of these HTTP CONNECT proxies seem to be used on localhost. Is that also true of these HTTPS proxies, and if so, how should certificate validation be handled?

zamnuts commented 4 years ago

A proxy hosted on localhost sounds like testing. In practice, a proxy will be on a different server.

Certificate validation will be handled using the typical means around root of trust (root certificate in the chain of trust). This root certificate will either be a well-known CA present in the operating system or default truststore, or it will be an internal CA where the root certificate is distributed to the application by augmenting the truststore or included as an override (see tls.createSecureContext's options.ca property).

Intermediate certificates should be provided by the endpoint. For example, in a certificate chain with 4 certificates, one will the the leaf, one will be the root, and the other two will be intermediates. The proxy, in this case, will supply at least the leaf and the two intermediates, and optionally the root. If the root is supplied, this must match that which is present in the truststore.

Single certificate chains, i.e. self-signed, may be used during testing, but is insecure in a production environment.

murgatroid99 commented 4 years ago

So, in the case where the root certificate is distributed to the application, how should the application provide it to the proxy code? Is there a standard environment variable for doing that?

zamnuts commented 4 years ago

how should the application provide it to the proxy code?

  1. tls.createSecureContext's options.ca property; how it gets it to this library's proxy config is up to the application.
  2. SSL_CERT_DIR or SSL_CERT_FILE, but requires node be started with the --use-openssl-ca option; requires the user specify this uncommon option, but nothing special needs to be done in the proxy lib
murgatroid99 commented 4 years ago

I don't think you understand my question. Currently, gRPC's proxy feature is controlled using environment variables. I am asking how this part of the feature should be controlled from a similar perspective. Of course gRPC will have to use that information by passing that argument to tls.createSecureContext, the question is how gRPC gets the information in the first place. And SSL_CERT_DIR and SSL_CERT_FILE are not useful here, because gRPC will still need to be able to establish TLS connections to outside servers through the tunnels established by the proxy servers, so it will still need to use the normal CA file by default.

zamnuts commented 4 years ago

I understand, but there's no good answer here given the current implementation. There is no conventional CA override for certificates via environment variables, especially in the proxy context. The closest is SSL_CERT_{DIR,FILE} as mentioned in previous. The only thing I can find on the web that's close is HTTPS_PROXY_CERT and REQUESTS_CA_BUNDLE, but this doesn't say whether its a path or the actual PEM-formatted (DER?) contents.

Dependency injection/inversion is the typical route (which is what I was alluding to via tls.createSecureContext).

By means of the ChannelOptions in gRPC, see https://github.com/grpc/grpc-node/blob/4db637e543bf69ce7012e34e4aa8fbd623b4e785/packages/grpc-js/src/channel-options.ts#L21-L36

...perhaps the desired signature would yield something similar to:

new routeguide.RouteGuide('localhost:50051', grpc.credentials.createInsecure(), {
  proxy: {
    proto: 'https',
    host: 'proxyhost',
    port: 8585,
    ca: 'path/to/bundle.crt'
  }
});
murgatroid99 commented 4 years ago

If there is no standard way of doing that, we can do what we want. Perhaps grpc_proxy_ssl_ca_path, possibly with an uppercase variant.

zamnuts commented 4 years ago

@bcoe had mentioned possible prior art with npm itself in very old versions. I checked 1.x, 2.x and 3.x, as well as its dependencies npm-registry-client and npmconf, and couldn't find anything related to a proxy certificate.

I advocate for: grpc_proxy_ca_file and GRPC_PROXY_CA_FILE.

This tells us its A) for the proxy, 2) should be a certificate, and 3) a file containing the ca bundle (opposed to a directory) and not the ca contents itself.

I take it you don't want to also support proxy configuration in ChannelOptions, right?

murgatroid99 commented 4 years ago

That environment variable name looks good to me.

I'm not opposed to configuring the proxy with ChannelOptions, but I'm not yet sure how I want to modify ChannelOptions in general moving forward. I'd like to have a plan for that before we start diverging from the C core so that we can maintain some level of consistency.