From 202e10d9ab0511d063ef3b487a0d9dc1e79e81a8 Mon Sep 17 00:00:00 2001 From: Nishad Mathur Date: Thu, 8 Oct 2026 18:07:24 -0700 Subject: [PATCH 1/6] Write agent mbus password verifiers for capable stemcells create-env passes cloud_provider.properties.agent.mbus to the CPI. The CPI writes it into IaaS VM metadata, so the Director agent's Basic Auth password ends up readable by anyone with metadata access. When the stemcell's stemcell.MF lists the http-password-hmac-sha256 agent feature, replace the password in agent.mbus, and in any resource pool env.bosh.mbus.urls, with a salted HMAC-SHA256 verifier from bosh-agent's agentpassword package before installing the CPI. The CLI keeps using the plaintext cloud_provider.mbus to talk to the agent, so only the copy that reaches the VM is hashed. Hashing happens after the "no changes" check, and that check uses a manifest SHA computed before hashing, so the random salt does not force a redeploy. Compatibility: - New CLI, stemcell without the feature: nothing is hashed. - Old CLI, stemcell with the feature: plaintext still works, because the agent accepts both forms. ai-assisted=yes TNZ-129825 Co-Authored-By: Chris Selzo Co-Authored-By: Claude Opus 5.5 --- cmd/agent_password.go | 61 +++++++ cmd/agent_password_test.go | 141 ++++++++++++++++ cmd/create_env_test.go | 159 ++++++++++++++++++ cmd/deployment_preparer.go | 14 ++ go.mod | 2 +- go.sum | 4 +- stemcell/agent_features_test.go | 30 ++++ stemcell/stemcell.go | 8 + .../v2/agentpassword/agentpassword.go | 93 ++++++++++ vendor/modules.txt | 3 +- 10 files changed, 511 insertions(+), 4 deletions(-) create mode 100644 cmd/agent_password.go create mode 100644 cmd/agent_password_test.go create mode 100644 stemcell/agent_features_test.go create mode 100644 vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go diff --git a/cmd/agent_password.go b/cmd/agent_password.go new file mode 100644 index 000000000..f371f33f0 --- /dev/null +++ b/cmd/agent_password.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "fmt" + + "github.com/cloudfoundry/bosh-agent/v2/agentpassword" + "github.com/cloudfoundry/bosh-utils/property" +) + +func hashCPIAgentMbus(props property.Map) error { + agent, _ := props["agent"].(property.Map) + mbus, ok := agent["mbus"].(string) + if !ok { + return nil + } + hashed, err := agentpassword.HashURL(mbus) + if err != nil { + return err + } + agent["mbus"] = hashed + return nil +} + +func hashResourcePoolMbusURLs(env property.Map) error { + boshRaw, ok := env["bosh"] + if !ok { + return nil + } + bosh, ok := boshRaw.(property.Map) + if !ok { + return fmt.Errorf("env.bosh must be a map") + } + mbusRaw, ok := bosh["mbus"] + if !ok { + return nil + } + mbus, ok := mbusRaw.(property.Map) + if !ok { + return fmt.Errorf("env.bosh.mbus must be a map") + } + urlsRaw, ok := mbus["urls"] + if !ok { + return nil + } + urls, ok := urlsRaw.(property.List) + if !ok { + return fmt.Errorf("env.bosh.mbus.urls must be a list") + } + for i, entryRaw := range urls { + entry, ok := entryRaw.(string) + if !ok { + return fmt.Errorf("env.bosh.mbus.urls entry must be a string") + } + hashed, err := agentpassword.HashURL(entry) + if err != nil { + return err + } + urls[i] = hashed + } + return nil +} diff --git a/cmd/agent_password_test.go b/cmd/agent_password_test.go new file mode 100644 index 000000000..07dd38cc3 --- /dev/null +++ b/cmd/agent_password_test.go @@ -0,0 +1,141 @@ +package cmd + +import ( + "github.com/cloudfoundry/bosh-agent/v2/agentpassword" + "github.com/cloudfoundry/bosh-utils/property" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("hashCPIAgentMbus", func() { + It("hashes agent.mbus in place when present", func() { + raw := "https://vcap:original-password@0.0.0.0:6868" + properties := property.Map{ + "agent": property.Map{ + "mbus": raw, + }, + "unrelated": "preserved", + } + + err := hashCPIAgentMbus(properties) + Expect(err).NotTo(HaveOccurred()) + + hashed := properties["agent"].(property.Map)["mbus"].(string) + Expect(hashed).To(HavePrefix("https://vcap:bosh-hmac-sha256$")) + Expect(properties["unrelated"]).To(Equal("preserved")) + + verifierStr := "bosh-hmac-sha256$" + hashed[len("https://vcap:bosh-hmac-sha256$"):len(hashed)-len("@0.0.0.0:6868")] + verifier, err := agentpassword.ParseVerifier(verifierStr) + Expect(err).NotTo(HaveOccurred()) + Expect(verifier.Matches("original-password")).To(BeTrue()) + }) + + It("is a no-op when props is nil or empty", func() { + Expect(hashCPIAgentMbus(nil)).To(Succeed()) + Expect(hashCPIAgentMbus(property.Map{})).To(Succeed()) + }) + + It("is a no-op when agent is absent or not a property.Map", func() { + props := property.Map{"unrelated": "preserved"} + Expect(hashCPIAgentMbus(props)).To(Succeed()) + Expect(props["unrelated"]).To(Equal("preserved")) + + props = property.Map{"agent": "not-a-map"} + Expect(hashCPIAgentMbus(props)).To(Succeed()) + Expect(props["agent"]).To(Equal("not-a-map")) + }) + + It("is a no-op when agent.mbus is absent or not a string", func() { + props := property.Map{"agent": property.Map{"other": "preserved"}} + Expect(hashCPIAgentMbus(props)).To(Succeed()) + Expect(props["agent"].(property.Map)["other"]).To(Equal("preserved")) + + props = property.Map{"agent": property.Map{"mbus": 123}} + Expect(hashCPIAgentMbus(props)).To(Succeed()) + Expect(props["agent"].(property.Map)["mbus"]).To(Equal(123)) + }) + + It("returns error if HashURL fails", func() { + props := property.Map{"agent": property.Map{"mbus": "invalid-url-%%%"}} + Expect(hashCPIAgentMbus(props)).To(HaveOccurred()) + }) +}) + +var _ = Describe("hashResourcePoolMbusURLs", func() { + It("hashes urls entries in place and preserves unrelated keys", func() { + raw := "https://vcap:original-password@0.0.0.0:6868" + env := property.Map{ + "bosh": property.Map{ + "mbus": property.Map{ + "urls": property.List{raw}, + "cert": property.Map{"certificate": "preserved"}, + }, + }, + "unrelated": "preserved", + } + + err := hashResourcePoolMbusURLs(env) + Expect(err).NotTo(HaveOccurred()) + + boshMap := env["bosh"].(property.Map) + mbusMap := boshMap["mbus"].(property.Map) + hashedURLs := mbusMap["urls"].(property.List) + Expect(hashedURLs).To(HaveLen(1)) + + hashed := hashedURLs[0].(string) + Expect(hashed).To(HavePrefix("https://vcap:bosh-hmac-sha256$")) + Expect(mbusMap["cert"].(property.Map)["certificate"]).To(Equal("preserved")) + Expect(env["unrelated"]).To(Equal("preserved")) + + verifierStr := "bosh-hmac-sha256$" + hashed[len("https://vcap:bosh-hmac-sha256$"):len(hashed)-len("@0.0.0.0:6868")] + verifier, err := agentpassword.ParseVerifier(verifierStr) + Expect(err).NotTo(HaveOccurred()) + Expect(verifier.Matches("original-password")).To(BeTrue()) + }) + + It("is a no-op when env is nil or empty", func() { + Expect(hashResourcePoolMbusURLs(nil)).To(Succeed()) + Expect(hashResourcePoolMbusURLs(property.Map{})).To(Succeed()) + }) + + It("is a no-op when missing a key at any level", func() { + env := property.Map{"unrelated": "val"} + Expect(hashResourcePoolMbusURLs(env)).To(Succeed()) + + env = property.Map{"bosh": property.Map{"other": "val"}} + Expect(hashResourcePoolMbusURLs(env)).To(Succeed()) + + env = property.Map{"bosh": property.Map{"mbus": property.Map{"other": "val"}}} + Expect(hashResourcePoolMbusURLs(env)).To(Succeed()) + }) + + It("returns an error without URLs when bosh is not a map", func() { + env := property.Map{"bosh": "not-a-map"} + err := hashResourcePoolMbusURLs(env) + Expect(err).To(MatchError("env.bosh must be a map")) + }) + + It("returns an error without URLs when mbus is not a map", func() { + env := property.Map{"bosh": property.Map{"mbus": "not-a-map"}} + err := hashResourcePoolMbusURLs(env) + Expect(err).To(MatchError("env.bosh.mbus must be a map")) + }) + + It("returns an error without URLs when urls is not a list", func() { + env := property.Map{"bosh": property.Map{"mbus": property.Map{"urls": "not-a-list"}}} + err := hashResourcePoolMbusURLs(env) + Expect(err).To(MatchError("env.bosh.mbus.urls must be a list")) + }) + + It("returns an error without URLs when any entry in urls is not a string", func() { + env := property.Map{"bosh": property.Map{"mbus": property.Map{"urls": property.List{123}}}} + err := hashResourcePoolMbusURLs(env) + Expect(err).To(MatchError("env.bosh.mbus.urls entry must be a string")) + }) + + It("returns error if HashURL fails without leaking URL", func() { + env := property.Map{"bosh": property.Map{"mbus": property.Map{"urls": property.List{"invalid-url-%%%"}}}} + err := hashResourcePoolMbusURLs(env) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/cmd/create_env_test.go b/cmd/create_env_test.go index 06e5ebfc0..35dcff52a 100644 --- a/cmd/create_env_test.go +++ b/cmd/create_env_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "regexp" + "github.com/cloudfoundry/bosh-agent/v2/agentpassword" bosherr "github.com/cloudfoundry/bosh-utils/errors" boshlog "github.com/cloudfoundry/bosh-utils/logger" biproperty "github.com/cloudfoundry/bosh-utils/property" @@ -557,6 +558,164 @@ var _ = Describe("CreateEnvCmd", func() { Expect(mockCloudFactory.NewCloudCallCount()).To(Equal(1)) }) + Context("when stemcell advertises http agent password verifiers feature", func() { + JustBeforeEach(func() { + extractedStemcell = bistemcell.NewExtractedStemcell( + bistemcell.Manifest{ + Name: "fake-stemcell-name", + Version: "fake-stemcell-version", + SHA1: "fake-stemcell-sha1", + ApiVersion: stemcellApiVersion, + CloudProperties: biproperty.Map{}, + AgentFeatures: []string{agentpassword.Feature}, + }, + "fake-extracted-path", + nil, + fs, + ) + fakeStemcellExtractor.SetExtractBehavior(stemcellTarballPath, extractedStemcell, nil) + + installationManifest.Properties = biproperty.Map{ + "agent": biproperty.Map{ + "mbus": "https://vcap:some-secret-pw@127.0.0.1:6868", + }, + } + installationManifest.Mbus = "https://vcap:director-client-pw@127.0.0.1:6868" + fakeInstallationParser.ParseReturns(installationManifest, nil) + + boshDeploymentManifest.ResourcePools[0].Env = biproperty.Map{ + "bosh": biproperty.Map{ + "mbus": biproperty.Map{ + "urls": biproperty.List{"https://vcap:rp-secret-pw@127.0.0.1:6868"}, + }, + }, + } + fakeDeploymentParser.ParseReturns(boshDeploymentManifest, nil) + }) + + It("hashes installationManifest agent.mbus before passing to CPI installer, while preserving client mbus", func() { + err := command.Run(fakeStage, defaultCreateEnvOpts) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockInstaller.InstallCallCount()).To(Equal(1)) + installedManifest, _ := mockInstaller.InstallArgsForCall(0) + agentProps := installedManifest.Properties["agent"].(biproperty.Map) + Expect(agentProps["mbus"]).To(HavePrefix("https://vcap:bosh-hmac-sha256$")) + + Expect(mockAgentClientFactory.NewAgentClientCallCount()).To(Equal(1)) + _, agentClientMbus, _ := mockAgentClientFactory.NewAgentClientArgsForCall(0) + Expect(agentClientMbus).To(Equal("https://vcap:director-client-pw@127.0.0.1:6868")) + + Expect(mockBlobstoreFactory.CreateCallCount()).To(Equal(1)) + blobstoreMbus, _ := mockBlobstoreFactory.CreateArgsForCall(0) + Expect(blobstoreMbus).To(Equal("https://vcap:director-client-pw@127.0.0.1:6868")) + }) + + It("replaces resource pool env.bosh.mbus.urls https entries with a verifier while preserving client mbus", func() { + err := command.Run(fakeStage, defaultCreateEnvOpts) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockDeployer.DeployCallCount()).To(Equal(1)) + _, deployedManifest, _, _, _, _, _, _ := mockDeployer.DeployArgsForCall(0) + Expect(deployedManifest.ResourcePools).To(HaveLen(1)) + boshMap := deployedManifest.ResourcePools[0].Env["bosh"].(biproperty.Map) + mbusMap := boshMap["mbus"].(biproperty.Map) + urls := mbusMap["urls"].(biproperty.List) + Expect(urls).To(HaveLen(1)) + Expect(urls[0].(string)).To(HavePrefix("https://vcap:bosh-hmac-sha256$")) + Expect(urls[0].(string)).To(HaveSuffix("@127.0.0.1:6868")) + + Expect(mockAgentClientFactory.NewAgentClientCallCount()).To(Equal(1)) + _, agentClientMbus, _ := mockAgentClientFactory.NewAgentClientArgsForCall(0) + Expect(agentClientMbus).To(Equal("https://vcap:director-client-pw@127.0.0.1:6868")) + }) + + Context("when cloud_provider.properties.agent.mbus is an https URL with no password", func() { + JustBeforeEach(func() { + installationManifest.Properties = biproperty.Map{ + "agent": biproperty.Map{ + "mbus": "https://vcap@127.0.0.1:6868", + }, + } + fakeInstallationParser.ParseReturns(installationManifest, nil) + }) + + It("returns an error containing 'URL missing password' and does not install or deploy", func() { + err := command.Run(fakeStage, defaultCreateEnvOpts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("URL missing password")) + Expect(mockInstaller.InstallCallCount()).To(Equal(0)) + Expect(mockDeployer.DeployCallCount()).To(Equal(0)) + }) + }) + + Context("when a resource pool env.bosh.mbus.urls entry is an https URL with no password", func() { + JustBeforeEach(func() { + boshDeploymentManifest.ResourcePools[0].Env = biproperty.Map{ + "bosh": biproperty.Map{ + "mbus": biproperty.Map{ + "urls": biproperty.List{"https://vcap@127.0.0.1:6868"}, + }, + }, + } + fakeDeploymentParser.ParseReturns(boshDeploymentManifest, nil) + }) + + It("returns an error containing 'URL missing password' and does not install or deploy", func() { + err := command.Run(fakeStage, defaultCreateEnvOpts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("URL missing password")) + Expect(mockInstaller.InstallCallCount()).To(Equal(0)) + Expect(mockDeployer.DeployCallCount()).To(Equal(0)) + }) + }) + }) + + Context("when stemcell does not advertise http agent password verifiers feature", func() { + JustBeforeEach(func() { + installationManifest.Properties = biproperty.Map{ + "agent": biproperty.Map{ + "mbus": "https://vcap:some-secret-pw@127.0.0.1:6868", + }, + } + installationManifest.Mbus = "https://vcap:director-client-pw@127.0.0.1:6868" + fakeInstallationParser.ParseReturns(installationManifest, nil) + + boshDeploymentManifest.ResourcePools[0].Env = biproperty.Map{ + "bosh": biproperty.Map{ + "mbus": biproperty.Map{ + "urls": biproperty.List{"https://vcap:rp-secret-pw@127.0.0.1:6868"}, + }, + }, + } + fakeDeploymentParser.ParseReturns(boshDeploymentManifest, nil) + }) + + It("leaves installationManifest agent.mbus unchanged", func() { + err := command.Run(fakeStage, defaultCreateEnvOpts) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockInstaller.InstallCallCount()).To(Equal(1)) + installedManifest, _ := mockInstaller.InstallArgsForCall(0) + agentProps := installedManifest.Properties["agent"].(biproperty.Map) + Expect(agentProps["mbus"]).To(Equal("https://vcap:some-secret-pw@127.0.0.1:6868")) + }) + + It("passes resource pool URLs to the deployer unchanged", func() { + err := command.Run(fakeStage, defaultCreateEnvOpts) + Expect(err).NotTo(HaveOccurred()) + + Expect(mockDeployer.DeployCallCount()).To(Equal(1)) + _, deployedManifest, _, _, _, _, _, _ := mockDeployer.DeployArgsForCall(0) + Expect(deployedManifest.ResourcePools).To(HaveLen(1)) + boshMap := deployedManifest.ResourcePools[0].Env["bosh"].(biproperty.Map) + mbusMap := boshMap["mbus"].(biproperty.Map) + urls := mbusMap["urls"].(biproperty.List) + Expect(urls).To(HaveLen(1)) + Expect(urls[0].(string)).To(Equal("https://vcap:rp-secret-pw@127.0.0.1:6868")) + }) + }) + It("adds a new 'installing CPI' event logger stage", func() { err := command.Run(fakeStage, defaultCreateEnvOpts) Expect(err).NotTo(HaveOccurred()) diff --git a/cmd/deployment_preparer.go b/cmd/deployment_preparer.go index a663407e9..9c53130a1 100644 --- a/cmd/deployment_preparer.go +++ b/cmd/deployment_preparer.go @@ -2,6 +2,7 @@ package cmd import ( bihttpagent "github.com/cloudfoundry/bosh-agent/v2/agentclient/http" + "github.com/cloudfoundry/bosh-agent/v2/agentpassword" bosherr "github.com/cloudfoundry/bosh-utils/errors" bihttpclient "github.com/cloudfoundry/bosh-utils/httpclient" boshlog "github.com/cloudfoundry/bosh-utils/logger" @@ -201,6 +202,19 @@ func (c *DeploymentPreparer) PrepareDeployment(stage biui.Stage, opts Deployment return nil } + if extractedStemcell.Manifest().SupportsAgentFeature(agentpassword.Feature) { + err = hashCPIAgentMbus(installationManifest.Properties) + if err != nil { + return bosherr.WrapError(err, "Hashing CPI agent HTTP password") + } + for i := range deploymentManifest.ResourcePools { + err = hashResourcePoolMbusURLs(deploymentManifest.ResourcePools[i].Env) + if err != nil { + return bosherr.WrapError(err, "Hashing resource pool agent HTTP passwords") + } + } + } + err = c.cpiInstaller.WithInstalledCpiRelease(installationManifest, target, stage, func(installation biinstall.Installation) error { stemcellApiVersion := c.stemcellApiVersion(extractedStemcell) diff --git a/go.mod b/go.mod index 5a462ddd5..fe59e91cb 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( code.cloudfoundry.org/clock v1.93.0 code.cloudfoundry.org/workpool v0.0.0-20250911194158-1489753f182e github.com/cheggaaa/pb/v3 v3.2.1 - github.com/cloudfoundry/bosh-agent/v2 v2.908.0 + github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2 github.com/cloudfoundry/bosh-davcli v0.0.518 github.com/cloudfoundry/bosh-gcscli v0.0.426 github.com/cloudfoundry/bosh-s3cli v0.0.447 diff --git a/go.sum b/go.sum index 2b729d288..005158026 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudfoundry/bosh-agent/v2 v2.908.0 h1:osrzAOCaAwHbV3niQI+a3Q5fNNh35bF2s/eU8jBjbns= -github.com/cloudfoundry/bosh-agent/v2 v2.908.0/go.mod h1:suwCWR5eoPDHwyULTcXWmOug8SKLvJJLeT9l49ZkBEs= +github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2 h1:TWy0XwFklvUIwj3g1gngQblNxo1Z8RJyvc/f3avBb20= +github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2/go.mod h1:9LH2n9Yd/4JmztgmEcE7mpc7ionYY7HDGx/sm8Cs1Aw= github.com/cloudfoundry/bosh-davcli v0.0.518 h1:4UndgPJrIzNSws3OXu7bW5kCz2ASOTh8r3ig8lygP5U= github.com/cloudfoundry/bosh-davcli v0.0.518/go.mod h1:DvyFOOWvT/92m8XAMHidtdDjb3PTJ5AalmLAIdeMIzE= github.com/cloudfoundry/bosh-gcscli v0.0.426 h1:yJtJyI55t6p+h+iKyEX9iBqqLe9KQfhlQ4UkydFWM4s= diff --git a/stemcell/agent_features_test.go b/stemcell/agent_features_test.go new file mode 100644 index 000000000..7a4d55210 --- /dev/null +++ b/stemcell/agent_features_test.go @@ -0,0 +1,30 @@ +package stemcell_test + +import ( + "github.com/cloudfoundry/bosh-cli/v7/stemcell" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v2" +) + +var _ = Describe("Agent features", func() { + It("detects supported features and preserves them when repacked", func() { + for _, tc := range []struct { + metadata string + supports bool + }{ + {"name: legacy\napi_version: 3\n", false}, + {"agent_features: [unrelated-feature]\n", false}, + {"agent_features: [http-password-hmac-sha256]\n", true}, + } { + var manifest stemcell.Manifest + Expect(yaml.Unmarshal([]byte(tc.metadata), &manifest)).To(Succeed()) + Expect(manifest.SupportsAgentFeature("http-password-hmac-sha256")).To(Equal(tc.supports)) + packed, err := yaml.Marshal(manifest) + Expect(err).NotTo(HaveOccurred()) + var unpacked stemcell.Manifest + Expect(yaml.Unmarshal(packed, &unpacked)).To(Succeed()) + Expect(unpacked.SupportsAgentFeature("http-password-hmac-sha256")).To(Equal(tc.supports)) + } + }) +}) diff --git a/stemcell/stemcell.go b/stemcell/stemcell.go index d86bd4d61..ddad5a2a2 100644 --- a/stemcell/stemcell.go +++ b/stemcell/stemcell.go @@ -3,6 +3,7 @@ package stemcell import ( "fmt" "path/filepath" + "slices" boshfu "github.com/cloudfoundry/bosh-utils/fileutil" biproperty "github.com/cloudfoundry/bosh-utils/property" @@ -39,10 +40,17 @@ type Manifest struct { SHA1 string `yaml:"sha1"` BoshProtocol string `yaml:"bosh_protocol"` StemcellFormats []string `yaml:"stemcell_formats,omitempty"` + AgentFeatures []string `yaml:"agent_features,omitempty"` ApiVersion int `yaml:"api_version,omitempty"` CloudProperties biproperty.Map `yaml:"cloud_properties"` } +// SupportsAgentFeature reports capabilities advertised for the packaged agent. +// Missing capabilities retain legacy behavior. +func (m Manifest) SupportsAgentFeature(feature string) bool { + return slices.Contains(m.AgentFeatures, feature) +} + func NewExtractedStemcell( manifest Manifest, extractedPath string, diff --git a/vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go b/vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go new file mode 100644 index 000000000..c8b25c2b0 --- /dev/null +++ b/vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go @@ -0,0 +1,93 @@ +package agentpassword + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "errors" + "net/url" + "strings" +) + +const ( + Feature = "http-password-hmac-sha256" + + prefix = "bosh-hmac-sha256$" + saltLength = 16 + keyLength = 32 +) + +type Verifier struct { + salt []byte + key []byte +} + +func IsVerifier(password string) bool { + return strings.HasPrefix(password, prefix) +} + +func ParseVerifier(s string) (*Verifier, error) { + if !strings.HasPrefix(s, prefix) { + return nil, errors.New("malformed HTTP password verifier: missing prefix") + } + parts := strings.Split(s, "$") + if len(parts) != 3 { + return nil, errors.New("malformed HTTP password verifier: invalid part count") + } + salt, err := base64.RawURLEncoding.Strict().DecodeString(parts[1]) + if err != nil || len(salt) != saltLength { + return nil, errors.New("malformed HTTP password verifier: invalid salt") + } + key, err := base64.RawURLEncoding.Strict().DecodeString(parts[2]) + if err != nil || len(key) != keyLength { + return nil, errors.New("malformed HTTP password verifier: invalid key") + } + return &Verifier{salt: salt, key: key}, nil +} + +func (v *Verifier) Matches(password string) bool { + if v == nil { + return false + } + mac := hmac.New(sha256.New, v.salt) + _, _ = mac.Write([]byte(password)) + return subtle.ConstantTimeCompare(v.key, mac.Sum(nil)) == 1 +} + +func HashURL(raw string) (string, error) { + u, err := url.Parse(raw) + if err != nil { + return "", errors.New("invalid URL") + } + if u.Scheme != "https" { + return raw, nil + } + if u.User == nil { + return "", errors.New("URL missing userinfo") + } + password, hasPassword := u.User.Password() + if !hasPassword || password == "" { + return "", errors.New("URL missing password") + } + if IsVerifier(password) { + return "", errors.New("URL password is already a verifier") + } + + salt := make([]byte, saltLength) + if _, err := rand.Read(salt); err != nil { + return "", errors.New("failed to generate random salt") + } + + mac := hmac.New(sha256.New, salt) + _, _ = mac.Write([]byte(password)) + key := mac.Sum(nil) + + encodedSalt := base64.RawURLEncoding.EncodeToString(salt) + encodedKey := base64.RawURLEncoding.EncodeToString(key) + verifier := prefix + encodedSalt + "$" + encodedKey + + u.User = url.UserPassword(u.User.Username(), verifier) + return u.String(), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5e64d654d..c8972ae52 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -225,12 +225,13 @@ github.com/cheggaaa/pb/v3/termutil # github.com/clipperhouse/uax29/v2 v2.7.0 ## explicit; go 1.18 github.com/clipperhouse/uax29/v2/graphemes -# github.com/cloudfoundry/bosh-agent/v2 v2.908.0 +# github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2 ## explicit; go 1.26.0 github.com/cloudfoundry/bosh-agent/v2/agentclient github.com/cloudfoundry/bosh-agent/v2/agentclient/applyspec github.com/cloudfoundry/bosh-agent/v2/agentclient/fakes github.com/cloudfoundry/bosh-agent/v2/agentclient/http +github.com/cloudfoundry/bosh-agent/v2/agentpassword # github.com/cloudfoundry/bosh-davcli v0.0.518 ## explicit; go 1.26.0 github.com/cloudfoundry/bosh-davcli/client From 0d2b0bf1dae1e6d2b87572aabbab065eb1fba7ad Mon Sep 17 00:00:00 2001 From: Nishad Mathur Date: Fri, 9 Oct 2026 11:34:05 -0700 Subject: [PATCH 2/6] Align the CLI draft with the shared hashed password API Update the agent dependency and tracked vendor copy together with the parser references in the command specs. This brings the shared password-length validation into create-env. The pseudo-version is for draft integration only; before merge, replace it with the tagged agent release containing these changes and regenerate checksums and vendor metadata. --- cmd/agent_password_test.go | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- .../v2/agentpassword/agentpassword.go | 22 +++++++++++++------ vendor/modules.txt | 2 +- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cmd/agent_password_test.go b/cmd/agent_password_test.go index 07dd38cc3..af725aac3 100644 --- a/cmd/agent_password_test.go +++ b/cmd/agent_password_test.go @@ -25,7 +25,7 @@ var _ = Describe("hashCPIAgentMbus", func() { Expect(properties["unrelated"]).To(Equal("preserved")) verifierStr := "bosh-hmac-sha256$" + hashed[len("https://vcap:bosh-hmac-sha256$"):len(hashed)-len("@0.0.0.0:6868")] - verifier, err := agentpassword.ParseVerifier(verifierStr) + verifier, err := agentpassword.ParseHashedPassword(verifierStr) Expect(err).NotTo(HaveOccurred()) Expect(verifier.Matches("original-password")).To(BeTrue()) }) @@ -88,7 +88,7 @@ var _ = Describe("hashResourcePoolMbusURLs", func() { Expect(env["unrelated"]).To(Equal("preserved")) verifierStr := "bosh-hmac-sha256$" + hashed[len("https://vcap:bosh-hmac-sha256$"):len(hashed)-len("@0.0.0.0:6868")] - verifier, err := agentpassword.ParseVerifier(verifierStr) + verifier, err := agentpassword.ParseHashedPassword(verifierStr) Expect(err).NotTo(HaveOccurred()) Expect(verifier.Matches("original-password")).To(BeTrue()) }) diff --git a/go.mod b/go.mod index fe59e91cb..a1eac263e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( code.cloudfoundry.org/clock v1.93.0 code.cloudfoundry.org/workpool v0.0.0-20250911194158-1489753f182e github.com/cheggaaa/pb/v3 v3.2.1 - github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2 + github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696 github.com/cloudfoundry/bosh-davcli v0.0.518 github.com/cloudfoundry/bosh-gcscli v0.0.426 github.com/cloudfoundry/bosh-s3cli v0.0.447 diff --git a/go.sum b/go.sum index 005158026..74cdad87f 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2 h1:TWy0XwFklvUIwj3g1gngQblNxo1Z8RJyvc/f3avBb20= -github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2/go.mod h1:9LH2n9Yd/4JmztgmEcE7mpc7ionYY7HDGx/sm8Cs1Aw= +github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696 h1:W9frZ4blZXpLVWUgFMC1MkuNh15Up9gMFT3CymM5YQg= +github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696/go.mod h1:9LH2n9Yd/4JmztgmEcE7mpc7ionYY7HDGx/sm8Cs1Aw= github.com/cloudfoundry/bosh-davcli v0.0.518 h1:4UndgPJrIzNSws3OXu7bW5kCz2ASOTh8r3ig8lygP5U= github.com/cloudfoundry/bosh-davcli v0.0.518/go.mod h1:DvyFOOWvT/92m8XAMHidtdDjb3PTJ5AalmLAIdeMIzE= github.com/cloudfoundry/bosh-gcscli v0.0.426 h1:yJtJyI55t6p+h+iKyEX9iBqqLe9KQfhlQ4UkydFWM4s= diff --git a/vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go b/vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go index c8b25c2b0..03fc7b55a 100644 --- a/vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go +++ b/vendor/github.com/cloudfoundry/bosh-agent/v2/agentpassword/agentpassword.go @@ -7,6 +7,7 @@ import ( "crypto/subtle" "encoding/base64" "errors" + "fmt" "net/url" "strings" ) @@ -14,21 +15,25 @@ import ( const ( Feature = "http-password-hmac-sha256" + // MaxPasswordLength is the maximum plaintext password length in bytes. + MaxPasswordLength = 1024 + prefix = "bosh-hmac-sha256$" saltLength = 16 keyLength = 32 ) -type Verifier struct { +type HashedPassword struct { salt []byte key []byte } -func IsVerifier(password string) bool { +// IsHashedPassword identifies the reserved prefix; ParseHashedPassword validates its contents. +func IsHashedPassword(password string) bool { return strings.HasPrefix(password, prefix) } -func ParseVerifier(s string) (*Verifier, error) { +func ParseHashedPassword(s string) (*HashedPassword, error) { if !strings.HasPrefix(s, prefix) { return nil, errors.New("malformed HTTP password verifier: missing prefix") } @@ -44,10 +49,10 @@ func ParseVerifier(s string) (*Verifier, error) { if err != nil || len(key) != keyLength { return nil, errors.New("malformed HTTP password verifier: invalid key") } - return &Verifier{salt: salt, key: key}, nil + return &HashedPassword{salt: salt, key: key}, nil } -func (v *Verifier) Matches(password string) bool { +func (v *HashedPassword) Matches(password string) bool { if v == nil { return false } @@ -71,8 +76,11 @@ func HashURL(raw string) (string, error) { if !hasPassword || password == "" { return "", errors.New("URL missing password") } - if IsVerifier(password) { - return "", errors.New("URL password is already a verifier") + if len(password) > MaxPasswordLength { + return "", fmt.Errorf("URL password exceeds %d bytes", MaxPasswordLength) + } + if IsHashedPassword(password) { + return "", errors.New("URL password is already hashed; provide the original password") } salt := make([]byte, saltLength) diff --git a/vendor/modules.txt b/vendor/modules.txt index c8972ae52..cb38024de 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -225,7 +225,7 @@ github.com/cheggaaa/pb/v3/termutil # github.com/clipperhouse/uax29/v2 v2.7.0 ## explicit; go 1.18 github.com/clipperhouse/uax29/v2/graphemes -# github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009041322-881bed56c8e2 +# github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696 ## explicit; go 1.26.0 github.com/cloudfoundry/bosh-agent/v2/agentclient github.com/cloudfoundry/bosh-agent/v2/agentclient/applyspec From e178f41f4f85ff366a32b3b0e4539bba53d6eab2 Mon Sep 17 00:00:00 2001 From: Nishad Mathur Date: Fri, 9 Oct 2026 11:36:59 -0700 Subject: [PATCH 3/6] Separate stemcell feature detection and serialization specs Use Ginkgo table entries so each metadata case reports separately. Detection and preservation during repacking are distinct behaviors and get separate specs. Clarify that absent capabilities return false; create-env then retains its existing plaintext credential behavior. --- stemcell/agent_features_test.go | 33 +++++++++++++++++++-------------- stemcell/stemcell.go | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/stemcell/agent_features_test.go b/stemcell/agent_features_test.go index 7a4d55210..5463cd490 100644 --- a/stemcell/agent_features_test.go +++ b/stemcell/agent_features_test.go @@ -8,23 +8,28 @@ import ( ) var _ = Describe("Agent features", func() { - It("detects supported features and preserves them when repacked", func() { - for _, tc := range []struct { - metadata string - supports bool - }{ - {"name: legacy\napi_version: 3\n", false}, - {"agent_features: [unrelated-feature]\n", false}, - {"agent_features: [http-password-hmac-sha256]\n", true}, - } { + DescribeTable("detects supported features", + func(metadata string, supports bool) { var manifest stemcell.Manifest - Expect(yaml.Unmarshal([]byte(tc.metadata), &manifest)).To(Succeed()) - Expect(manifest.SupportsAgentFeature("http-password-hmac-sha256")).To(Equal(tc.supports)) + Expect(yaml.Unmarshal([]byte(metadata), &manifest)).To(Succeed()) + Expect(manifest.SupportsAgentFeature("http-password-hmac-sha256")).To(Equal(supports)) + }, + Entry("legacy metadata without a feature list", "name: legacy\napi_version: 3\n", false), + Entry("an unrelated feature", "agent_features: [unrelated-feature]\n", false), + Entry("the HTTP password feature", "agent_features: [http-password-hmac-sha256]\n", true), + ) + + DescribeTable("preserves agent features when repacked", + func(features []string) { + manifest := stemcell.Manifest{AgentFeatures: features} packed, err := yaml.Marshal(manifest) Expect(err).NotTo(HaveOccurred()) var unpacked stemcell.Manifest Expect(yaml.Unmarshal(packed, &unpacked)).To(Succeed()) - Expect(unpacked.SupportsAgentFeature("http-password-hmac-sha256")).To(Equal(tc.supports)) - } - }) + Expect(unpacked.AgentFeatures).To(Equal(features)) + }, + Entry("no feature list", []string(nil)), + Entry("an unrelated feature", []string{"unrelated-feature"}), + Entry("the HTTP password feature", []string{"http-password-hmac-sha256"}), + ) }) diff --git a/stemcell/stemcell.go b/stemcell/stemcell.go index ddad5a2a2..b7e30d47c 100644 --- a/stemcell/stemcell.go +++ b/stemcell/stemcell.go @@ -46,7 +46,7 @@ type Manifest struct { } // SupportsAgentFeature reports capabilities advertised for the packaged agent. -// Missing capabilities retain legacy behavior. +// Absent capabilities return false. func (m Manifest) SupportsAgentFeature(feature string) bool { return slices.Contains(m.AgentFeatures, feature) } From f83c43767d64a249cb72ed25e18bc834e47432dd Mon Sep 17 00:00:00 2001 From: Nishad Mathur Date: Wed, 21 Oct 2026 13:52:16 -0700 Subject: [PATCH 4/6] Pin agent password hashing to the merged agent implementation Use the merged bosh-agent#487 commit after rebasing onto current main. Regenerate module checksums and vendor metadata, retaining all other dependency versions from main. A tagged agent release containing #487 is still required before merge. ai-assisted=yes TNZ-129825 --- go.mod | 2 +- go.sum | 4 ++-- vendor/modules.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index a1eac263e..42580451a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( code.cloudfoundry.org/clock v1.93.0 code.cloudfoundry.org/workpool v0.0.0-20250911194158-1489753f182e github.com/cheggaaa/pb/v3 v3.2.1 - github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696 + github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24 github.com/cloudfoundry/bosh-davcli v0.0.518 github.com/cloudfoundry/bosh-gcscli v0.0.426 github.com/cloudfoundry/bosh-s3cli v0.0.447 diff --git a/go.sum b/go.sum index 74cdad87f..5aecd1210 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696 h1:W9frZ4blZXpLVWUgFMC1MkuNh15Up9gMFT3CymM5YQg= -github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696/go.mod h1:9LH2n9Yd/4JmztgmEcE7mpc7ionYY7HDGx/sm8Cs1Aw= +github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24 h1:XHh9C/T6nwUqgfnmAGUKIG0YJP4jcAJEbAeQJIjecw4= +github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24/go.mod h1:suwCWR5eoPDHwyULTcXWmOug8SKLvJJLeT9l49ZkBEs= github.com/cloudfoundry/bosh-davcli v0.0.518 h1:4UndgPJrIzNSws3OXu7bW5kCz2ASOTh8r3ig8lygP5U= github.com/cloudfoundry/bosh-davcli v0.0.518/go.mod h1:DvyFOOWvT/92m8XAMHidtdDjb3PTJ5AalmLAIdeMIzE= github.com/cloudfoundry/bosh-gcscli v0.0.426 h1:yJtJyI55t6p+h+iKyEX9iBqqLe9KQfhlQ4UkydFWM4s= diff --git a/vendor/modules.txt b/vendor/modules.txt index cb38024de..5b89e5432 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -225,7 +225,7 @@ github.com/cheggaaa/pb/v3/termutil # github.com/clipperhouse/uax29/v2 v2.7.0 ## explicit; go 1.18 github.com/clipperhouse/uax29/v2/graphemes -# github.com/cloudfoundry/bosh-agent/v2 v2.900.1-0.20261009183247-50adbab21696 +# github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24 ## explicit; go 1.26.0 github.com/cloudfoundry/bosh-agent/v2/agentclient github.com/cloudfoundry/bosh-agent/v2/agentclient/applyspec From d1810efdf6ec729c1d6f67a9d9526e63ffbe6311 Mon Sep 17 00:00:00 2001 From: Nishad Mathur Date: Wed, 21 Oct 2026 14:44:02 -0700 Subject: [PATCH 5/6] Cover password hashing across all resource pools and mbus URLs Keep hashing every resource pool, including unused pools, so each HTTPS mbus URL receives a password verifier. Document the all-pools policy and expand the create-env regression test to two pools with two URLs each. Check every verifier against its original password while preserving the plaintext mbus used by the CLI client. ai-assisted=yes TNZ-129825 --- cmd/create_env_test.go | 54 ++++++++++++++++++++++++++++++++------ cmd/deployment_preparer.go | 1 + 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/cmd/create_env_test.go b/cmd/create_env_test.go index 35dcff52a..d934561cb 100644 --- a/cmd/create_env_test.go +++ b/cmd/create_env_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "path/filepath" "regexp" @@ -611,19 +612,56 @@ var _ = Describe("CreateEnvCmd", func() { Expect(blobstoreMbus).To(Equal("https://vcap:director-client-pw@127.0.0.1:6868")) }) - It("replaces resource pool env.bosh.mbus.urls https entries with a verifier while preserving client mbus", func() { + It("hashes every HTTPS mbus URL in every resource pool while preserving client mbus", func() { + originalURLs := map[string]biproperty.List{ + "unused-pool": { + "https://vcap:unused-first-password@127.0.0.1:6868", + "https://vcap:unused-second-password@127.0.0.2:6868", + }, + "selected-pool": { + "https://vcap:selected-first-password@127.0.0.3:6868", + "https://vcap:selected-second-password@127.0.0.4:6868", + }, + } + poolTemplate := boshDeploymentManifest.ResourcePools[0] + boshDeploymentManifest.ResourcePools = nil + boshDeploymentManifest.Jobs[0].ResourcePool = "selected-pool" + for _, name := range []string{"unused-pool", "selected-pool"} { + pool := poolTemplate + pool.Name = name + pool.Env = biproperty.Map{ + "bosh": biproperty.Map{ + "mbus": biproperty.Map{ + "urls": append(biproperty.List{}, originalURLs[name]...), + }, + }, + } + boshDeploymentManifest.ResourcePools = append(boshDeploymentManifest.ResourcePools, pool) + } + fakeDeploymentParser.ParseReturns(boshDeploymentManifest, nil) + err := command.Run(fakeStage, defaultCreateEnvOpts) Expect(err).NotTo(HaveOccurred()) Expect(mockDeployer.DeployCallCount()).To(Equal(1)) _, deployedManifest, _, _, _, _, _, _ := mockDeployer.DeployArgsForCall(0) - Expect(deployedManifest.ResourcePools).To(HaveLen(1)) - boshMap := deployedManifest.ResourcePools[0].Env["bosh"].(biproperty.Map) - mbusMap := boshMap["mbus"].(biproperty.Map) - urls := mbusMap["urls"].(biproperty.List) - Expect(urls).To(HaveLen(1)) - Expect(urls[0].(string)).To(HavePrefix("https://vcap:bosh-hmac-sha256$")) - Expect(urls[0].(string)).To(HaveSuffix("@127.0.0.1:6868")) + Expect(deployedManifest.ResourcePools).To(HaveLen(2)) + for _, pool := range deployedManifest.ResourcePools { + mbus := pool.Env["bosh"].(biproperty.Map)["mbus"].(biproperty.Map) + urls := mbus["urls"].(biproperty.List) + Expect(urls).To(HaveLen(2)) + for i, raw := range urls { + hashedURL, err := url.Parse(raw.(string)) + Expect(err).NotTo(HaveOccurred()) + hashedPassword, _ := hashedURL.User.Password() + verifier, err := agentpassword.ParseHashedPassword(hashedPassword) + Expect(err).NotTo(HaveOccurred()) + originalURL, err := url.Parse(originalURLs[pool.Name][i].(string)) + Expect(err).NotTo(HaveOccurred()) + originalPassword, _ := originalURL.User.Password() + Expect(verifier.Matches(originalPassword)).To(BeTrue()) + } + } Expect(mockAgentClientFactory.NewAgentClientCallCount()).To(Equal(1)) _, agentClientMbus, _ := mockAgentClientFactory.NewAgentClientArgsForCall(0) diff --git a/cmd/deployment_preparer.go b/cmd/deployment_preparer.go index 9c53130a1..808112b8b 100644 --- a/cmd/deployment_preparer.go +++ b/cmd/deployment_preparer.go @@ -207,6 +207,7 @@ func (c *DeploymentPreparer) PrepareDeployment(stage biui.Stage, opts Deployment if err != nil { return bosherr.WrapError(err, "Hashing CPI agent HTTP password") } + // Hash all pools, including unused ones, so their HTTPS passwords are not left in plaintext. for i := range deploymentManifest.ResourcePools { err = hashResourcePoolMbusURLs(deploymentManifest.ResourcePools[i].Env) if err != nil { From dcba1349ed964c0462b208b360a570e200381183 Mon Sep 17 00:00:00 2001 From: Nishad Mathur Date: Wed, 21 Oct 2026 15:08:48 -0700 Subject: [PATCH 6/6] Use tagged bosh-agent v2.910.0 for HTTP password hashing Replace the merged-commit pseudo-version with v2.910.0, which points at the same agent commit and satisfies the tagged-release prerequisite. Regenerate go.sum and vendor metadata without changing package sources or unrelated dependencies. ai-assisted=yes TNZ-129825 --- go.mod | 2 +- go.sum | 4 ++-- vendor/modules.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 42580451a..fea68c43c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( code.cloudfoundry.org/clock v1.93.0 code.cloudfoundry.org/workpool v0.0.0-20250911194158-1489753f182e github.com/cheggaaa/pb/v3 v3.2.1 - github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24 + github.com/cloudfoundry/bosh-agent/v2 v2.910.0 github.com/cloudfoundry/bosh-davcli v0.0.518 github.com/cloudfoundry/bosh-gcscli v0.0.426 github.com/cloudfoundry/bosh-s3cli v0.0.447 diff --git a/go.sum b/go.sum index 5aecd1210..b8590a610 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24 h1:XHh9C/T6nwUqgfnmAGUKIG0YJP4jcAJEbAeQJIjecw4= -github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24/go.mod h1:suwCWR5eoPDHwyULTcXWmOug8SKLvJJLeT9l49ZkBEs= +github.com/cloudfoundry/bosh-agent/v2 v2.910.0 h1:TTkfnFascR1smz1RkH2av3ytMoBCvf/+C41FdKyJusg= +github.com/cloudfoundry/bosh-agent/v2 v2.910.0/go.mod h1:suwCWR5eoPDHwyULTcXWmOug8SKLvJJLeT9l49ZkBEs= github.com/cloudfoundry/bosh-davcli v0.0.518 h1:4UndgPJrIzNSws3OXu7bW5kCz2ASOTh8r3ig8lygP5U= github.com/cloudfoundry/bosh-davcli v0.0.518/go.mod h1:DvyFOOWvT/92m8XAMHidtdDjb3PTJ5AalmLAIdeMIzE= github.com/cloudfoundry/bosh-gcscli v0.0.426 h1:yJtJyI55t6p+h+iKyEX9iBqqLe9KQfhlQ4UkydFWM4s= diff --git a/vendor/modules.txt b/vendor/modules.txt index 5b89e5432..ac486cbba 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -225,7 +225,7 @@ github.com/cheggaaa/pb/v3/termutil # github.com/clipperhouse/uax29/v2 v2.7.0 ## explicit; go 1.18 github.com/clipperhouse/uax29/v2/graphemes -# github.com/cloudfoundry/bosh-agent/v2 v2.909.1-0.20261021201500-52d5577b5e24 +# github.com/cloudfoundry/bosh-agent/v2 v2.910.0 ## explicit; go 1.26.0 github.com/cloudfoundry/bosh-agent/v2/agentclient github.com/cloudfoundry/bosh-agent/v2/agentclient/applyspec