diff --git a/cmd/create_env_test.go b/cmd/create_env_test.go index aac570267..00b1a70a4 100644 --- a/cmd/create_env_test.go +++ b/cmd/create_env_test.go @@ -183,7 +183,7 @@ var _ = Describe("CreateEnvCmd", func() { mockBlobstoreFactory = mockblobstore.NewMockFactory(mockCtrl) mockBlobstore = mockblobstore.NewMockBlobstore(mockCtrl) - mockBlobstoreFactory.EXPECT().Create(mbusURL, gomock.Any()).Return(mockBlobstore, nil).AnyTimes() + mockBlobstoreFactory.EXPECT().Create(mbusURL, SecureTLSClientMatcher()).Return(mockBlobstore, nil).AnyTimes() mockVMManagerFactory = mockvm.NewMockManagerFactory(mockCtrl) fakeVMManager = fakebivm.NewFakeManager() @@ -254,6 +254,9 @@ var _ = Describe("CreateEnvCmd", func() { {Name: "fake-cpi-release-job-name", Release: "fake-cpi-release-name"}, }, Mbus: mbusURL, + Cert: biinstallmanifest.Certificate{ + CA: validCACert, + }, } // parsed BOSH deployment manifest diff --git a/cmd/deployment_deleter.go b/cmd/deployment_deleter.go index d1f278bba..d761c8fee 100644 --- a/cmd/deployment_deleter.go +++ b/cmd/deployment_deleter.go @@ -230,7 +230,11 @@ func (c *deploymentDeleter) deploymentManager(installation biinstall.Installatio c.logger.Debug(c.logTag, "Creating blobstore client...") - blobstore, err := c.blobstoreFactory.Create(installationMbus, bihttpclient.CreateDefaultClientInsecureSkipVerify()) + certPool, err := biinstallmanifest.Certificate{CA: caCert}.CACertPool() + if err != nil { + return nil, bosherr.WrapError(err, "Parsing CA certificate for blobstore client") + } + blobstore, err := c.blobstoreFactory.Create(installationMbus, bihttpclient.CreateDefaultClient(certPool)) if err != nil { return nil, bosherr.WrapError(err, "Creating blobstore client") } diff --git a/cmd/deployment_deleter_test.go b/cmd/deployment_deleter_test.go index 9a8a1cccc..5949745a2 100644 --- a/cmd/deployment_deleter_test.go +++ b/cmd/deployment_deleter_test.go @@ -339,7 +339,7 @@ cloud_provider: mockBlobstoreFactory = mockblobstore.NewMockFactory(mockCtrl) mockBlobstore = mockblobstore.NewMockBlobstore(mockCtrl) - mockBlobstoreFactory.EXPECT().Create(mbusURL, gomock.Any()).Return(mockBlobstore, nil).AnyTimes() + mockBlobstoreFactory.EXPECT().Create(mbusURL, SecureTLSClientMatcher()).Return(mockBlobstore, nil).AnyTimes() mockDeploymentManagerFactory = mockdeployment.NewMockManagerFactory(mockCtrl) mockDeploymentManager = mockdeployment.NewMockManager(mockCtrl) diff --git a/cmd/deployment_preparer.go b/cmd/deployment_preparer.go index cdca826e0..a132b0b0f 100644 --- a/cmd/deployment_preparer.go +++ b/cmd/deployment_preparer.go @@ -250,7 +250,11 @@ func (c *DeploymentPreparer) deploy( } vmManager := c.vmManagerFactory.NewManager(cloud, agentClient) - blobstore, err := c.blobstoreFactory.Create(installationManifest.Mbus, bihttpclient.CreateDefaultClientInsecureSkipVerify()) + certPool, err := installationManifest.Cert.CACertPool() + if err != nil { + return bosherr.WrapError(err, "Parsing CA certificate for blobstore client") + } + blobstore, err := c.blobstoreFactory.Create(installationManifest.Mbus, bihttpclient.CreateDefaultClient(certPool)) if err != nil { return bosherr.WrapError(err, "Creating blobstore client") } diff --git a/cmd/suite_test.go b/cmd/suite_test.go index d3e47471b..145e7f84a 100644 --- a/cmd/suite_test.go +++ b/cmd/suite_test.go @@ -2,8 +2,10 @@ package cmd_test import ( "crypto/tls" + "net/http" "testing" + "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -15,6 +17,7 @@ var ( cacertBytes []byte validCACert string ) + var _ = BeforeSuite(func() { var err error cert, cacertBytes, err = testutils.CertSetup() @@ -26,3 +29,28 @@ func TestReg(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "cmd") } + +// secureTLSClientMatcher is a gomock.Matcher that asserts an *http.Client +// has TLS certificate verification enabled (InsecureSkipVerify == false). +// Use SecureTLSClientMatcher() to obtain an instance. +type secureTLSClientMatcher struct{} + +func SecureTLSClientMatcher() gomock.Matcher { + return secureTLSClientMatcher{} +} + +func (m secureTLSClientMatcher) Matches(x interface{}) bool { + client, ok := x.(*http.Client) + if !ok { + return false + } + transport, ok := client.Transport.(*http.Transport) + if !ok { + return false + } + return transport.TLSClientConfig != nil && !transport.TLSClientConfig.InsecureSkipVerify +} + +func (m secureTLSClientMatcher) String() string { + return "is a secure *http.Client with TLS certificate verification enabled (InsecureSkipVerify=false)" +} diff --git a/installation/manifest/certificate_test.go b/installation/manifest/certificate_test.go new file mode 100644 index 000000000..73ab31ea7 --- /dev/null +++ b/installation/manifest/certificate_test.go @@ -0,0 +1,52 @@ +package manifest_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "github.com/cloudfoundry/bosh-cli/v7/installation/manifest" +) + +var _ = Describe("Certificate", func() { + Describe("CACertPool", func() { + It("returns nil when CA is empty", func() { + certPool, err := Certificate{CA: ""}.CACertPool() + Expect(err).ToNot(HaveOccurred()) + Expect(certPool).To(BeNil()) + }) + + It("returns a cert pool when a valid PEM CA cert is provided", func() { + caCert := `-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgIJAPerMgLAne5vMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwIBcNMTYwMTE2MDY0NTA0WhgPMjI4OTEwMzAwNjQ1MDRa +MEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ +bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCtSo3KPjnVPzodb6+mNwbCdcpzVop8OmfwJ3ynQtyBEzGaKsAn4tlz +/wfQQrKFHgxqVpqcoxAlWPNMs5+iO2Jst3Gz2+oLcaDyz/EWorw0iF5q1F6+WYHp +EijY20MzaWYMyu4UhhlbJCkSGZSjujh5SFOAXQwWYJXsqjyxA9KaTD6OdH5Kpger +B9D4zogX0We00eouyvvz/sAeDbTshk9sJRGWHNFJr+TjVx2D01alU49liAL94yF6 +1eEOEbE50OAhv9RNsRh6O58idaHg30bbMf1yAzcgBvh8CzIHH0BPofoF2pRfztoY +uudZ0ftJjTz4fA2h/7GOVzxemrTjx88vAgMBAAGjUDBOMB0GA1UdDgQWBBQjz5Q2 +YW2kBTb4XLqKFZMSBLpi6zAfBgNVHSMEGDAWgBQjz5Q2YW2kBTb4XLqKFZMSBLpi +6zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQA/s94M/mSGELHJWIb1 +oE0IKHWajBd3Pc8+O1TZRE+ke3q+rZRfcxd2dAjq6zQHJUs2+fs0B3DyT9Wtyyoq +UrRdsgprOdf2Cuw8bMIsCQOvqWKhhdlLTnCi2xaGJawGsIkheuD1n+Il9gRQ2WGy +lACxVngPwjNYxjOE+CUnSZCuAmAfQYzqto3bNPqkgEwb7ueODeOiyhR8SKsH7ySW +QAOSxgrLBblGLWcDF9fjMeYaUnI34pHviCKeVxfgsxDR+Jg11F78sPdYLOF6ipBe +/5qTYucsY20B2EKtlscD0mSYBRwbVrSQt2RYbTCwaibxWUC13VV+YEk0NAv9Mm04 +6sKO +-----END CERTIFICATE-----` + + certPool, err := Certificate{CA: caCert}.CACertPool() + Expect(err).ToNot(HaveOccurred()) + Expect(certPool).ToNot(BeNil()) + }) + + It("returns an error when the CA cert is not valid PEM", func() { + _, err := Certificate{CA: "not-a-valid-pem"}.CACertPool() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Parsing certificate 1: Missing PEM block")) + }) + }) +}) diff --git a/installation/manifest/manifest.go b/installation/manifest/manifest.go index 1cd724f9e..3c021f771 100644 --- a/installation/manifest/manifest.go +++ b/installation/manifest/manifest.go @@ -1,6 +1,9 @@ package manifest import ( + "crypto/x509" + + boshcrypto "github.com/cloudfoundry/bosh-utils/crypto" biproperty "github.com/cloudfoundry/bosh-utils/property" ) @@ -18,6 +21,15 @@ type Certificate struct { CA string } +// CACertPool parses the PEM-encoded CA certificate and returns an *x509.CertPool. +// Returns (nil, nil) when CA is empty, meaning the system root CAs will be used. +func (c Certificate) CACertPool() (*x509.CertPool, error) { + if len(c.CA) == 0 { + return nil, nil + } + return boshcrypto.CertPoolFromPEM([]byte(c.CA)) +} + type ReleaseJobRef struct { Name string Release string