From d7c6627925e7c8376f6383c508bd14943612b2d4 Mon Sep 17 00:00:00 2001 From: dongmen <20351731+asddongmen@users.noreply.github.com> Date: Tue, 14 Dec 2021 18:16:35 +0800 Subject: [PATCH] http_api (ticdc): check --cert-allowed-cn before add server common name (#3628) --- cdc/capture/capture.go | 6 ++ cdc/capture/http_handler.go | 10 --- cdc/http_status.go | 14 +++- cdc/http_status_test.go | 146 ++++++++++++++++++++++++++++++++++++ pkg/security/test_util.go | 132 ++++++++++++++++++++++++++++++++ 5 files changed, 294 insertions(+), 14 deletions(-) create mode 100644 pkg/security/test_util.go diff --git a/cdc/capture/capture.go b/cdc/capture/capture.go index fc848f119ef..d4399f10f22 100644 --- a/cdc/capture/capture.go +++ b/cdc/capture/capture.go @@ -88,6 +88,12 @@ func NewCapture(pdClient pd.Client, kvStorage tidbkv.Storage, etcdClient *etcd.C } } +func NewCapture4Test() *Capture { + return &Capture{ + info: &model.CaptureInfo{ID: "capture-for-test", AdvertiseAddr: "127.0.0.1", Version: "test"}, + } +} + func (c *Capture) reset(ctx context.Context) error { c.captureMu.Lock() defer c.captureMu.Unlock() diff --git a/cdc/capture/http_handler.go b/cdc/capture/http_handler.go index 2b6ec2325b2..697394e880e 100644 --- a/cdc/capture/http_handler.go +++ b/cdc/capture/http_handler.go @@ -672,11 +672,6 @@ func (h *HTTPHandler) ListCapture(c *gin.Context) { // @Failure 500,400 {object} model.HTTPError // @Router /api/v1/status [get] func (h *HTTPHandler) ServerStatus(c *gin.Context) { - if !h.capture.IsOwner() { - h.forwardToOwner(c) - return - } - status := model.ServerStatus{ Version: version.ReleaseVersion, GitHash: version.GitHash, @@ -697,11 +692,6 @@ func (h *HTTPHandler) ServerStatus(c *gin.Context) { // @Failure 500 {object} model.HTTPError // @Router /api/v1/health [get] func (h *HTTPHandler) Health(c *gin.Context) { - if !h.capture.IsOwner() { - h.forwardToOwner(c) - return - } - ctx := c.Request.Context() if _, err := h.capture.GetOwner(ctx); err != nil { diff --git a/cdc/http_status.go b/cdc/http_status.go index 29cf8fd9cfa..90106df6fbf 100644 --- a/cdc/http_status.go +++ b/cdc/http_status.go @@ -59,11 +59,17 @@ func (s *Server) startStatusHTTP() error { prometheus.DefaultGatherer = registry router.Any("/metrics", gin.WrapH(promhttp.Handler())) - err := conf.Security.AddSelfCommonName() - if err != nil { - log.Error("status server set tls config failed", zap.Error(err)) - return errors.Trace(err) + // if CertAllowedCN was specified, we should add server's common name + // otherwise, https requests sent to non-owner capture can't be forward + // to owner + if len(conf.Security.CertAllowedCN) != 0 { + err := conf.Security.AddSelfCommonName() + if err != nil { + log.Error("status server set tls config failed", zap.Error(err)) + return errors.Trace(err) + } } + tlsConfig, err := conf.Security.ToTLSConfigWithVerify() if err != nil { log.Error("status server get tls config failed", zap.Error(err)) diff --git a/cdc/http_status_test.go b/cdc/http_status_test.go index 71cbb66a188..23ab2274383 100644 --- a/cdc/http_status_test.go +++ b/cdc/http_status_test.go @@ -15,16 +15,27 @@ package cdc import ( "bytes" + "context" + "crypto/tls" + "encoding/json" "fmt" "io" "net/http" "net/url" + "strings" "time" "github.com/pingcap/check" "github.com/pingcap/failpoint" + "github.com/pingcap/ticdc/cdc/capture" + "github.com/pingcap/ticdc/cdc/model" "github.com/pingcap/ticdc/pkg/config" + cerrors "github.com/pingcap/ticdc/pkg/errors" + "github.com/pingcap/ticdc/pkg/retry" + security2 "github.com/pingcap/ticdc/pkg/security" "github.com/pingcap/ticdc/pkg/util/testleak" + "github.com/pingcap/tidb/br/pkg/httputil" + "github.com/tikv/pd/pkg/tempurl" "go.etcd.io/etcd/clientv3/concurrency" ) @@ -159,3 +170,138 @@ func testHandleFailpoint(c *check.C) { }) c.Assert(failpointHit, check.IsFalse) } + +func (s *httpStatusSuite) TestServerTLSWithoutCommonName(c *check.C) { + defer testleak.AfterTest(c) + addr := tempurl.Alloc()[len("http://"):] + // Do not specify common name + security, err := security2.NewCredential4Test("") + c.Assert(err, check.IsNil) + conf := config.GetDefaultServerConfig() + conf.Addr = addr + conf.AdvertiseAddr = addr + conf.Security = &security + config.StoreGlobalServerConfig(conf) + + server, err := NewServer([]string{"https://127.0.0.1:2379"}) + server.capture = capture.NewCapture4Test() + c.Assert(err, check.IsNil) + err = server.startStatusHTTP() + c.Assert(err, check.IsNil) + defer func() { + c.Assert(server.statusServer.Close(), check.IsNil) + }() + + statusURL := fmt.Sprintf("https://%s/api/v1/status", addr) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // test cli sends request without a cert will success + err = retry.Do(ctx, func() error { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + cli := &http.Client{Transport: tr} + resp, err := cli.Get(statusURL) + if err != nil { + return err + } + decoder := json.NewDecoder(resp.Body) + captureInfo := &model.CaptureInfo{} + err = decoder.Decode(captureInfo) + c.Assert(err, check.IsNil) + c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID) + resp.Body.Close() + return nil + }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError)) + c.Assert(err, check.IsNil) + + // test cli sends request with a cert will success + err = retry.Do(ctx, func() error { + tlsConfig, err := security.ToTLSConfigWithVerify() + if err != nil { + c.Assert(err, check.IsNil) + } + cli := httputil.NewClient(tlsConfig) + resp, err := cli.Get(statusURL) + if err != nil { + return err + } + decoder := json.NewDecoder(resp.Body) + captureInfo := &model.CaptureInfo{} + err = decoder.Decode(captureInfo) + c.Assert(err, check.IsNil) + c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID) + resp.Body.Close() + return nil + }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError)) + c.Assert(err, check.IsNil) +} + +// +func (s *httpStatusSuite) TestServerTLSWithCommonName(c *check.C) { + defer testleak.AfterTest(c) + addr := tempurl.Alloc()[len("http://"):] + // specify a common name + security, err := security2.NewCredential4Test("test") + c.Assert(err, check.IsNil) + conf := config.GetDefaultServerConfig() + conf.Addr = addr + conf.AdvertiseAddr = addr + conf.Security = &security + config.StoreGlobalServerConfig(conf) + + server, err := NewServer([]string{"https://127.0.0.1:2379"}) + server.capture = capture.NewCapture4Test() + c.Assert(err, check.IsNil) + err = server.startStatusHTTP() + c.Assert(err, check.IsNil) + defer func() { + c.Assert(server.statusServer.Close(), check.IsNil) + }() + + statusURL := fmt.Sprintf("https://%s/api/v1/status", addr) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // test cli sends request without a cert will fail + err = retry.Do(ctx, func() error { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + cli := &http.Client{Transport: tr} + resp, err := cli.Get(statusURL) + if err != nil { + return err + } + decoder := json.NewDecoder(resp.Body) + captureInfo := &model.CaptureInfo{} + err = decoder.Decode(captureInfo) + c.Assert(err, check.IsNil) + c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID) + resp.Body.Close() + return nil + }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError)) + c.Assert(strings.Contains(err.Error(), "remote error: tls: bad certificate"), check.IsTrue) + + // test cli sends request with a cert will success + err = retry.Do(ctx, func() error { + tlsConfig, err := security.ToTLSConfigWithVerify() + if err != nil { + c.Assert(err, check.IsNil) + } + cli := httputil.NewClient(tlsConfig) + resp, err := cli.Get(statusURL) + if err != nil { + return err + } + decoder := json.NewDecoder(resp.Body) + captureInfo := &model.CaptureInfo{} + err = decoder.Decode(captureInfo) + c.Assert(err, check.IsNil) + c.Assert(captureInfo.ID, check.Equals, server.capture.Info().ID) + resp.Body.Close() + return nil + }, retry.WithMaxTries(retryTime), retry.WithBackoffBaseDelay(50), retry.WithIsRetryableErr(cerrors.IsRetryableError)) + c.Assert(err, check.IsNil) +} diff --git a/pkg/security/test_util.go b/pkg/security/test_util.go new file mode 100644 index 00000000000..652c3549842 --- /dev/null +++ b/pkg/security/test_util.go @@ -0,0 +1,132 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package security + +import "os" + +// All these certificates or keys are use for testing only. +var certPem = `-----BEGIN CERTIFICATE----- +MIIDjzCCAnegAwIBAgIUWBTDQm4xOYDxZBTkpCQouREtT8QwDQYJKoZIhvcNAQEL +BQAwVzELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB0JlaWppbmcxEDAOBgNVBAcTB0Jl +aWppbmcxEDAOBgNVBAoTB1BpbmdDQVAxEjAQBgNVBAMTCU15IG93biBDQTAgFw0y +MDAyMTgwOTExMDBaGA8yMTIwMDEyNTA5MTEwMFowFjEUMBIGA1UEAxMLdGlkYi1z +ZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCr2ZxAb+dItEQz +avuza0IoIT/UolC9XTGaQiCUUPZUMN9hb4KYEwTks1ZthHovTIJUdTwHtpWfDUWx +uIXhOlRjfD+viY4aXtBsaK8xi9F7o2HbFQ5O9y3AXK/YW+u0FfWtnn/xAtvPUgUc +61NXtBTMvNard9ICIXW+FWxLcFSaHpC9ZTyr13KWmZRDbai1JFeaKvATMW30r7Dd +Ur1npppzt7ZdG6tU/FuqBBSrZtuVSGKLwVx0JQDw16eVan4friY3ZPNVoZvkKyvt +I1LsBYMQ8p+ijtbcftvMqWZFVw95F1+3C2JIjWN9ujGmvJr+dtPIE8T/J8tT9Jif +9vz16nOLAgMBAAGjgZEwgY4wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRVB/Bvdzvh +6WQRWpc9SzcbXLz77zAfBgNVHSMEGDAWgBSdAhKsS8BKSOidoGCUYNeaFma4/zAP +BgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQAAqg5pgGQqORKRSdlY +wzVvzKaulpvjZfVMM6YiOUtmlU0CGWq7E3gLFzkvebpU0KsFlbyZ92h/2Fw5Ay2b +kxkCy18mJ4lGkvF0cU4UD3XheFMvD2QWWRX4WPpAhStofrWOXeyq3Div2+fQjMJd +kyeWUzPU7T467IWUHOWNsFAjfVHNsmG45qLGt+XQckHTvASX5IvN+5tkRUCW30vO +b3BdDQUFglGTUFU2epaZGTti0SYiRiY+9R3zFWX4uBcEBYhk9e/0BU8FqdWW5GjI +pFpH9t64CjKIdRQXpIn4cogK/GwyuRuDPV/RkMjrIqOi7pGejXwyDe9avHFVR6re +oowA +-----END CERTIFICATE-----` + +var caPem = `-----BEGIN CERTIFICATE----- +MIIDgDCCAmigAwIBAgIUHWvlRJydvYTR0ot3b8f6IlSHcGUwDQYJKoZIhvcNAQEL +BQAwVzELMAkGA1UEBhMCQ04xEDAOBgNVBAgTB0JlaWppbmcxEDAOBgNVBAcTB0Jl +aWppbmcxEDAOBgNVBAoTB1BpbmdDQVAxEjAQBgNVBAMTCU15IG93biBDQTAgFw0y +MDAyMTgwNzQxMDBaGA8yMTIwMDEyNTA3NDEwMFowVzELMAkGA1UEBhMCQ04xEDAO +BgNVBAgTB0JlaWppbmcxEDAOBgNVBAcTB0JlaWppbmcxEDAOBgNVBAoTB1BpbmdD +QVAxEjAQBgNVBAMTCU15IG93biBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAOAdNtjanFhPaKJHQjr7+h/Cpps5bLc6S1vmgi/EIi9PKv3eyDgtlW1r +As2sjXRMHjcuZp2hHJ9r9FrMQD1rQQq5vJzQqM+eyWLc2tyZWXNWkZVvpjU4Hy5k +jZFLXoyHgAvps/LGu81F5Lk5CvLHswWTyGQUCFi1l/cYcQg6AExh2pO/WJu4hQhe +1mBBIKsJhZ5b5tWruLeI+YIjD1oo1ADMHYLK1BHON2fUmUHRGbrYKu4yCuyip3wn +rbVlpabn7l1JBMatCUJLHR6VWQ2MNjrOXAEUYm4xGEN+tUYyUOGl5mHFguLl3OIn +wj+1dT3WOr/NraPYlwVOnAd9GNbPJj0CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ0CEqxLwEpI6J2gYJRg15oWZrj/ +MA0GCSqGSIb3DQEBCwUAA4IBAQCf8xRf7q1xAaGrc9HCPvN4OFkxDwz1CifrvrLR +ZgIWGUdCHDW2D1IiWKZQWeJKC1otA5x0hrS5kEGfkLFhADEU4txwp70DQaBArPti +pSgheIEbaT0H3BUTYSgS3VL2HjxN5OVMN6jNG3rWyxnJpNOCsJhhJXPK50CRZ7fk +Dcodj6FfEM2bfp2bGkxyVtUch7eepfUVbslXa7jE7Y8M3cr9NoLUcSP6D1RJWkNd +dBQoUsb6Ckq27ozEKOgwuBVv4BrrbFN//+7WHP8Vy6sSMyd+dJLBi6wehJjQhIz6 +vqLWE81rSJuxZqjLpCkFdeEF+9SRjWegU0ZDM4V+YeX53BPC +-----END CERTIFICATE----- +` + +var keyPem = `-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAq9mcQG/nSLREM2r7s2tCKCE/1KJQvV0xmkIglFD2VDDfYW+C +mBME5LNWbYR6L0yCVHU8B7aVnw1FsbiF4TpUY3w/r4mOGl7QbGivMYvRe6Nh2xUO +TvctwFyv2FvrtBX1rZ5/8QLbz1IFHOtTV7QUzLzWq3fSAiF1vhVsS3BUmh6QvWU8 +q9dylpmUQ22otSRXmirwEzFt9K+w3VK9Z6aac7e2XRurVPxbqgQUq2bblUhii8Fc +dCUA8NenlWp+H64mN2TzVaGb5Csr7SNS7AWDEPKfoo7W3H7bzKlmRVcPeRdftwti +SI1jfboxprya/nbTyBPE/yfLU/SYn/b89epziwIDAQABAoIBACPlI08OULgN90Tq +LsLuP3ZUY5nNgaHcKnU3JMj2FE3Hm5ElkpijOF1w3Dep+T+R8pMjnbNavuvnAMy7 +ZzOBVIknNcI7sDPv5AcQ4q8trkbt/I2fW0rBNIw+j/hYUuZdw+BNABpeZ31pe2nr ++Y+TLNkLBKfyMiqBxK88mE81mmZKblyvXCawW0A/iDDJ7fPNqoGF+y9ylTYaNRPk +aJGnaEZobJ4Lm5tSqW4gRX2ft6Hm67RkvVaopPFnlkvfusXUTFUqEVQCURRUqXbf +1ah2chUHxj22UdY9540H5yVNgEP3oR+uS/hbZqxKcJUTznUW5th3CyQPIKMlGlcB +p+zWlTECgYEAxlY4zGJw4QQwGYMKLyWCSHUgKYrKu2Ub2JKJFMTdsoj9H7DI+WHf +lQaO9NCOo2lt0ofYM1MzEpI5Cl/aMrPw+mwquBbxWdMHXK2eSsUQOVo9HtUjgK2t +J2AYFCfsYndo+hCj3ApMHgiY3sghTCXeycvT52bm11VeNVcs3pKxIYMCgYEA3dAJ +PwIfAB8t+6JCP2yYH4ExNjoMNYMdXqhz4vt3UGwgskRqTW6qdd9JvrRQ/JPvGpDy +T375h/+lLw0E4ljsnOPGSzbXNf4bYRHTwPOL+LqVM4Bg90hjclqphElHChxep1di +WcdArB0oae/l4M96z3GjfnXIUVOp8K6BUQCab1kCgYAFFAQUR5j4SfEpVg+WsXEq +hcUzCxixv5785pOX8opynctNWmtq5zSgTjCu2AAu8u4a69t/ROwT16aaO2YM0kqj +Ps3BNOUtFZgkqVVaOL13mnXiKjbkfo3majFzoqoMw13uuSpY4fKc+j9fxOQFXRrd +M9jTHfFfJhJpbzf44uyiHQKBgFIPwzvyVvG+l05/Ky83x9fv/frn4thxV45LmAQj +sHKqbjZFpWZcSOgu4aOSJlwrhsw3T84lVcAAzmXn1STAbVll01jEQz6QciSpacP6 +1pAAx240UqtptpD6BbkROxz8ffA/Hf3E/6Itb2QyAsP3PqI8kpYYkTG1WCvZA7Kq +HHiRAoGAXbUZ25LcrmyuxKWpbty8fck1tjKPvclQB35rOx6vgnfW6pcKMeebYvgq +nJka/QunEReOH/kGxAd/+ymvUBuFQCfFg3Aus+DtAuh9AkBr+cIyPjJqynnIT87J +MbkOw4uEhDJAtGUR9o1j83N1f05bnEwssXiXR0LZPylb9Qzc4tg= +-----END RSA PRIVATE KEY----- +` + +// NewCredential4Test return a Credential for testing +func NewCredential4Test(cn string) (Credential, error) { + res := Credential{} + if cn != "" { + res.CertAllowedCN = append(res.CertAllowedCN, cn) + } + cert, err := os.CreateTemp("", "ticdc-test-cert") + if err != nil { + return res, err + } + _, err = cert.Write([]byte(certPem)) + if err != nil { + return res, err + } + + ca, err := os.CreateTemp("", "ticdc-test-ca") + if err != nil { + return res, err + } + _, err = ca.Write([]byte(caPem)) + if err != nil { + return res, err + } + + key, err := os.CreateTemp("", "ticdc-test-key") + if err != nil { + return res, err + } + _, err = key.Write([]byte(keyPem)) + if err != nil { + return res, err + } + + res.CertPath = cert.Name() + res.CAPath = ca.Name() + res.KeyPath = key.Name() + + return res, nil +}