Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
zamnuts opened this issue Mar 8, 2020 · 9 comments
Open

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

zamnuts opened this issue Mar 8, 2020 · 9 comments

Comments

@zamnuts
Copy link

zamnuts commented Mar 8, 2020

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:

  • The intended host to proxy
  • The username/password when using an authenticated proxy

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
Copy link
Member

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
Copy link
Author

zamnuts commented Apr 27, 2020

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
Copy link
Member

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
Copy link
Author

zamnuts commented May 2, 2020

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
Copy link
Member

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
Copy link
Author

zamnuts commented May 3, 2020

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

export interface ChannelOptions {
'grpc.ssl_target_name_override'?: string;
'grpc.primary_user_agent'?: string;
'grpc.secondary_user_agent'?: string;
'grpc.default_authority'?: string;
'grpc.keepalive_time_ms'?: number;
'grpc.keepalive_timeout_ms'?: number;
'grpc.service_config'?: string;
'grpc.max_concurrent_streams'?: number;
'grpc.initial_reconnect_backoff_ms'?: number;
'grpc.max_reconnect_backoff_ms'?: number;
'grpc.use_local_subchannel_pool'?: number;
'grpc.max_send_message_length'?: number;
'grpc.max_receive_message_length'?: number;
[key: string]: string | number | undefined;
}

...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
Copy link
Member

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
Copy link
Author

zamnuts commented May 10, 2020

@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
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants