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

Allow to configure secure connection to RPC server #181

Merged
merged 3 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## master

- Allow to configure secure gRPC connection. ([@Envek][])

Option `--rpc_tls_root_ca` allow to specify private root certificate authority certificate to verify RPC server certificate.
Option `--rpc_tls_verify` allow to disable RPC server certificate verification (insecure, use only in test/development).

## 1.4.0 (2023-07-07)

- Add HTTP RPC implementation. ([@palkan][])
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ test-conformance: tmp/anycable-go-test
BUNDLE_GEMFILE=.circleci/Gemfile bundle exec anyt -c "tmp/anycable-go-test --headers=cookie,x-api-token" --target-url="ws://localhost:8080/cable"

test-conformance-ssl: tmp/anycable-go-test
BUNDLE_GEMFILE=.circleci/Gemfile bundle exec anyt -c "tmp/anycable-go-test --headers=cookie,x-api-token --ssl_key=etc/ssl/server.key --ssl_cert=etc/ssl/server.crt --port=8443" --target-url="wss://localhost:8443/cable"
ANYCABLE_RPC_TLS_CERT=etc/ssl/server.crt \
ANYCABLE_RPC_TLS_KEY=etc/ssl/server.key \
BUNDLE_GEMFILE=.circleci/Gemfile bundle exec anyt -c \
"tmp/anycable-go-test --headers=cookie,x-api-token --rpc_enable_tls --rpc_tls_verify=false --ssl_key=etc/ssl/server.key --ssl_cert=etc/ssl/server.crt --port=8443" \
--target-url="wss://localhost:8443/cable"

test-conformance-http: tmp/anycable-go-test
BUNDLE_GEMFILE=.circleci/Gemfile \
Expand Down
13 changes: 13 additions & 0 deletions cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,19 @@ func rpcCLIFlags(c *config.Config, headers, cookieFilter *string) []cli.Flag {
Destination: &c.RPC.EnableTLS,
},

&cli.BoolFlag{
Name: "rpc_tls_verify",
Usage: "Whether to verify the RPC server certificate",
Destination: &c.RPC.TLSVerify,
Value: true,
},

&cli.StringFlag{
Name: "rpc_tls_root_ca",
Usage: "CA root certificate file path or contents in PEM format (if not set, system CAs will be used)",
Destination: &c.RPC.TLSRootCA,
},

&cli.IntFlag{
Name: "rpc_max_call_recv_size",
Usage: "Override default MaxCallRecvMsgSize for RPC client (bytes)",
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ anycable-go --port=443 -ssl_cert=path/to/ssl.cert -ssl_key=path/to/ssl.key

If your RPC server requires TLS you can enable it via `--rpc_enable_tls` (`ANYCABLE_RPC_ENABLE_TLS`).

If RPC server uses certificate issued by private CA, then you can pass either its file path or PEM contents with `--rpc_tls_root_ca` (`ANYCABLE_RPC_TLS_ROOT_CA`).

If RPC uses self-signed certificate, you can disable RPC server certificate verification by setting `--rpc_tls_verify` (`ANYCABLE_RPC_TLS_VERIFY`) to `false`, but this is insecure, use only in test/development.

## Concurrency settings

AnyCable-Go uses a single Go gRPC client\* to communicate with AnyCable RPC servers (see [the corresponding PR](https://github.com/anycable/anycable-go/pull/88)). We limit the number of concurrent RPC calls to avoid flooding servers (and getting `ResourceExhausted` exceptions in response).
Expand Down
57 changes: 55 additions & 2 deletions rpc/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package rpc

import pb "github.com/anycable/anycable-go/protos"
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"

pb "github.com/anycable/anycable-go/protos"
)

const (
defaultRPCHost = "localhost:50051"
Expand All @@ -27,6 +35,10 @@ type Config struct {
Concurrency int
// Enable client-side TLS on RPC connections?
EnableTLS bool
// Whether to verify the RPC server's certificate chain and host name
TLSVerify bool
// CA root TLS certificate path
TLSRootCA string
// Max receive msg size (bytes)
MaxRecvSize int
// Max send msg size (bytes)
Expand All @@ -43,5 +55,46 @@ type Config struct {

// NewConfig builds a new config
func NewConfig() Config {
return Config{Concurrency: 28, EnableTLS: false, Host: defaultRPCHost, Implementation: "grpc", RequestTimeout: 3000}
return Config{Concurrency: 28, EnableTLS: false, TLSVerify: true, Host: defaultRPCHost, Implementation: "grpc", RequestTimeout: 3000}
}

// Whether secure connection to RPC server is enabled either explicitly or implicitly
func (c *Config) TLSEnabled() bool {
return c.EnableTLS || c.TLSRootCA != ""
}

// TLSConfig builds TLS configuration for RPC client
func (c *Config) TLSConfig() (*tls.Config, error) {
if !c.TLSEnabled() {
return nil, nil
}

var certPool *x509.CertPool = nil // use system CA certificates
if c.TLSRootCA != "" {
var rootCertificate []byte
var error error
if info, err := os.Stat(c.TLSRootCA); !os.IsNotExist(err) && !info.IsDir() {
rootCertificate, error = os.ReadFile(c.TLSRootCA)
if error != nil {
return nil, fmt.Errorf("failed to read RPC root CA certificate: %s", error)
}
} else {
rootCertificate = []byte(c.TLSRootCA)
}

certPool = x509.NewCertPool()
ok := certPool.AppendCertsFromPEM(rootCertificate)
if !ok {
return nil, errors.New("failed to parse RPC root CA certificate")
}
}

// #nosec G402: InsecureSkipVerify explicitly allowed to be set to true for development/testing
tlsConfig := &tls.Config{
InsecureSkipVerify: !c.TLSVerify,
MinVersion: tls.VersionTLS12,
RootCAs: certPool,
}

return tlsConfig, nil
}
7 changes: 6 additions & 1 deletion rpc/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,13 @@ func NewHTTPDialer(c *Config) (Dialer, error) {
}

func NewHTTPService(c *Config) (*HTTPService, error) {
tlsConfig, error := c.TLSConfig()
if error != nil {
return nil, error
}

client := &http.Client{
Transport: &http.Transport{},
Transport: &http.Transport{TLSClientConfig: tlsConfig},
}

baseURL, err := url.Parse(c.Host)
Expand Down
11 changes: 5 additions & 6 deletions rpc/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rpc

import (
"context"
"crypto/tls"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -185,7 +184,7 @@ func NewController(metrics metrics.Instrumenter, config *Config) *Controller {
// Start initializes RPC connection pool
func (c *Controller) Start() error {
host := c.config.Host
enableTLS := c.config.EnableTLS
enableTLS := c.config.TLSEnabled()
impl := c.config.Implementation

dialer := c.config.DialFun
Expand Down Expand Up @@ -493,7 +492,7 @@ func newContext(sessionID string) context.Context {

func defaultDialer(conf *Config) (pb.RPCClient, ClientHelper, error) {
host := conf.Host
enableTLS := conf.EnableTLS
enableTLS := conf.TLSEnabled()

kacp := keepalive.ClientParameters{
Time: 10 * time.Second, // send pings every 10 seconds if there is no activity
Expand All @@ -511,9 +510,9 @@ func defaultDialer(conf *Config) (pb.RPCClient, ClientHelper, error) {
}

if enableTLS {
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
tlsConfig, error := conf.TLSConfig()
if error != nil {
return nil, nil, error
}

dialOptions = append(dialOptions, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
Expand Down
Loading