CVE-2026-53475
Description
Assisted migration agent hardcodes insecure TLS, allowing MITM attackers to steal vCenter credentials.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Assisted migration agent hardcodes insecure TLS, allowing MITM attackers to steal vCenter credentials.
Vulnerability
The assisted-migration-agent application hardcodes insecure Transport Layer Security (TLS) connections when communicating with vCenter. Specifically, the soap.NewClient(u, true), vmware.NewVsphereClient(ctx, creds, true), and cfg.Insecure = true settings are hardcoded across multiple code paths, with no option to provide a CA bundle or enable verification [2]. This affects the assisted-migration-agent application.
Exploitation
A Man-in-the-Middle (MITM) attacker, potentially through ARP spoofing, rogue DHCP, or a compromised switch, can intercept traffic between the assisted-migration-agent and vCenter. The attacker needs network visibility to intercept these connections to harvest vCenter administrator credentials [2].
Impact
Successful exploitation allows an attacker to harvest vCenter administrator credentials. This can lead to unauthorized access to vCenter, potentially enabling further compromise or data theft. This vulnerability can be chained with other vulnerabilities for cross-tenant credential theft [2].
Mitigation
A pull request has been submitted to replace the hardcoded skipTls=true behavior with user selections for TLS verification [3]. The specific fixed version and release date are not yet disclosed in the available references. There is no mention of workarounds or End-of-Life status.
AI Insight generated on Jun 10, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)
Patches
1b940fec9f503ECOPROJECT-4720 | fix: replace hardcoded "skipTls=true" behaviour
27 files changed · +956 −275
api/v1/extension.go+26 −0 modified@@ -1,12 +1,38 @@ package v1 import ( + "crypto/x509" + "errors" "fmt" "time" "github.com/kubev2v/assisted-migration-agent/internal/models" ) +// CredsFromAPI converts a VcenterCredentials API type to models.Credentials. +func CredsFromAPI(v VcenterCredentials) (models.Credentials, error) { + c := models.Credentials{ + URL: v.Url, + Username: v.Username, + Password: v.Password, + } + if v.Cacert != nil { + if v.SkipTls != nil && *v.SkipTls { + return models.Credentials{}, errors.New("skipTls and cacert are mutually exclusive") + } + pemBytes := []byte(*v.Cacert) + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pemBytes) { + return models.Credentials{}, errors.New("cacert: no valid PEM certificates found") + } + c.CACert = pemBytes + } else { + // no cacert: default to skip-verify for backwards compat unless explicitly false + c.SkipTLS = v.SkipTls == nil || *v.SkipTls + } + return c, nil +} + func (a *AgentStatus) FromModel(m models.AgentStatus) { switch m.Console.Current { case models.ConsoleStatusConnected:
api/v1/openapi.yaml+15 −0 modified@@ -1644,6 +1644,21 @@ components: minLength: 1 x-oapi-codegen-extra-tags: binding: "required,min=1" + cacert: + type: string + description: >- + PEM-encoded CA certificate bundle for verifying the vCenter TLS + certificate. Mutually exclusive with skipTls. + x-oapi-codegen-extra-tags: + binding: "omitempty" + skipTls: + type: boolean + description: >- + When true, TLS certificate verification is skipped. Must not be set + to true alongside cacert — that combination returns 400. When + omitted: if cacert is absent the connection skips TLS verification + (backwards compatibility); if cacert is provided the connection + verifies TLS using that certificate — skipTls need not be sent. Group: type: object
api/v1/spec.gen.go+204 −201 modified@@ -1226,207 +1226,210 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "n+zhnO6tDiZfP379fwEAAP//eLRdIUoIAQA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file
api/v1/types.gen.go+6 −1 modified@@ -690,7 +690,12 @@ type VMNIC struct { // VcenterCredentials defines model for VcenterCredentials. type VcenterCredentials struct { - Password string `binding:"required,min=1" json:"password"` + // Cacert PEM-encoded CA certificate bundle for verifying the vCenter TLS certificate. Mutually exclusive with skipTls. + Cacert *string `binding:"omitempty" json:"cacert,omitempty"` + Password string `binding:"required,min=1" json:"password"` + + // SkipTls When true, TLS certificate verification is skipped. Must not be set to true alongside cacert — that combination returns 400. When omitted: if cacert is absent the connection skips TLS verification (backwards compatibility); if cacert is provided the connection verifies TLS using that certificate — skipTls need not be sent. + SkipTls *bool `json:"skipTls,omitempty"` // Url vCenter URL Url string `binding:"required,url" json:"url"`
api/v2/openapi.yaml+15 −0 modified@@ -718,6 +718,21 @@ components: minLength: 1 x-oapi-codegen-extra-tags: binding: "required,min=1" + cacert: + type: string + description: >- + PEM-encoded CA certificate bundle for verifying the vCenter TLS + certificate. Mutually exclusive with skipTls. + x-oapi-codegen-extra-tags: + binding: "omitempty" + skipTls: + type: boolean + description: >- + When true, TLS certificate verification is skipped. Must not be set + to true alongside cacert — that combination returns 400. When + omitted: if cacert is absent the connection skips TLS verification + (backwards compatibility); if cacert is provided the connection + verifies TLS using that certificate — skipTls need not be sent. # ── VDDK ───────────────────────────────────────────────────────────── VddkProperties:
api/v2/types.gen.go+6 −1 modified@@ -232,7 +232,12 @@ type UpdateGroupRequest struct { // VcenterCredentials defines model for VcenterCredentials. type VcenterCredentials struct { - Password string `binding:"required,min=1" json:"password"` + // Cacert PEM-encoded CA certificate bundle for verifying the vCenter TLS certificate. Mutually exclusive with skipTls. + Cacert *string `binding:"omitempty" json:"cacert,omitempty"` + Password string `binding:"required,min=1" json:"password"` + + // SkipTls When true, TLS certificate verification is skipped. Must not be set to true alongside cacert — that combination returns 400. When omitted: if cacert is absent the connection skips TLS verification (backwards compatibility); if cacert is provided the connection verifies TLS using that certificate — skipTls need not be sent. + SkipTls *bool `json:"skipTls,omitempty"` // Url vCenter URL Url string `binding:"required,url" json:"url"`
internal/handlers/v1/collector.go+8 −5 modified@@ -6,7 +6,6 @@ import ( "github.com/gin-gonic/gin" v1 "github.com/kubev2v/assisted-migration-agent/api/v1" - "github.com/kubev2v/assisted-migration-agent/internal/models" srvErrors "github.com/kubev2v/assisted-migration-agent/pkg/errors" ) @@ -26,10 +25,14 @@ func (h *Handler) StartCollector(c *gin.Context) { return } - creds := models.Credentials{ - URL: req.Url, - Username: req.Username, - Password: req.Password, + creds, err := v1.CredsFromAPI(req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := creds.Validate(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return } if err := h.collectorSrv.Start(c.Request.Context(), creds); err != nil {
internal/handlers/v1/collector_test.go+75 −0 modified@@ -297,6 +297,81 @@ var _ = Describe("Collector Handlers", func() { Expect(json.Unmarshal(w.Body.Bytes(), &response)).To(Succeed()) Expect(response["error"]).To(Equal("unexpected error")) }) + + It("should wire cacert from the request body", func() { + caCert := generateCACertPEM() + body, _ := json.Marshal(map[string]interface{}{ + "url": "https://vcenter.example.com/sdk", + "username": "admin", + "password": "secret", + "cacert": caCert, + }) + req := httptest.NewRequest(http.MethodPost, "/collector", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusAccepted)) + Expect(mockCollector.LastCreds.CACert).To(Equal([]byte(caCert))) + Expect(mockCollector.LastCreds.SkipTLS).To(BeFalse()) + }) + + It("should wire skipTls=true from the request body", func() { + body, _ := json.Marshal(map[string]interface{}{ + "url": "https://vcenter.example.com/sdk", + "username": "admin", + "password": "secret", + "skipTls": true, + }) + req := httptest.NewRequest(http.MethodPost, "/collector", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusAccepted)) + Expect(mockCollector.LastCreds.SkipTLS).To(BeTrue()) + Expect(mockCollector.LastCreds.CACert).To(BeNil()) + }) + + It("should return 400 when both cacert and skipTls=true are provided", func() { + body, _ := json.Marshal(map[string]interface{}{ + "url": "https://vcenter.example.com/sdk", + "username": "admin", + "password": "secret", + "cacert": generateCACertPEM(), + "skipTls": true, + }) + req := httptest.NewRequest(http.MethodPost, "/collector", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusBadRequest)) + Expect(mockCollector.StartCallCount).To(Equal(0)) + var resp map[string]string + Expect(json.Unmarshal(w.Body.Bytes(), &resp)).To(Succeed()) + Expect(resp["error"]).To(ContainSubstring("mutually exclusive")) + }) + + It("should default SkipTLS to true when neither field is provided (backwards compat)", func() { + body, _ := json.Marshal(map[string]interface{}{ + "url": "https://vcenter.example.com/sdk", + "username": "admin", + "password": "secret", + }) + req := httptest.NewRequest(http.MethodPost, "/collector", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusAccepted)) + Expect(mockCollector.LastCreds.SkipTLS).To(BeTrue()) + Expect(mockCollector.LastCreds.CACert).To(BeNil()) + }) }) Describe("StopCollector", func() {
internal/handlers/v1/forecaster.go+17 −8 modified@@ -52,11 +52,16 @@ func (h *Handler) StartForecaster(c *gin.Context) { Pairs: pairs, } if req.Credentials != nil { - forecastReq.Credentials = models.Credentials{ - URL: req.Credentials.Url, - Username: req.Credentials.Username, - Password: req.Credentials.Password, + creds, err := v1api.CredsFromAPI(*req.Credentials) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := creds.Validate(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return } + forecastReq.Credentials = creds } if req.DiskSizeGb != nil { forecastReq.DiskSizeGB = *req.DiskSizeGb @@ -156,10 +161,14 @@ func (h *Handler) PutForecasterCredentials(c *gin.Context) { return } - creds := models.Credentials{ - URL: req.Url, - Username: req.Username, - Password: req.Password, + creds, err := v1api.CredsFromAPI(req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := creds.Validate(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return } if err := h.forecasterSrv.VerifyCredentials(c.Request.Context(), creds); err != nil {
internal/handlers/v1/handlers_suite_test.go+27 −0 modified@@ -2,8 +2,16 @@ package v1_test import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "io" + "math/big" "testing" + "time" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" @@ -15,6 +23,20 @@ import ( "github.com/kubev2v/assisted-migration-agent/internal/services" ) +// generateCACertPEM returns a PEM-encoded self-signed CA certificate for use in handler tests. +func generateCACertPEM() string { + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + } + der, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) +} + func TestHandlers(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Handlers Suite") @@ -32,6 +54,7 @@ type MockCollectorService struct { StartError error StartCallCount int StopCallCount int + LastCreds models.Credentials } func (m *MockCollectorService) GetStatus() models.CollectorStatus { @@ -40,6 +63,7 @@ func (m *MockCollectorService) GetStatus() models.CollectorStatus { func (m *MockCollectorService) Start(ctx context.Context, creds models.Credentials) error { m.StartCallCount++ + m.LastCreds = creds return m.StartError } @@ -154,6 +178,7 @@ type MockInspectorService struct { CancelVmsInspectionCallCount int StopCallCount int IsBusyResult bool + LastCreds models.Credentials } func (m *MockInspectorService) IsBusy() bool { @@ -162,11 +187,13 @@ func (m *MockInspectorService) IsBusy() bool { func (m *MockInspectorService) Start(ctx context.Context, creds models.Credentials, vmIDs []string) error { m.StartCallCount++ + m.LastCreds = creds return m.StartError } func (m *MockInspectorService) Credentials(ctx context.Context, creds models.Credentials) error { _, _ = ctx, creds + m.LastCreds = creds return m.CredentialsError }
internal/handlers/v1/inspector.go+16 −9 modified@@ -8,7 +8,6 @@ import ( "github.com/gin-gonic/gin" v1 "github.com/kubev2v/assisted-migration-agent/api/v1" - "github.com/kubev2v/assisted-migration-agent/internal/models" srvErrors "github.com/kubev2v/assisted-migration-agent/pkg/errors" ) @@ -39,10 +38,14 @@ func (h *Handler) StartInspection(c *gin.Context) { return } - creds := models.Credentials{ - URL: req.Credentials.Url, - Username: req.Credentials.Username, - Password: req.Credentials.Password, + creds, err := v1.CredsFromAPI(req.Credentials) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := creds.Validate(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return } if err := h.inspectorSrv.Start(c.Request.Context(), creds, req.VmIds); err != nil { @@ -107,10 +110,14 @@ func (h *Handler) ValidateInspectorCredentials(c *gin.Context) { return } - creds := models.Credentials{ - URL: req.Url, - Username: req.Username, - Password: req.Password, + creds, err := v1.CredsFromAPI(req) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := creds.Validate(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return } if err := h.inspectorSrv.Credentials(c.Request.Context(), creds); err != nil {
internal/handlers/v1/rightsizing.go+14 −8 modified@@ -63,15 +63,21 @@ func (h *Handler) TriggerRightsizingCollection(c *gin.Context) { return } + creds, err := v1.CredsFromAPI(req.Credentials) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if err := creds.Validate(); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + params := models.RightsizingParams{ - Credentials: models.Credentials{ - URL: req.Credentials.Url, - Username: req.Credentials.Username, - Password: req.Credentials.Password, - }, - LookbackH: defaultInt(req.LookbackHours, 720), - IntervalID: defaultInt(req.IntervalId, 7200), - BatchSize: defaultInt(req.BatchSize, 64), + Credentials: creds, + LookbackH: defaultInt(req.LookbackHours, 720), + IntervalID: defaultInt(req.IntervalId, 7200), + BatchSize: defaultInt(req.BatchSize, 64), } if req.NameFilter != nil { params.NameFilter = *req.NameFilter
internal/handlers/v1/rightsizing_test.go+45 −0 modified@@ -252,6 +252,51 @@ var _ = Describe("Rightsizing Handlers", func() { Expect(w.Code).To(Equal(http.StatusInternalServerError)) }) + + It("should wire cacert into RightsizingParams", func() { + caCert := generateCACertPEM() + mockSvc.TriggerResult = &models.RightsizingReportSummary{ID: "r1"} + body, _ := json.Marshal(map[string]interface{}{ + "credentials": map[string]interface{}{ + "url": "https://vcenter.example.com/sdk", + "username": "admin", + "password": "secret", + "cacert": caCert, + }, + }) + req := httptest.NewRequest(http.MethodPost, "/rightsizing", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusAccepted)) + Expect(mockSvc.LastTriggerParams.CACert).To(Equal([]byte(caCert))) + Expect(mockSvc.LastTriggerParams.SkipTLS).To(BeFalse()) + }) + + It("should return 400 when both cacert and skipTls=true are provided", func() { + body, _ := json.Marshal(map[string]interface{}{ + "credentials": map[string]interface{}{ + "url": "https://vcenter.example.com/sdk", + "username": "admin", + "password": "secret", + "cacert": generateCACertPEM(), + "skipTls": true, + }, + }) + req := httptest.NewRequest(http.MethodPost, "/rightsizing", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusBadRequest)) + Expect(mockSvc.TriggerCallCount).To(Equal(0)) + var resp map[string]string + Expect(json.Unmarshal(w.Body.Bytes(), &resp)).To(Succeed()) + Expect(resp["error"]).To(ContainSubstring("mutually exclusive")) + }) }) Describe("ListRightsizingReportClusters", func() {
internal/models/vcenter.go+21 −4 modified@@ -1,8 +1,25 @@ package models -// Credentials holds vCenter connection credentials. +import "errors" + +// Credentials holds vCenter connection credentials and TLS options. type Credentials struct { - URL string - Username string - Password string + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password"` + // CACert is a PEM-encoded CA certificate bundle used to verify the vCenter + // TLS certificate. Mutually exclusive with SkipTLS. + CACert []byte `json:"cacert,omitempty"` + // SkipTLS disables TLS certificate verification. Mutually exclusive with CACert. + // Default false — verified TLS is the secure default. + SkipTLS bool `json:"skipTls"` +} + +// Validate returns an error if the TLS fields are in a conflicting state. +// Call this at every service entry point before making any vCenter connection. +func (c Credentials) Validate() error { + if c.SkipTLS && len(c.CACert) > 0 { + return errors.New("skipTls and cacert are mutually exclusive: set skipTls=true for unverified TLS or provide a cacert for verified TLS, not both") + } + return nil }
internal/models/vcenter_test.go+76 −0 added@@ -0,0 +1,76 @@ +package models_test + +import ( + "strings" + "testing" + + "github.com/kubev2v/assisted-migration-agent/internal/models" +) + +func TestCredentials_Validate(t *testing.T) { + validPEM := []byte("-----BEGIN CERTIFICATE-----\nMIIBxxx\n-----END CERTIFICATE-----\n") + + tests := []struct { + name string + creds models.Credentials + wantErr bool + errMustNot string // substring that must NOT appear in the error (cert safety) + }{ + { + name: "no TLS fields: valid", + creds: models.Credentials{URL: "https://vc/sdk", Username: "u", Password: "p"}, + wantErr: false, + }, + { + name: "only SkipTLS true: valid", + creds: models.Credentials{SkipTLS: true}, + wantErr: false, + }, + { + name: "only CACert provided: valid", + creds: models.Credentials{CACert: validPEM}, + wantErr: false, + }, + { + name: "both SkipTLS and CACert: invalid", + creds: models.Credentials{SkipTLS: true, CACert: validPEM}, + wantErr: true, + errMustNot: "BEGIN CERTIFICATE", // cert content must not leak into error + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.creds.Validate() + if tc.wantErr && err == nil { + t.Fatal("expected error, got nil") + } + if !tc.wantErr && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err != nil && tc.errMustNot != "" { + if strings.Contains(err.Error(), tc.errMustNot) { + t.Errorf("error message must not contain %q, got: %v", tc.errMustNot, err) + } + } + }) + } +} + +func TestCredentials_Validate_ErrorDoesNotLeakCertContent(t *testing.T) { + // Use a cert with a distinctive marker string unlikely to appear in error prose. + sentinel := "SENTINELMARKER99" + fakePEM := []byte("-----BEGIN CERTIFICATE-----\n" + sentinel + "\n-----END CERTIFICATE-----\n") + + creds := models.Credentials{ + SkipTLS: true, + CACert: fakePEM, + } + err := creds.Validate() + if err == nil { + t.Fatal("expected validation error") + } + if strings.Contains(err.Error(), sentinel) { + t.Errorf("error message must not contain cert content; got: %v", err) + } +}
internal/services/forecaster.go+13 −3 modified@@ -14,7 +14,6 @@ import ( "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/session" "github.com/vmware/govmomi/vim25" - "github.com/vmware/govmomi/vim25/soap" "go.uber.org/zap" @@ -111,7 +110,7 @@ func (f *ForecasterService) Start(ctx context.Context, req models.ForecastReques zap.S().Infow("starting forecaster", "pairs", len(req.Pairs), "diskSizeGB", req.DiskSizeGB, "iterations", req.Iterations, "concurrency", req.Concurrency) - vClient, err := vmware.NewVsphereClient(ctx, cred.URL, cred.Username, cred.Password, true) + vClient, err := vmware.NewVsphereClient(ctx, &cred) if err != nil { zap.S().Named("forecaster_service").Errorw("failed to connect to vSphere", "error", err) return srvErrors.NewVCenterError(err) @@ -171,6 +170,10 @@ func (f *ForecasterService) VerifyCredentials(ctx context.Context, credentials m } credentials.URL = u + if err := credentials.Validate(); err != nil { + return err + } + if err := f.verifyCredentialsAndPrivileges(ctx, &credentials, models.ForecasterRequiredPrivileges); err != nil { return err } @@ -191,7 +194,11 @@ func (f *ForecasterService) verifyCredentialsAndPrivileges(ctx context.Context, verifyCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - vimClient, err := vim25.NewClient(verifyCtx, soap.NewClient(u, true)) + soapClient, err := vmware.NewSoapClient(u, creds.SkipTLS, creds.CACert) + if err != nil { + return err + } + vimClient, err := vim25.NewClient(verifyCtx, soapClient) if err != nil { return err } @@ -246,6 +253,9 @@ func (f *ForecasterService) resolveCredentials(ctx context.Context, creds models creds.URL = u if creds.URL != "" { + if err := creds.Validate(); err != nil { + return models.Credentials{}, err + } if err := f.verifyCredentialsAndPrivileges(ctx, &creds, models.ForecasterRequiredPrivileges); err != nil { return models.Credentials{}, err }
internal/services/inspector.go+5 −1 modified@@ -94,7 +94,11 @@ func (i *InspectorService) Start(ctx context.Context, creds models.Credentials, } creds.URL = url - vClient, err := vmware.NewVsphereClient(ctx, creds.URL, creds.Username, creds.Password, true) + if err := creds.Validate(); err != nil { + return err + } + + vClient, err := vmware.NewVsphereClient(ctx, &creds) if err != nil { zap.S().Named("inspector_service").Errorw("failed to connect to vSphere", "error", err) return srvErrors.NewVCenterError(err)
internal/services/inspector_test.go+1 −0 modified@@ -25,6 +25,7 @@ func getVCenterCredentials() models.Credentials { URL: "https://localhost:8989/sdk", Username: "user", Password: "pass", + SkipTLS: true, } }
internal/services/rightsizing.go+5 −7 modified@@ -249,13 +249,8 @@ func buildVSphereConfig(params models.RightsizingParams, lookback time.Duration) VCenterURL: params.URL, Username: params.Username, Password: params.Password, - // TLS verification is skipped because on-prem vCenter deployments almost universally use - // self-signed certificates, and even CA-signed certs frequently fail verification due to - // SAN mismatches (the cert is issued for the FQDN but the agent may connect via IP or alias). - // So, in order to provide a valid cert, the vCenter admins will need to hav a cert for each of the possible IPs/aliases the agent might use. - // The risk is accepted: the agent runs inside the customer's own network, so the MITM - // threat model is low. - Insecure: true, + Insecure: params.SkipTLS, + CACert: params.CACert, NameFilter: params.NameFilter, ClusterID: params.ClusterID, Lookback: lookback, @@ -268,6 +263,9 @@ func buildVSphereConfig(params models.RightsizingParams, lookback time.Duration) // The report shell is persisted in DuckDB synchronously before returning (202 Accepted). // Callers poll GET /rightsizing/{id} to observe metrics being populated. func (s *RightsizingService) TriggerCollection(ctx context.Context, params models.RightsizingParams) (*models.RightsizingReportSummary, error) { + if err := params.Validate(); err != nil { + return nil, err + } applyCollectionDefaults(¶ms) s.mu.Lock()
pkg/collector/vsphere.go+13 −5 modified@@ -107,16 +107,24 @@ func createProvider(creds *models.Credentials) *api.Provider { // createSecret creates a Kubernetes Secret with vCenter credentials. func createSecret(creds *models.Credentials) *core.Secret { + skipVerify := "false" + if creds.SkipTLS { + skipVerify = "true" + } + data := map[string][]byte{ + "user": []byte(creds.Username), + "password": []byte(creds.Password), + "insecureSkipVerify": []byte(skipVerify), + } + if len(creds.CACert) > 0 { + data["ca.crt"] = creds.CACert + } return &core.Secret{ ObjectMeta: meta.ObjectMeta{ Name: "vsphere-secret", Namespace: "default", }, - Data: map[string][]byte{ - "user": []byte(creds.Username), - "password": []byte(creds.Password), - "insecureSkipVerify": []byte("true"), - }, + Data: data, } }
pkg/collector/vsphere_test.go+73 −0 added@@ -0,0 +1,73 @@ +package collector + +import ( + "testing" + + "github.com/kubev2v/assisted-migration-agent/internal/models" +) + +func TestCreateSecret_DefaultsToVerifiedTLS(t *testing.T) { + creds := &models.Credentials{ + Username: "admin", + Password: "secret", + } + s := createSecret(creds) + + got := string(s.Data["insecureSkipVerify"]) + if got != "false" { + t.Errorf("insecureSkipVerify: want %q, got %q", "false", got) + } + if _, ok := s.Data["ca.crt"]; ok { + t.Error("ca.crt must not be present when CACert is empty") + } +} + +func TestCreateSecret_SkipTLS(t *testing.T) { + creds := &models.Credentials{ + Username: "admin", + Password: "secret", + SkipTLS: true, + } + s := createSecret(creds) + + got := string(s.Data["insecureSkipVerify"]) + if got != "true" { + t.Errorf("insecureSkipVerify: want %q, got %q", "true", got) + } +} + +func TestCreateSecret_WithCACert(t *testing.T) { + caPEM := []byte("-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----\n") + creds := &models.Credentials{ + Username: "admin", + Password: "secret", + CACert: caPEM, + } + s := createSecret(creds) + + got := string(s.Data["insecureSkipVerify"]) + if got != "false" { + t.Errorf("insecureSkipVerify: want %q, got %q", "false", got) + } + + gotCACert := s.Data["ca.crt"] + if string(gotCACert) != string(caPEM) { + t.Errorf("ca.crt: want %q, got %q", caPEM, gotCACert) + } +} + +func TestCreateSecret_CACertNotInInsecureKey(t *testing.T) { + caPEM := []byte("-----BEGIN CERTIFICATE-----\nMIIBxxx\n-----END CERTIFICATE-----\n") + creds := &models.Credentials{CACert: caPEM} + s := createSecret(creds) + + // The cert must only be in "ca.crt", not leaked into other fields. + for k, v := range s.Data { + if k == "ca.crt" { + continue + } + if string(v) == string(caPEM) { + t.Errorf("cert content leaked into secret key %q", k) + } + } +}
pkg/rightsizing/vsphere.go+25 −1 modified@@ -2,6 +2,8 @@ package rightsizing import ( "context" + "crypto/x509" + "errors" "fmt" "maps" "net/url" @@ -42,6 +44,7 @@ type Config struct { Username string Password string Insecure bool + CACert []byte NameFilter string ClusterID string Lookback time.Duration @@ -63,6 +66,23 @@ type VMReport struct { Warnings []string } +// buildSoapClient creates a SOAP client with TLS configured from the given +// parameters. CACert content is never included in error messages or logs. +// Mirrors vmware.NewSoapClient; kept separate to avoid cross-package coupling. +func buildSoapClient(u *url.URL, insecure bool, caCert []byte) (*soap.Client, error) { + sc := soap.NewClient(u, insecure) + if len(caCert) == 0 { + return sc, nil + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(caCert) { + return nil, errors.New("invalid CA bundle: no valid PEM certificates found") + } + // soap.NewClient always initialises TLSClientConfig. + sc.DefaultTransport().TLSClientConfig.RootCAs = pool + return sc, nil +} + // Connect authenticates a govmomi client. Never logs the password. func Connect(ctx context.Context, cfg Config) (*govmomi.Client, error) { u, err := soap.ParseURL(cfg.VCenterURL) @@ -71,7 +91,11 @@ func Connect(ctx context.Context, cfg Config) (*govmomi.Client, error) { } u.User = url.UserPassword(cfg.Username, cfg.Password) - soapClient := soap.NewClient(u, cfg.Insecure) + soapClient, err := buildSoapClient(u, cfg.Insecure, cfg.CACert) + if err != nil { + return nil, fmt.Errorf("failed to configure TLS: %w", err) + } + vimClient, err := vim25.NewClient(ctx, soapClient) if err != nil { return nil, fmt.Errorf("failed to create vim25 client: %w", err)
pkg/rightsizing/vsphere_test.go+77 −0 added@@ -0,0 +1,77 @@ +package rightsizing + +import ( + "net/url" + "strings" + "testing" + + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" +) + +func generateTestCert(t *testing.T) []byte { + t.Helper() + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + } + der, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) +} + +func TestBuildSoapClient_NoCACert(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + sc, err := buildSoapClient(u, false, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if sc == nil { + t.Fatal("expected non-nil soap client") + } +} + +func TestBuildSoapClient_ValidCACert(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + caPEM := generateTestCert(t) + + sc, err := buildSoapClient(u, false, caPEM) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + transport := sc.DefaultTransport() + if transport.TLSClientConfig == nil || transport.TLSClientConfig.RootCAs == nil { + t.Fatal("expected RootCAs to be populated") + } +} + +func TestBuildSoapClient_InvalidCACert(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + _, err := buildSoapClient(u, false, []byte("not a cert")) + if err == nil { + t.Fatal("expected error for invalid PEM") + } +} + +func TestBuildSoapClient_ErrorDoesNotLeakCertContent(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + sentinel := "SENTINELMARKER99" + badPEM := []byte("-----BEGIN CERTIFICATE-----\n" + sentinel + "\n-----END CERTIFICATE-----\n") + + _, err := buildSoapClient(u, false, badPEM) + if err == nil { + t.Fatal("expected error for invalid PEM") + } + if strings.Contains(err.Error(), sentinel) { + t.Errorf("error must not contain cert content; got: %v", err) + } +}
pkg/vmware/auth.go+10 −3 modified@@ -9,7 +9,6 @@ import ( "github.com/vmware/govmomi" "github.com/vmware/govmomi/session" - "github.com/vmware/govmomi/vim25/soap" "go.uber.org/zap" "github.com/kubev2v/assisted-migration-agent/internal/models" @@ -65,17 +64,25 @@ func (m *VMManager) ValidatePrivileges(ctx context.Context, moid string, require } func VerifyCredentials(ctx context.Context, creds *models.Credentials, resourceName string) error { + if err := creds.Validate(); err != nil { + return err + } + u, err := url.ParseRequestURI(creds.URL) if err != nil { return err } - u.User = url.UserPassword(creds.Username, creds.Password) verifyCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - vimClient, err := vim25.NewClient(verifyCtx, soap.NewClient(u, true)) + soapClient, err := NewSoapClient(u, creds.SkipTLS, creds.CACert) + if err != nil { + return err + } + + vimClient, err := vim25.NewClient(verifyCtx, soapClient) if err != nil { return err }
pkg/vmware/client.go+15 −18 modified@@ -9,30 +9,27 @@ import ( "github.com/vmware/govmomi/session" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/soap" + + "github.com/kubev2v/assisted-migration-agent/internal/models" ) -// NewVsphereClient creates and authenticates a new vSphere client connection to a vCenter server. -// -// Parameters: -// - ctx: the context for the API request. -// - vcenterUrl: the URL of the vCenter server (e.g., "https://vcenter.example.com/sdk"). -// - username: the username for authentication. -// - password: the password for authentication. -// - insecure: if true, skips TLS certificate verification (use only for testing). -// -// Returns an error if: -// - the vCenter URL cannot be parsed, -// - the vim25 client creation fails, -// - or authentication to vCenter fails. -func NewVsphereClient(ctx context.Context, vcenterUrl, username, password string, insecure bool) (*govmomi.Client, error) { - u, err := soap.ParseURL(vcenterUrl) +// NewVsphereClient creates and authenticates a govmomi client using the provided +// credentials. TLS behaviour is governed by creds.SkipTLS and creds.CACert. +// Callers must invoke creds.Validate() before calling this function. +func NewVsphereClient(ctx context.Context, creds *models.Credentials) (*govmomi.Client, error) { + if creds == nil { + return nil, fmt.Errorf("credentials must not be nil") + } + u, err := soap.ParseURL(creds.URL) if err != nil { return nil, fmt.Errorf("failed to parse vCenter URL: %w", err) } + u.User = url.UserPassword(creds.Username, creds.Password) - u.User = url.UserPassword(username, password) - - soapClient := soap.NewClient(u, insecure) + soapClient, err := NewSoapClient(u, creds.SkipTLS, creds.CACert) + if err != nil { + return nil, fmt.Errorf("failed to configure TLS: %w", err) + } vimClient, err := vim25.NewClient(ctx, soapClient) if err != nil {
pkg/vmware/tls.go+30 −0 added@@ -0,0 +1,30 @@ +package vmware + +import ( + "crypto/x509" + "errors" + "net/url" + + "github.com/vmware/govmomi/vim25/soap" +) + +// NewSoapClient creates a govmomi SOAP client with TLS configured from the +// provided parameters. Pass insecure=true only when the caller has explicitly +// opted out of certificate verification (SkipTLS=true on Credentials). +// If caCert is non-empty it must be a PEM-encoded certificate bundle; it is +// loaded into the client's root CA pool. caCert content is never logged. +func NewSoapClient(u *url.URL, insecure bool, caCert []byte) (*soap.Client, error) { + sc := soap.NewClient(u, insecure) + if len(caCert) == 0 { + return sc, nil + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(caCert) { + return nil, errors.New("invalid CA bundle: no valid PEM certificates found") + } + // soap.NewClient always initialises TLSClientConfig; SetRootCAs() is not used + // because it loads from file paths, not in-memory PEM bytes. + transport := sc.DefaultTransport() + transport.TLSClientConfig.RootCAs = pool + return sc, nil +}
pkg/vmware/tls_test.go+118 −0 added@@ -0,0 +1,118 @@ +package vmware + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net/url" + "strings" + "testing" + "time" +) + +// generateSelfSignedCert returns a PEM-encoded self-signed certificate for testing. +func generateSelfSignedCert(t *testing.T) ([]byte, *x509.Certificate) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("generate key: %v", err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatalf("create cert: %v", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("parse cert: %v", err) + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + return pemBytes, cert +} + +func TestNewSoapClient_NoCACert(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + sc, err := NewSoapClient(u, false, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if sc == nil { + t.Fatal("expected non-nil soap client") + } +} + +func TestNewSoapClient_ValidCACert(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + caPEM, caCert := generateSelfSignedCert(t) + + sc, err := NewSoapClient(u, false, caPEM) + if err != nil { + t.Fatalf("unexpected error with valid CA: %v", err) + } + if sc == nil { + t.Fatal("expected non-nil soap client") + } + + transport := sc.DefaultTransport() + if transport.TLSClientConfig == nil || transport.TLSClientConfig.RootCAs == nil { + t.Fatal("expected RootCAs to be set on the TLS config") + } + + // Verify the specific cert was loaded, not just any non-nil pool. + expected := x509.NewCertPool() + expected.AddCert(caCert) + if !transport.TLSClientConfig.RootCAs.Equal(expected) { + t.Fatal("RootCAs pool does not contain the expected CA certificate") + } +} + +func TestNewSoapClient_InvalidCACert(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + badPEM := []byte("this is not a valid PEM certificate") + + _, err := NewSoapClient(u, false, badPEM) + if err == nil { + t.Fatal("expected error for invalid PEM, got nil") + } + + // Ensure the bad PEM content is not echoed in the error message. + if strings.Contains(err.Error(), string(badPEM)) { + t.Errorf("error message must not contain the raw cert bytes") + } +} + +func TestNewSoapClient_SkipTLS(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + sc, err := NewSoapClient(u, true, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + transport := sc.DefaultTransport() + if transport.TLSClientConfig == nil || !transport.TLSClientConfig.InsecureSkipVerify { + t.Fatal("expected InsecureSkipVerify=true when skipTLS is true") + } +} + +func TestNewSoapClient_ErrorDoesNotLeakCertContent(t *testing.T) { + u, _ := url.Parse("https://vcenter.example.com/sdk") + sentinel := "SENTINELMARKER99" + badPEM := []byte("-----BEGIN CERTIFICATE-----\n" + sentinel + "\n-----END CERTIFICATE-----\n") + + _, err := NewSoapClient(u, false, badPEM) + if err == nil { + t.Fatal("expected error for invalid PEM") + } + if strings.Contains(err.Error(), sentinel) { + t.Errorf("error must not contain cert content; got: %v", err) + } +}
Vulnerability mechanics
Root cause
"The application hardcodes insecure TLS verification settings for vCenter connections."
Attack vector
A Man-in-the-Middle (MITM) attacker can intercept network traffic between the application and vCenter. This is possible because the application uses hardcoded insecure TLS settings, effectively disabling certificate verification. By intercepting this traffic, the attacker can harvest vCenter administrator credentials.
Affected code
The vulnerability exists in multiple code paths within the assisted-migration-agent that connect to vCenter. Specifically, the hardcoded `insecure=true` or `skipTls=true` behavior is present in `pkg/vmware/auth.go`, `inspector.go`, `forecaster.go`, and `rightsizing.go` [ref_id=1]. The patch modifies `api/v1/spec.gen.go` to remove this hardcoded behavior.
What the fix does
The patch modifies the application to no longer hardcode TLS verification as disabled. Instead, it introduces user-selectable options for TLS verification, allowing for secure connections. This change removes the vulnerability by ensuring that TLS verification is enabled by default, preventing MITM attackers from intercepting credentials.
Preconditions
- networkThe attacker must be in a network position to perform a Man-in-the-Middle attack (e.g., ARP spoofing, rogue DHCP, compromised switch).
- authThe attacker does not require any specific privileges on the vCenter server.
Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.