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 290446f
Show file tree
Hide file tree
Showing 4 changed files with 201 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
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Broker service configuration values:
| <tt>SECURITY_USER_NAME</tt> <b>*</b> | api.user | string | <p>Broker authentication username</p>|
| <tt>SECURITY_USER_PASSWORD</tt> <b>*</b> | api.password | string | <p>Broker authentication password</p>|
| <tt>PORT</tt> | api.port | string | <p>Port to bind broker to</p>|
| <tt>TLS_CERT_CHAIN</tt> | api.certCaBundle | string | <p>File path to a pem encoded certificate chain</p>|
| <tt>TLS_PRIVATE_KEY</tt> | api.tlsKey | string | <p>File path to a pem encoded private key</p>|

## Feature flags Configuration

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/v2/integrationtest/packer"
"github.com/cloudfoundry/cloud-service-broker/v2/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)
})
})

When("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())
})
})
})

When("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())
})
})
})
110 changes: 109 additions & 1 deletion internal/testdrive/broker_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ 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"
"strings"
"time"

. "github.com/onsi/gomega"

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

View workflow job for this annotation

GitHub Actions / Go test

should not use dot imports (ST1001)

"github.com/cloudfoundry/cloud-service-broker/v2/pkg/client"
"github.com/cloudfoundry/cloud-service-broker/v2/utils/freeport"
"github.com/google/uuid"
Expand Down Expand Up @@ -83,7 +93,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 +118,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...)
Expand Down

0 comments on commit 290446f

Please sign in to comment.