From ab9839c34e0482653182a09a775c9648a53c524c Mon Sep 17 00:00:00 2001 From: ifindlay-cci Date: Tue, 3 Sep 2024 13:35:21 +0100 Subject: [PATCH] feat: added tls support to cloud service broker app When running as an app in CF we can rely on the platform to handle TLS setup, but on a VM currently there is no way to have encrypted traffic. TPCF-26820 --- cmd/serve.go | 25 ++++++- integrationtest/server_test.go | 66 ++++++++++++++++++ internal/testdrive/broker_start.go | 108 ++++++++++++++++++++++++++++- 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 integrationtest/server_test.go diff --git a/cmd/serve.go b/cmd/serve.go index feb61bf7b..40262c9b3 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -51,6 +51,8 @@ const ( apiPasswordProp = "api.password" apiPortProp = "api.port" apiHostProp = "api.host" + tlsCertCaBundleProp = "api.certCaBundle" + tlsKeyProp = "api.tlsKey" encryptionPasswords = "db.encryption.passwords" encryptionEnabled = "db.encryption.enabled" ) @@ -84,6 +86,8 @@ func init() { _ = viper.BindEnv(apiHostProp, "CSB_LISTENER_HOST") _ = viper.BindEnv(encryptionPasswords, "ENCRYPTION_PASSWORDS") _ = viper.BindEnv(encryptionEnabled, "ENCRYPTION_ENABLED") + _ = viper.BindEnv(tlsCertCaBundleProp, "TLS_CERT_CHAIN") + _ = viper.BindEnv(tlsKeyProp, "TLS_PRIVATE_KEY") } func serve() { @@ -210,7 +214,26 @@ func startServer(registry pakBroker.BrokerRegistry, db *sql.DB, brokerapi http.H port := viper.GetString(apiPortProp) host := viper.GetString(apiHostProp) logger.Info("Serving", lager.Data{"port": port}) - _ = http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), router) + + tlsCertCaBundle := viper.GetString(tlsCertCaBundleProp) + tlsKey := viper.GetString(tlsKeyProp) + + logger.Info("tlsCertCaBundle", lager.Data{"tlsCertCaBundle": tlsCertCaBundle}) + logger.Info("tlsKey", lager.Data{"tlsKey": tlsKey}) + + httpServer := &http.Server{ + Addr: fmt.Sprintf("%s:%s", host, port), + Handler: router, + } + + if tlsCertCaBundle != "" && tlsKey != "" { + err := httpServer.ListenAndServeTLS(tlsCertCaBundle, tlsKey) + if err != nil { + logger.Fatal("Failed to start broker", err) + } + } else { + _ = httpServer.ListenAndServe() + } } func labelName(label string) string { diff --git a/integrationtest/server_test.go b/integrationtest/server_test.go new file mode 100644 index 000000000..b6f9d81a4 --- /dev/null +++ b/integrationtest/server_test.go @@ -0,0 +1,66 @@ +package integrationtest_test + +import ( + "fmt" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/cloudfoundry/cloud-service-broker/v3/integrationtest/packer" + "github.com/cloudfoundry/cloud-service-broker/v3/internal/testdrive" +) + +var _ = Describe("Starting Server", func() { + + const userProvidedPlan = `[{"name": "user-plan-unique","id":"8b52a460-b246-11eb-a8f5-d349948e2481"}]` + + var brokerpak string + + BeforeEach(func() { + brokerpak = must(packer.BuildBrokerpak(csb, fixtures("service-catalog"))) + + DeferCleanup(func() { + cleanup(brokerpak) + }) + }) + + FWhen("TLS data is provided", func() { + When("Valid data exists", func() { + It("Should accept HTTPS requests", func() { + isValid := true + broker, err := testdrive.StartBroker(csb, brokerpak, database, testdrive.WithTLSConfig(isValid), testdrive.WithEnv(fmt.Sprintf("GSB_SERVICE_ALPHA_SERVICE_PLANS=%s", userProvidedPlan)), testdrive.WithOutputs(GinkgoWriter, GinkgoWriter)) + Expect(err).NotTo(HaveOccurred()) + + _, err = http.Get(fmt.Sprintf("https://localhost:%d", broker.Port)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + When("Invalid data exists", func() { + It("Should fail to start", func() { + notValid := false + _, err := testdrive.StartBroker(csb, brokerpak, database, testdrive.WithTLSConfig(notValid), testdrive.WithEnv(fmt.Sprintf("GSB_SERVICE_ALPHA_SERVICE_PLANS=%s", userProvidedPlan)), testdrive.WithOutputs(GinkgoWriter, GinkgoWriter)) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + FWhen("No TLS data is provided", func() { + It("Should return an error for HTTPS requests", func() { + broker, err := testdrive.StartBroker(csb, brokerpak, database, testdrive.WithEnv(fmt.Sprintf("GSB_SERVICE_ALPHA_SERVICE_PLANS=%s", userProvidedPlan)), testdrive.WithOutputs(GinkgoWriter, GinkgoWriter)) + Expect(err).NotTo(HaveOccurred()) + + _, err = http.Get(fmt.Sprintf("https://localhost:%d", broker.Port)) + Expect(err).To(HaveOccurred()) + }) + + It("Should succeed for HTTP requests", func() { + broker, err := testdrive.StartBroker(csb, brokerpak, database, testdrive.WithEnv(fmt.Sprintf("GSB_SERVICE_ALPHA_SERVICE_PLANS=%s", userProvidedPlan)), testdrive.WithOutputs(GinkgoWriter, GinkgoWriter)) + Expect(err).NotTo(HaveOccurred()) + + _, err = http.Get(fmt.Sprintf("http://localhost:%d", broker.Port)) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/internal/testdrive/broker_start.go b/internal/testdrive/broker_start.go index edc506cd1..fbc0b93c5 100644 --- a/internal/testdrive/broker_start.go +++ b/internal/testdrive/broker_start.go @@ -2,8 +2,16 @@ package testdrive import ( "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" "io" + "math/big" + "net" "net/http" "os" "os/exec" @@ -83,7 +91,16 @@ func StartBroker(csbPath, bpk, db string, opts ...StartBrokerOption) (*Broker, e start := time.Now() for { - response, err := http.Head(fmt.Sprintf("http://localhost:%d", port)) + scheme := "http" + for _, envVar := range cmd.Env { + if strings.HasPrefix(envVar, "TLS_") { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + scheme = "https" + break + } + } + + response, err := http.Head(fmt.Sprintf("%s://localhost:%d", scheme, port)) switch { case err == nil && response.StatusCode == http.StatusOK: return &broker, nil @@ -99,6 +116,95 @@ func StartBroker(csbPath, bpk, db string, opts ...StartBrokerOption) (*Broker, e } } +func createCAKeyPair(msg string) (*x509.Certificate, []byte, *rsa.PrivateKey) { + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Country: []string{msg}, + }, + IsCA: true, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + Expect(err).NotTo(HaveOccurred()) + + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + Expect(err).NotTo(HaveOccurred()) + + caPEM, _ := encodeKeyPair(caBytes, x509.MarshalPKCS1PrivateKey(caPrivKey)) + + return ca, caPEM, caPrivKey +} + +func createKeyPairSignedByCA(ca *x509.Certificate, caPrivKey *rsa.PrivateKey) ([]byte, []byte) { + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1658), + Subject: pkix.Name{ + Country: []string{"GB"}, + }, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + Expect(err).NotTo(HaveOccurred()) + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) + Expect(err).NotTo(HaveOccurred()) + + certPEM, certPrivKeyPEM := encodeKeyPair(certBytes, x509.MarshalPKCS1PrivateKey(certPrivKey)) + return certPEM, certPrivKeyPEM +} + +func encodeKeyPair(caBytes, caPrivKeyBytes []byte) ([]byte, []byte) { + caPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + caPrivKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: caPrivKeyBytes, + }) + + return caPEM, caPrivKeyPEM +} + +func WithTLSConfig(isValid bool) StartBrokerOption { + return func(cfg *startBrokerConfig) { + ca, _, caPrivKey := createCAKeyPair("US") + + serverCert, serverPrivKey := createKeyPairSignedByCA(ca, caPrivKey) + + certFileBuf, err := os.CreateTemp("", "") + Expect(err).NotTo(HaveOccurred()) + defer certFileBuf.Close() + + privKeyFileBuf, err := os.CreateTemp("", "") + Expect(err).NotTo(HaveOccurred()) + defer privKeyFileBuf.Close() + + if !isValid { + serverPrivKey[10] = 'a' + } + + Expect(os.WriteFile(privKeyFileBuf.Name(), serverPrivKey, 0644)).To(Succeed()) + + Expect(os.WriteFile(certFileBuf.Name(), serverCert, 0644)).To(Succeed()) + + cfg.env = append(cfg.env, fmt.Sprintf("TLS_CERT_CHAIN=%s", certFileBuf.Name())) + cfg.env = append(cfg.env, fmt.Sprintf("TLS_PRIVATE_KEY=%s", privKeyFileBuf.Name())) + } +} + func WithEnv(extraEnv ...string) StartBrokerOption { return func(cfg *startBrokerConfig) { cfg.env = append(cfg.env, extraEnv...)