Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support HTTPS endpoints #1084

Merged
merged 6 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 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,27 @@ 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)

tlsCertCaBundleFilePath := viper.GetString(tlsCertCaBundleProp)
tlsKeyFilePath := viper.GetString(tlsKeyProp)

logger.Info("tlsCertCaBundle", lager.Data{"tlsCertCaBundle": tlsCertCaBundleFilePath})
logger.Info("tlsKey", lager.Data{"tlsKey": tlsKeyFilePath})

httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%s", host, port),
Handler: router,
}
var err error
if tlsCertCaBundleFilePath != "" && tlsKeyFilePath != "" {
err = httpServer.ListenAndServeTLS(tlsCertCaBundleFilePath, tlsKeyFilePath)
} else {
err = httpServer.ListenAndServe()
}
// when the server is receiving a signal, we probably do not want to panic.
if err != http.ErrServerClosed {
logger.Fatal("Failed to start broker", err)
}
}

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
2 changes: 1 addition & 1 deletion integrationtest/import_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ var _ = Describe("Import State", func() {
By("importing state into the vacant service instance")
req := must(http.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:%d/import_state/%s", broker.Port, instance.GUID), bytes.NewReader(stateToImport)))
req.SetBasicAuth(broker.Username, broker.Password)
importResponse := must(http.DefaultClient.Do(req))
importResponse := must(broker.Client.Do(req))
Expect(importResponse).To(HaveHTTPStatus(http.StatusOK))

By("checking that the state was imported into the database")
Expand Down
1 change: 1 addition & 0 deletions integrationtest/maintenance_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var _ = Describe("Maintenance Info", func() {
broker = must(testdrive.StartBroker(
csb, brokerpak, database,
testdrive.WithOutputs(GinkgoWriter, GinkgoWriter),
testdrive.WithTLSConfig(),
testdrive.WithEnv("TERRAFORM_UPGRADES_ENABLED=true"),
))
})
Expand Down
64 changes: 64 additions & 0 deletions integrationtest/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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() {
broker, err := testdrive.StartBroker(csb, brokerpak, database, testdrive.WithTLSConfig(), testdrive.WithEnv(fmt.Sprintf("GSB_SERVICE_ALPHA_SERVICE_PLANS=%s", userProvidedPlan)), testdrive.WithOutputs(GinkgoWriter, GinkgoWriter))
Expect(err).NotTo(HaveOccurred())

_, err = broker.Client.Get(fmt.Sprintf("https://localhost:%d", broker.Port))
Expect(err).NotTo(HaveOccurred())
})
})

When("Invalid data exists", func() {
It("Should fail to start", func() {
_, err := testdrive.StartBroker(csb, brokerpak, database, testdrive.WithInvalidTLSConfig(), 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())
})
})
})
124 changes: 123 additions & 1 deletion internal/testdrive/broker_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@ 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"

//lint:ignore ST1001 we do not care because this is a test helper
. "github.com/onsi/gomega"

"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 @@ -82,8 +93,24 @@ func StartBroker(csbPath, bpk, db string, opts ...StartBrokerOption) (*Broker, e
}

start := time.Now()

scheme := "http"

for _, envVar := range cmd.Env {
if strings.HasPrefix(envVar, "TLS_") {

ignoreSelfSignedCerts := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
broker.Client.BaseURL.Scheme = "https"
broker.Client.Transport = ignoreSelfSignedCerts
scheme = "https"
break
}
}

for {
response, err := http.Head(fmt.Sprintf("http://localhost:%d", port))
response, err := broker.Client.Head(fmt.Sprintf("%s://localhost:%d", scheme, port))
switch {
case err == nil && response.StatusCode == http.StatusOK:
return &broker, nil
Expand All @@ -99,6 +126,101 @@ func StartBroker(csbPath, bpk, db string, opts ...StartBrokerOption) (*Broker, e
}
}

func createCAKeyPair(msg string) (*x509.Certificate, *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())

return ca, 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())

return encodeKeyPair(certBytes, x509.MarshalPKCS1PrivateKey(certPrivKey))
}

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 WithInvalidTLSConfig() StartBrokerOption {
return func(cfg *startBrokerConfig) {
tlsConfig(cfg, false)
}
}

func WithTLSConfig() StartBrokerOption {
return func(cfg *startBrokerConfig) {
tlsConfig(cfg, true)
}
}

func tlsConfig(cfg *startBrokerConfig, valid bool) {
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 !valid {
// If the isValid parameter is false, the server private key is intentionally corrupted
// by modifying one of its bytes.
serverPrivKey[10] = 'a'
}

Expect(os.WriteFile(privKeyFileBuf.Name(), serverPrivKey, 0o644)).To(Succeed())

Expect(os.WriteFile(certFileBuf.Name(), serverCert, 0o644)).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
3 changes: 2 additions & 1 deletion pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func New(username, password, hostname string, port int) (*Client, error) {
}

type Client struct {
http.Client
BaseURL *url.URL
}

Expand Down Expand Up @@ -135,7 +136,7 @@ func (client *Client) makeRequest(method, path, requestID string, body any) *Bro
return &br
}

resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)

br.UpdateResponse(resp)
br.UpdateError(err)
Expand Down
Loading