vitess.io/vitess@v0.16.2/go/test/endtoend/encryption/encryptedtransport/encrypted_transport_test.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  /* This test makes sure encrypted transport over gRPC works.
    18  
    19  The security chains are setup the following way:
    20  
    21  * root CA
    22  * vttablet server CA
    23  * vttablet server instance cert/key
    24  * vttablet client CA
    25  * vttablet client 1 cert/key
    26  * vtgate server CA
    27  * vtgate server instance cert/key (common name is 'localhost')
    28  * vtgate client CA
    29  * vtgate client 1 cert/key
    30  * vtgate client 2 cert/key
    31  
    32  The following table shows all the checks we perform:
    33  process:            will check its peer is signed by:  for link:
    34  
    35  vttablet             vttablet client CA                vtgate -> vttablet
    36  vtgate               vttablet server CA                vtgate -> vttablet
    37  
    38  vtgate               vtgate client CA                  client -> vtgate
    39  client               vtgate server CA                  client -> vtgate
    40  
    41  Additionally, we have the following constraints:
    42  - the client certificate common name is used as immediate
    43  caller ID by vtgate, and forwarded to vttablet. This allows us to use
    44  table ACLs on the vttablet side.
    45  - the vtgate server certificate common name is set to 'localhost' so it matches
    46  the hostname dialed by the vtgate clients. This is not a requirement for the
    47  go client, that can set its expected server name. However, the python gRPC
    48  client doesn't have the ability to set the server name, so they must match.
    49  - the python client needs to have the full chain for the server validation
    50  (that is 'vtgate server CA' + 'root CA'). A go client doesn't. So we read both
    51  below when using the python client, but we only pass the intermediate cert
    52  to the go clients (for vtgate -> vttablet link). */
    53  
    54  package encryptedtransport
    55  
    56  import (
    57  	"context"
    58  	"flag"
    59  	"fmt"
    60  	"io"
    61  	"os"
    62  	"os/exec"
    63  	"path"
    64  	"testing"
    65  
    66  	"github.com/pkg/errors"
    67  
    68  	"vitess.io/vitess/go/test/endtoend/encryption"
    69  
    70  	"vitess.io/vitess/go/vt/proto/vtrpc"
    71  	"vitess.io/vitess/go/vt/vterrors"
    72  
    73  	"github.com/stretchr/testify/assert"
    74  	"github.com/stretchr/testify/require"
    75  
    76  	"vitess.io/vitess/go/test/endtoend/cluster"
    77  	"vitess.io/vitess/go/vt/grpcclient"
    78  	"vitess.io/vitess/go/vt/log"
    79  	querypb "vitess.io/vitess/go/vt/proto/query"
    80  	vtgatepb "vitess.io/vitess/go/vt/proto/vtgate"
    81  	vtgateservicepb "vitess.io/vitess/go/vt/proto/vtgateservice"
    82  )
    83  
    84  var (
    85  	clusterInstance    *cluster.LocalProcessCluster
    86  	createVtInsertTest = `create table vt_insert_test (
    87  								id bigint auto_increment,
    88  								msg varchar(64),
    89  								keyspace_id bigint(20) unsigned NOT NULL,
    90  								primary key (id)
    91  								) Engine = InnoDB`
    92  	keyspace      = "test_keyspace"
    93  	hostname      = "localhost"
    94  	shardName     = "0"
    95  	cell          = "zone1"
    96  	certDirectory string
    97  	grpcCert      = ""
    98  	grpcKey       = ""
    99  	grpcCa        = ""
   100  	grpcName      = ""
   101  )
   102  
   103  func TestSecureTransport(t *testing.T) {
   104  	defer cluster.PanicHandler(t)
   105  	flag.Parse()
   106  
   107  	// initialize cluster
   108  	_, err := clusterSetUp(t)
   109  	require.Nil(t, err, "setup failed")
   110  
   111  	primaryTablet := *clusterInstance.Keyspaces[0].Shards[0].Vttablets[0]
   112  	replicaTablet := *clusterInstance.Keyspaces[0].Shards[0].Vttablets[1]
   113  
   114  	// creating table_acl_config.json file
   115  	tableACLConfigJSON := path.Join(certDirectory, "table_acl_config.json")
   116  	f, err := os.Create(tableACLConfigJSON)
   117  	require.NoError(t, err)
   118  
   119  	_, err = f.WriteString(`{
   120  	"table_groups": [
   121  	{
   122  		"table_names_or_prefixes": ["vt_insert_test"],
   123  		"readers": ["vtgate client 1"],
   124  		"writers": ["vtgate client 1"],
   125  		"admins": ["vtgate client 1"]
   126  	}
   127    ]
   128  }`)
   129  	require.NoError(t, err)
   130  	err = f.Close()
   131  	require.NoError(t, err)
   132  
   133  	// start the tablets
   134  	for _, tablet := range []cluster.Vttablet{primaryTablet, replicaTablet} {
   135  		tablet.VttabletProcess.ExtraArgs = append(tablet.VttabletProcess.ExtraArgs, "--table-acl-config", tableACLConfigJSON, "--queryserver-config-strict-table-acl")
   136  		tablet.VttabletProcess.ExtraArgs = append(tablet.VttabletProcess.ExtraArgs, serverExtraArguments("vttablet-server-instance", "vttablet-client")...)
   137  		err = tablet.VttabletProcess.Setup()
   138  		require.NoError(t, err)
   139  	}
   140  
   141  	// setup replication
   142  	var vtctlClientArgs []string
   143  
   144  	vtctlClientTmArgs := append(vtctlClientArgs, tmclientExtraArgs("vttablet-client-1")...)
   145  
   146  	// Reparenting
   147  	vtctlClientArgs = append(vtctlClientTmArgs, "InitShardPrimary", "--", "--force", "test_keyspace/0", primaryTablet.Alias)
   148  	err = clusterInstance.VtctlProcess.ExecuteCommand(vtctlClientArgs...)
   149  	require.NoError(t, err)
   150  
   151  	err = clusterInstance.StartVTOrc("test_keyspace")
   152  	require.NoError(t, err)
   153  
   154  	// Apply schema
   155  	var vtctlApplySchemaArgs = append(vtctlClientTmArgs, "ApplySchema", "--", "--sql", createVtInsertTest, "test_keyspace")
   156  	err = clusterInstance.VtctlProcess.ExecuteCommand(vtctlApplySchemaArgs...)
   157  	require.NoError(t, err)
   158  
   159  	for _, tablet := range []cluster.Vttablet{primaryTablet, replicaTablet} {
   160  		var vtctlTabletArgs []string
   161  		vtctlTabletArgs = append(vtctlTabletArgs, tmclientExtraArgs("vttablet-client-1")...)
   162  		vtctlTabletArgs = append(vtctlTabletArgs, "RunHealthCheck", tablet.Alias)
   163  		_, err = clusterInstance.VtctlProcess.ExecuteCommandWithOutput(vtctlTabletArgs...)
   164  		require.NoError(t, err)
   165  	}
   166  
   167  	// start vtgate
   168  	clusterInstance.VtGateExtraArgs = append(clusterInstance.VtGateExtraArgs, tabletConnExtraArgs("vttablet-client-1")...)
   169  	clusterInstance.VtGateExtraArgs = append(clusterInstance.VtGateExtraArgs, serverExtraArguments("vtgate-server-instance", "vtgate-client")...)
   170  	err = clusterInstance.StartVtgate()
   171  	require.NoError(t, err)
   172  
   173  	grpcAddress := fmt.Sprintf("%s:%d", "localhost", clusterInstance.VtgateProcess.GrpcPort)
   174  
   175  	// 'vtgate client 1' is authorized to access vt_insert_test
   176  	setCreds(t, "vtgate-client-1", "vtgate-server")
   177  	ctx := context.Background()
   178  	request := getRequest("select * from vt_insert_test")
   179  	vc, err := getVitessClient(grpcAddress)
   180  	require.NoError(t, err)
   181  
   182  	qr, err := vc.Execute(ctx, request)
   183  	require.NoError(t, err)
   184  	err = vterrors.FromVTRPC(qr.Error)
   185  	require.NoError(t, err)
   186  
   187  	// 'vtgate client 2' is not authorized to access vt_insert_test
   188  	setCreds(t, "vtgate-client-2", "vtgate-server")
   189  	request = getRequest("select * from vt_insert_test")
   190  	vc, err = getVitessClient(grpcAddress)
   191  	require.NoError(t, err)
   192  	qr, err = vc.Execute(ctx, request)
   193  	require.NoError(t, err)
   194  	err = vterrors.FromVTRPC(qr.Error)
   195  	require.Error(t, err)
   196  	assert.Contains(t, err.Error(), "Select command denied to user")
   197  	assert.Contains(t, err.Error(), "for table 'vt_insert_test' (ACL check error)")
   198  
   199  	useEffectiveCallerID(ctx, t)
   200  	useEffectiveGroups(ctx, t)
   201  
   202  	clusterInstance.Teardown()
   203  }
   204  
   205  func useEffectiveCallerID(ctx context.Context, t *testing.T) {
   206  	// now restart vtgate in the mode where we don't use SSL
   207  	// for client connections, but we copy effective caller id
   208  	// into immediate caller id.
   209  	clusterInstance.VtGateExtraArgs = []string{"--grpc_use_effective_callerid"}
   210  	clusterInstance.VtGateExtraArgs = append(clusterInstance.VtGateExtraArgs, tabletConnExtraArgs("vttablet-client-1")...)
   211  	err := clusterInstance.RestartVtgate()
   212  	require.NoError(t, err)
   213  
   214  	grpcAddress := fmt.Sprintf("%s:%d", "localhost", clusterInstance.VtgateProcess.GrpcPort)
   215  
   216  	setSSLInfoEmpty()
   217  
   218  	// get vitess client
   219  	vc, err := getVitessClient(grpcAddress)
   220  	require.NoError(t, err)
   221  
   222  	// test with empty effective caller Id
   223  	request := getRequest("select * from vt_insert_test")
   224  	qr, err := vc.Execute(ctx, request)
   225  	require.NoError(t, err)
   226  	err = vterrors.FromVTRPC(qr.Error)
   227  	require.Error(t, err)
   228  	assert.Contains(t, err.Error(), "Select command denied to user")
   229  	assert.Contains(t, err.Error(), "for table 'vt_insert_test' (ACL check error)")
   230  
   231  	// 'vtgate client 1' is authorized to access vt_insert_test
   232  	callerID := &vtrpc.CallerID{
   233  		Principal: "vtgate client 1",
   234  	}
   235  	request = getRequestWithCallerID(callerID, "select * from vt_insert_test")
   236  	qr, err = vc.Execute(ctx, request)
   237  	require.NoError(t, err)
   238  	err = vterrors.FromVTRPC(qr.Error)
   239  	require.NoError(t, err)
   240  
   241  	// 'vtgate client 2' is not authorized to access vt_insert_test
   242  	callerID = &vtrpc.CallerID{
   243  		Principal: "vtgate client 2",
   244  	}
   245  	request = getRequestWithCallerID(callerID, "select * from vt_insert_test")
   246  	qr, err = vc.Execute(ctx, request)
   247  	require.NoError(t, err)
   248  	err = vterrors.FromVTRPC(qr.Error)
   249  	require.Error(t, err)
   250  	assert.Contains(t, err.Error(), "Select command denied to user")
   251  	assert.Contains(t, err.Error(), "for table 'vt_insert_test' (ACL check error)")
   252  }
   253  
   254  func useEffectiveGroups(ctx context.Context, t *testing.T) {
   255  	// now restart vtgate in the mode where we don't use SSL
   256  	// for client connections, but we copy effective caller's groups
   257  	// into immediate caller id.
   258  	clusterInstance.VtGateExtraArgs = []string{"--grpc_use_effective_callerid", "--grpc-use-effective-groups"}
   259  	clusterInstance.VtGateExtraArgs = append(clusterInstance.VtGateExtraArgs, tabletConnExtraArgs("vttablet-client-1")...)
   260  	err := clusterInstance.RestartVtgate()
   261  	require.NoError(t, err)
   262  
   263  	grpcAddress := fmt.Sprintf("%s:%d", "localhost", clusterInstance.VtgateProcess.GrpcPort)
   264  
   265  	setSSLInfoEmpty()
   266  
   267  	// get vitess client
   268  	vc, err := getVitessClient(grpcAddress)
   269  	require.NoError(t, err)
   270  
   271  	// test with empty effective caller Id
   272  	request := getRequest("select * from vt_insert_test")
   273  	qr, err := vc.Execute(ctx, request)
   274  	require.NoError(t, err)
   275  	err = vterrors.FromVTRPC(qr.Error)
   276  	require.Error(t, err)
   277  	assert.Contains(t, err.Error(), "Select command denied to user")
   278  	assert.Contains(t, err.Error(), "for table 'vt_insert_test' (ACL check error)")
   279  
   280  	// 'vtgate client 1' is authorized to access vt_insert_test
   281  	callerID := &vtrpc.CallerID{
   282  		Principal: "my-caller",
   283  		Groups:    []string{"vtgate client 1"},
   284  	}
   285  	request = getRequestWithCallerID(callerID, "select * from vt_insert_test")
   286  	qr, err = vc.Execute(ctx, request)
   287  	require.NoError(t, err)
   288  	err = vterrors.FromVTRPC(qr.Error)
   289  	require.NoError(t, err)
   290  
   291  	// 'vtgate client 2' is not authorized to access vt_insert_test
   292  	callerID = &vtrpc.CallerID{
   293  		Principal: "my-caller",
   294  		Groups:    []string{"vtgate client 2"},
   295  	}
   296  	request = getRequestWithCallerID(callerID, "select * from vt_insert_test")
   297  	qr, err = vc.Execute(ctx, request)
   298  	require.NoError(t, err)
   299  	err = vterrors.FromVTRPC(qr.Error)
   300  	require.Error(t, err)
   301  	assert.Contains(t, err.Error(), "Select command denied to user")
   302  	assert.Contains(t, err.Error(), "for table 'vt_insert_test' (ACL check error)")
   303  }
   304  
   305  func clusterSetUp(t *testing.T) (int, error) {
   306  	var mysqlProcesses []*exec.Cmd
   307  	clusterInstance = cluster.NewCluster(cell, hostname)
   308  
   309  	// Start topo server
   310  	if err := clusterInstance.StartTopo(); err != nil {
   311  		return 1, errors.Wrap(err, "unable to start topo")
   312  	}
   313  
   314  	// create all certs
   315  	log.Info("Creating certificates")
   316  	certDirectory = path.Join(clusterInstance.TmpDirectory, "certs")
   317  	_ = encryption.CreateDirectory(certDirectory, 0700)
   318  
   319  	err := encryption.ExecuteVttlstestCommand("--root", certDirectory, "CreateCA")
   320  	require.NoError(t, err)
   321  
   322  	err = createIntermediateCA("ca", "01", "vttablet-server", "vttablet server CA")
   323  	require.NoError(t, err)
   324  
   325  	err = createIntermediateCA("ca", "02", "vttablet-client", "vttablet client CA")
   326  	require.NoError(t, err)
   327  
   328  	err = createIntermediateCA("ca", "03", "vtgate-server", "vtgate server CA")
   329  	require.NoError(t, err)
   330  
   331  	err = createIntermediateCA("ca", "04", "vtgate-client", "vtgate client CA")
   332  	require.NoError(t, err)
   333  
   334  	err = createSignedCert("vttablet-server", "01", "vttablet-server-instance", "vttablet server instance")
   335  	require.NoError(t, err)
   336  
   337  	err = createSignedCert("vttablet-client", "01", "vttablet-client-1", "vttablet client 1")
   338  	require.NoError(t, err)
   339  
   340  	err = createSignedCert("vtgate-server", "01", "vtgate-server-instance", "localhost")
   341  	require.NoError(t, err)
   342  
   343  	err = createSignedCert("vtgate-client", "01", "vtgate-client-1", "vtgate client 1")
   344  	require.NoError(t, err)
   345  
   346  	err = createSignedCert("vtgate-client", "02", "vtgate-client-2", "vtgate client 2")
   347  	require.NoError(t, err)
   348  
   349  	for _, keyspaceStr := range []string{keyspace} {
   350  		KeyspacePtr := &cluster.Keyspace{Name: keyspaceStr}
   351  		keyspace := *KeyspacePtr
   352  		if err := clusterInstance.VtctlProcess.CreateKeyspace(keyspace.Name); err != nil {
   353  			return 1, err
   354  		}
   355  		shard := &cluster.Shard{
   356  			Name: shardName,
   357  		}
   358  		for i := 0; i < 2; i++ {
   359  			// instantiate vttablet object with reserved ports
   360  			tablet := clusterInstance.NewVttabletInstance("replica", 0, cell)
   361  
   362  			// Start Mysqlctl process
   363  			tablet.MysqlctlProcess = *cluster.MysqlCtlProcessInstance(tablet.TabletUID, tablet.MySQLPort, clusterInstance.TmpDirectory)
   364  			proc, err := tablet.MysqlctlProcess.StartProcess()
   365  			if err != nil {
   366  				return 1, err
   367  			}
   368  			mysqlProcesses = append(mysqlProcesses, proc)
   369  			// start vttablet process
   370  			tablet.VttabletProcess = cluster.VttabletProcessInstance(
   371  				tablet.HTTPPort,
   372  				tablet.GrpcPort,
   373  				tablet.TabletUID,
   374  				clusterInstance.Cell,
   375  				shardName,
   376  				keyspace.Name,
   377  				clusterInstance.VtctldProcess.Port,
   378  				tablet.Type,
   379  				clusterInstance.TopoProcess.Port,
   380  				clusterInstance.Hostname,
   381  				clusterInstance.TmpDirectory,
   382  				clusterInstance.VtTabletExtraArgs,
   383  				clusterInstance.DefaultCharset)
   384  			tablet.Alias = tablet.VttabletProcess.TabletPath
   385  			shard.Vttablets = append(shard.Vttablets, tablet)
   386  		}
   387  		keyspace.Shards = append(keyspace.Shards, *shard)
   388  		clusterInstance.Keyspaces = append(clusterInstance.Keyspaces, keyspace)
   389  	}
   390  	for _, proc := range mysqlProcesses {
   391  		err := proc.Wait()
   392  		if err != nil {
   393  			return 1, errors.Wrap(err, "unable to wait on mysql process")
   394  		}
   395  	}
   396  	return 0, nil
   397  }
   398  
   399  func createIntermediateCA(ca string, serial string, name string, commonName string) error {
   400  	log.Infof("Creating intermediate signed cert and key %s", commonName)
   401  	tmpProcess := exec.Command(
   402  		"vttlstest",
   403  		"CreateIntermediateCA",
   404  		"--root", certDirectory,
   405  		"--parent", ca,
   406  		"--serial", serial,
   407  		"--common-name", commonName,
   408  		name)
   409  	return tmpProcess.Run()
   410  }
   411  
   412  func createSignedCert(ca string, serial string, name string, commonName string) error {
   413  	log.Infof("Creating signed cert and key %s", commonName)
   414  	tmpProcess := exec.Command(
   415  		"vttlstest",
   416  		"CreateSignedCert",
   417  		"--root", certDirectory,
   418  		"--parent", ca,
   419  		"--serial", serial,
   420  		"--common-name", commonName,
   421  		name)
   422  	return tmpProcess.Run()
   423  }
   424  
   425  func serverExtraArguments(name string, ca string) []string {
   426  	args := []string{"--grpc_cert", certDirectory + "/" + name + "-cert.pem",
   427  		"--grpc_key", certDirectory + "/" + name + "-key.pem",
   428  		"--grpc_ca", certDirectory + "/" + ca + "-cert.pem"}
   429  	return args
   430  }
   431  
   432  func tmclientExtraArgs(name string) []string {
   433  	ca := "vttablet-server"
   434  	var args = []string{"--tablet_manager_grpc_cert", certDirectory + "/" + name + "-cert.pem",
   435  		"--tablet_manager_grpc_key", certDirectory + "/" + name + "-key.pem",
   436  		"--tablet_manager_grpc_ca", certDirectory + "/" + ca + "-cert.pem",
   437  		"--tablet_manager_grpc_server_name", "vttablet server instance"}
   438  	return args
   439  }
   440  
   441  func tabletConnExtraArgs(name string) []string {
   442  	ca := "vttablet-server"
   443  	args := []string{"--tablet_grpc_cert", certDirectory + "/" + name + "-cert.pem",
   444  		"--tablet_grpc_key", certDirectory + "/" + name + "-key.pem",
   445  		"--tablet_grpc_ca", certDirectory + "/" + ca + "-cert.pem",
   446  		"--tablet_grpc_server_name", "vttablet server instance"}
   447  	return args
   448  }
   449  
   450  func getVitessClient(addr string) (vtgateservicepb.VitessClient, error) {
   451  	opt, err := grpcclient.SecureDialOption(grpcCert, grpcKey, grpcCa, "", grpcName)
   452  	if err != nil {
   453  		return nil, err
   454  	}
   455  	cc, err := grpcclient.Dial(addr, grpcclient.FailFast(false), opt)
   456  	if err != nil {
   457  		return nil, err
   458  	}
   459  	c := vtgateservicepb.NewVitessClient(cc)
   460  	return c, nil
   461  }
   462  
   463  func setCreds(t *testing.T, name string, ca string) {
   464  	f1, err := os.Open(path.Join(certDirectory, "ca-cert.pem"))
   465  	require.NoError(t, err)
   466  	b1, err := io.ReadAll(f1)
   467  	require.NoError(t, err)
   468  
   469  	f2, err := os.Open(path.Join(certDirectory, ca+"-cert.pem"))
   470  	require.NoError(t, err)
   471  	b2, err := io.ReadAll(f2)
   472  	require.NoError(t, err)
   473  
   474  	caContent := append(b1, b2...)
   475  	fileName := "ca-" + name + ".pem"
   476  	caVtgateClient := path.Join(certDirectory, fileName)
   477  	f, err := os.Create(caVtgateClient)
   478  	require.NoError(t, err)
   479  	_, err = f.Write(caContent)
   480  	require.NoError(t, err)
   481  
   482  	grpcCa = caVtgateClient
   483  	grpcKey = path.Join(certDirectory, name+"-key.pem")
   484  	grpcCert = path.Join(certDirectory, name+"-cert.pem")
   485  
   486  	err = f.Close()
   487  	require.NoError(t, err)
   488  	err = f2.Close()
   489  	require.NoError(t, err)
   490  	err = f1.Close()
   491  	require.NoError(t, err)
   492  }
   493  
   494  func setSSLInfoEmpty() {
   495  	grpcCa = ""
   496  	grpcCert = ""
   497  	grpcKey = ""
   498  	grpcName = ""
   499  }
   500  
   501  func getSession() *vtgatepb.Session {
   502  	return &vtgatepb.Session{
   503  		TargetString: "test_keyspace:0@primary",
   504  	}
   505  }
   506  
   507  func getRequestWithCallerID(callerID *vtrpc.CallerID, sql string) *vtgatepb.ExecuteRequest {
   508  	session := getSession()
   509  	return &vtgatepb.ExecuteRequest{
   510  		CallerId: callerID,
   511  		Session:  session,
   512  		Query: &querypb.BoundQuery{
   513  			Sql: sql,
   514  		},
   515  	}
   516  }
   517  
   518  func getRequest(sql string) *vtgatepb.ExecuteRequest {
   519  	session := getSession()
   520  	return &vtgatepb.ExecuteRequest{
   521  		Session: session,
   522  		Query: &querypb.BoundQuery{
   523  			Sql: sql,
   524  		},
   525  	}
   526  }