Skip to content

Commit

Permalink
feat: added tls support to cloud service broker app
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ifindlay-cci committed Sep 3, 2024
1 parent 8860e24 commit ab9839c
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 2 deletions.
25 changes: 24 additions & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
66 changes: 66 additions & 0 deletions integrationtest/server_test.go
Original file line number Diff line number Diff line change
@@ -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"

Check failure on line 10 in integrationtest/server_test.go

View workflow job for this annotation

GitHub Actions / Go test

no required module provides package github.com/cloudfoundry/cloud-service-broker/v3/integrationtest/packer; to add it:
"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())
})
})
})
108 changes: 107 additions & 1 deletion internal/testdrive/broker_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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())

Check failure on line 133 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: Expect

Check failure on line 133 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: HaveOccurred

caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
Expect(err).NotTo(HaveOccurred())

Check failure on line 136 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: Expect

Check failure on line 136 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: 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())

Check failure on line 158 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: Expect

Check failure on line 158 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: HaveOccurred

certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)
Expect(err).NotTo(HaveOccurred())

Check failure on line 161 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: Expect

Check failure on line 161 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: 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())

Check failure on line 188 in internal/testdrive/broker_start.go

View workflow job for this annotation

GitHub Actions / Go test

undefined: Expect
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...)
Expand Down

0 comments on commit ab9839c

Please sign in to comment.