From 861d0ee12247a0c87ae8c62016e0fe92e6adfc46 Mon Sep 17 00:00:00 2001 From: Magnus Jensen Date: Mon, 2 Mar 2026 22:25:46 +0200 Subject: [PATCH] don't allow manual_agent_install without bootstrap package (#40812) **Related issue:** Resolves #38432 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually --- .../38432-dont-allow-mai-without-bootstrap | 1 + ..._enterprise_integration_deprecated_test.go | 25 ++++++++-- .../gitops_enterprise_integration_test.go | 47 ++++++++++++++---- .../gitops/testdata/signed.pkg | Bin 0 -> 4412 bytes ee/server/service/teams.go | 3 ++ server/fleet/app.go | 12 +++++ server/fleet/errors.go | 4 ++ server/service/appconfig.go | 10 ++++ server/service/appconfig_test.go | 15 ++++++ 9 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 changes/38432-dont-allow-mai-without-bootstrap create mode 100644 cmd/fleetctl/integrationtest/gitops/testdata/signed.pkg diff --git a/changes/38432-dont-allow-mai-without-bootstrap b/changes/38432-dont-allow-mai-without-bootstrap new file mode 100644 index 0000000000..35000edff1 --- /dev/null +++ b/changes/38432-dont-allow-mai-without-bootstrap @@ -0,0 +1 @@ +* Fixed an issue where it was possible to configure manual_agent_install without specifiying a bootstrap package via the API and GitOps. \ No newline at end of file diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go index c6cf80616b..db515c4b1d 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_deprecated_test.go @@ -693,9 +693,21 @@ func (s *enterpriseIntegrationGitopsTestSuite) TestMacOSSetupDeprecated() { ctx := context.Background() + originalAppConfig, err := s.DS.AppConfig(ctx) + require.NoError(t, err) + t.Cleanup(func() { + err := s.DS.SaveAppConfig(ctx, originalAppConfig) + require.NoError(t, err) + }) + user := s.createGitOpsUser(t) fleetctlConfig := s.createFleetctlConfig(t, user) + bootstrapServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "testdata/signed.pkg") + })) + defer bootstrapServer.Close() + const ( globalConfig = ` agent_options: @@ -713,6 +725,7 @@ queries: agent_options: controls: macos_setup: + bootstrap_package: %s manual_agent_install: %t org_settings: server_settings: @@ -727,6 +740,7 @@ queries: noTeamConfig = `name: No team controls: macos_setup: + bootstrap_package: %s manual_agent_install: true policies: software: @@ -735,6 +749,7 @@ software: teamConfig = ` controls: macos_setup: + bootstrap_package: %s manual_agent_install: %t software: queries: @@ -755,7 +770,7 @@ team_settings: noTeamFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = noTeamFile.WriteString(noTeamConfig) + _, err = noTeamFile.WriteString(fmt.Sprintf(noTeamConfig, bootstrapServer.URL)) require.NoError(t, err) err = noTeamFile.Close() require.NoError(t, err) @@ -766,26 +781,26 @@ team_settings: teamName := uuid.NewString() teamFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = teamFile.WriteString(fmt.Sprintf(teamConfig, true, teamName)) + _, err = teamFile.WriteString(fmt.Sprintf(teamConfig, bootstrapServer.URL, true, teamName)) require.NoError(t, err) err = teamFile.Close() require.NoError(t, err) teamFileClear, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = teamFileClear.WriteString(fmt.Sprintf(teamConfig, false, teamName)) + _, err = teamFileClear.WriteString(fmt.Sprintf(teamConfig, bootstrapServer.URL, false, teamName)) require.NoError(t, err) err = teamFileClear.Close() require.NoError(t, err) globalFileOnlySet, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = globalFileOnlySet.WriteString(fmt.Sprintf(globalConfigOnly, true)) + _, err = globalFileOnlySet.WriteString(fmt.Sprintf(globalConfigOnly, bootstrapServer.URL, true)) require.NoError(t, err) err = globalFileOnlySet.Close() require.NoError(t, err) globalFileOnlyClear, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = globalFileOnlyClear.WriteString(fmt.Sprintf(globalConfigOnly, false)) + _, err = globalFileOnlyClear.WriteString(fmt.Sprintf(globalConfigOnly, bootstrapServer.URL, false)) require.NoError(t, err) err = globalFileOnlyClear.Close() require.NoError(t, err) diff --git a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go index 4d225018f3..cc80030b9e 100644 --- a/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go +++ b/cmd/fleetctl/integrationtest/gitops/gitops_enterprise_integration_test.go @@ -344,7 +344,7 @@ settings: // Check that all the teams exist teamsJSON := fleetctl.RunAppForTest(t, []string{"get", "teams", "--config", fleetctlConfig.Name(), "--json"}) - assert.Equal(t, 12, strings.Count(teamsJSON, "fleet_id")) + assert.Equal(t, 6, strings.Count(teamsJSON, "fleet_id")) // Real run with all the files, and delete other teams args = []string{"gitops", "--config", fleetctlConfig.Name(), "--delete-other-teams", "-f", globalFile} @@ -355,7 +355,7 @@ settings: // Check that only the right teams exist teamsJSON = fleetctl.RunAppForTest(t, []string{"get", "teams", "--config", fleetctlConfig.Name(), "--json"}) - assert.Equal(t, 10, strings.Count(teamsJSON, "fleet_id")) + assert.Equal(t, 4, strings.Count(teamsJSON, "fleet_id")) assert.NotContains(t, teamsJSON, deletedTeamName) // Real run with one file at a time @@ -1472,9 +1472,21 @@ func (s *enterpriseIntegrationGitopsTestSuite) TestMacOSSetup() { t := s.T() ctx := context.Background() + originalAppConfig, err := s.DS.AppConfig(ctx) + require.NoError(t, err) + t.Cleanup(func() { + err := s.DS.SaveAppConfig(ctx, originalAppConfig) + require.NoError(t, err) + }) + user := s.createGitOpsUser(t) fleetctlConfig := s.createFleetctlConfig(t, user) + bootstrapServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "testdata/signed.pkg") + })) + defer bootstrapServer.Close() + const ( globalConfig = ` agent_options: @@ -1492,6 +1504,7 @@ reports: agent_options: controls: macos_setup: + bootstrap_package: %s manual_agent_install: %t org_settings: server_settings: @@ -1506,6 +1519,7 @@ reports: noTeamConfig = `name: No team controls: macos_setup: + bootstrap_package: %s manual_agent_install: true policies: software: @@ -1514,6 +1528,7 @@ software: teamConfig = ` controls: macos_setup: + bootstrap_package: %s manual_agent_install: %t software: reports: @@ -1534,7 +1549,7 @@ settings: noTeamFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = noTeamFile.WriteString(noTeamConfig) + _, err = noTeamFile.WriteString(fmt.Sprintf(noTeamConfig, bootstrapServer.URL)) require.NoError(t, err) err = noTeamFile.Close() require.NoError(t, err) @@ -1545,26 +1560,26 @@ settings: teamName := uuid.NewString() teamFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = teamFile.WriteString(fmt.Sprintf(teamConfig, true, teamName)) + _, err = teamFile.WriteString(fmt.Sprintf(teamConfig, bootstrapServer.URL, true, teamName)) require.NoError(t, err) err = teamFile.Close() require.NoError(t, err) teamFileClear, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = teamFileClear.WriteString(fmt.Sprintf(teamConfig, false, teamName)) + _, err = teamFileClear.WriteString(fmt.Sprintf(teamConfig, bootstrapServer.URL, false, teamName)) require.NoError(t, err) err = teamFileClear.Close() require.NoError(t, err) globalFileOnlySet, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = globalFileOnlySet.WriteString(fmt.Sprintf(globalConfigOnly, true)) + _, err = globalFileOnlySet.WriteString(fmt.Sprintf(globalConfigOnly, bootstrapServer.URL, true)) require.NoError(t, err) err = globalFileOnlySet.Close() require.NoError(t, err) globalFileOnlyClear, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = globalFileOnlyClear.WriteString(fmt.Sprintf(globalConfigOnly, false)) + _, err = globalFileOnlyClear.WriteString(fmt.Sprintf(globalConfigOnly, bootstrapServer.URL, false)) require.NoError(t, err) err = globalFileOnlyClear.Close() require.NoError(t, err) @@ -3363,12 +3378,25 @@ settings: func (s *enterpriseIntegrationGitopsTestSuite) TestDisallowSoftwareSetupExperience() { t := s.T() + ctx := context.Background() + + originalAppConfig, err := s.DS.AppConfig(ctx) + require.NoError(t, err) + t.Cleanup(func() { + err = s.DS.SaveAppConfig(ctx, originalAppConfig) + require.NoError(t, err) + }) user := s.createGitOpsUser(t) fleetctlConfig := s.createFleetctlConfig(t, user) test.CreateInsertGlobalVPPToken(t, s.DS) teamName := uuid.NewString() + bootstrapServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "testdata/signed.pkg") + })) + defer bootstrapServer.Close() + // The global template includes VPP token assignment to the team // The location "Jungle" comes from test.CreateInsertGlobalVPPToken globalTemplate := `agent_options: @@ -3392,6 +3420,7 @@ queries: testVPP := ` controls: macos_setup: + bootstrap_package: %s manual_agent_install: true software: app_store_apps: @@ -3416,6 +3445,7 @@ team_settings: testPackages := ` controls: macos_setup: + bootstrap_package: %s manual_agent_install: true software: app_store_apps: @@ -3481,7 +3511,7 @@ team_settings: require.NoError(t, err) teamFile, err := os.CreateTemp(t.TempDir(), "*.yml") require.NoError(t, err) - _, err = fmt.Fprintf(teamFile, tc.teamTemplate, tc.teamName, tc.teamSettings) + _, err = fmt.Fprintf(teamFile, tc.teamTemplate, bootstrapServer.URL, tc.teamName, tc.teamSettings) require.NoError(t, err) err = teamFile.Close() require.NoError(t, err) @@ -3507,7 +3537,6 @@ team_settings: } else { require.NoError(t, err) } - }) } } diff --git a/cmd/fleetctl/integrationtest/gitops/testdata/signed.pkg b/cmd/fleetctl/integrationtest/gitops/testdata/signed.pkg new file mode 100644 index 0000000000000000000000000000000000000000..80797dbf577708960c1fa598cb01f92d65a2ad6e GIT binary patch literal 4412 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 C%#9EL literal 0 HcmV?d00001 diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index c31d20cee6..1bfba762c1 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -1115,6 +1115,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec, if err := spec.MDM.WindowsUpdates.Validate(); err != nil { return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("windows_updates", err.Error())) } + if err := spec.MDM.MacOSSetup.Validate(); err != nil { + return nil, ctxerr.Wrap(ctx, err) // Error message coming from validate is already invalid argument + } if create { diff --git a/server/fleet/app.go b/server/fleet/app.go index ba80c815a4..8e7919981f 100644 --- a/server/fleet/app.go +++ b/server/fleet/app.go @@ -541,6 +541,18 @@ type MacOSSetup struct { RequireAllSoftware bool `json:"require_all_software_macos"` } +func (mos *MacOSSetup) Validate() error { + if mos == nil { + return nil + } + + if mos.ManualAgentInstall.Valid && mos.ManualAgentInstall.Value && (!mos.BootstrapPackage.Valid || mos.BootstrapPackage.Value == "") { + return NewInvalidArgumentError("macos_setup.manual_agent_install", `Couldn't enable manual_agent_install. To use this option, first specify a bootstrap package.`) + } + + return nil +} + func (mos *MacOSSetup) SetDefaultsIfNeeded() { if mos == nil { return diff --git a/server/fleet/errors.go b/server/fleet/errors.go index b0acce17d8..26d3669990 100644 --- a/server/fleet/errors.go +++ b/server/fleet/errors.go @@ -107,6 +107,10 @@ func (e *InvalidArgumentError) Append(name, reason string) { }) } +func (e *InvalidArgumentError) AppendInvalidArgument(invalidArg InvalidArgument) { + e.Errors = append(e.Errors, invalidArg) +} + func (e *InvalidArgumentError) Appendf(name, reasonFmt string, args ...interface{}) { e.Append(name, fmt.Sprintf(reasonFmt, args...)) } diff --git a/server/service/appconfig.go b/server/service/appconfig.go index c9cd767ede..ccd1965d3d 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -1472,6 +1472,16 @@ func (svc *Service) validateMDM( invalid.Append("ipados_updates", err.Error()) } + if err := mdm.MacOSSetup.Validate(); err != nil { + var invalidArgErr *fleet.InvalidArgumentError + if errors.As(err, &invalidArgErr) { + firstInvalidErr := invalidArgErr.Errors[0] // We only expect one invalid argument error entry from the validate + invalid.AppendInvalidArgument(firstInvalidErr) + } else { + invalid.Append("macos_setup", err.Error()) + } + } + // WindowsUpdates updatingWindowsUpdates := !mdm.WindowsUpdates.Equal(oldMdm.WindowsUpdates) if updatingWindowsUpdates { diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index a6369a9d81..acff759963 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -1293,6 +1293,21 @@ func TestMDMConfig(t *testing.T) { }, expectedError: fleet.CantDisableDiskEncryptionIfPINRequiredErrMsg, }, + { + name: "manual_agent_install enabled without bootstrap package", + licenseTier: "premium", + oldMDM: fleet.MDM{ + MacOSSetup: fleet.MacOSSetup{ + ManualAgentInstall: optjson.SetBool(false), + }, + }, + newMDM: fleet.MDM{ + MacOSSetup: fleet.MacOSSetup{ + ManualAgentInstall: optjson.SetBool(true), + }, + }, + expectedError: "macos_setup.manual_agent_install Couldn't enable manual_agent_install. To use this option, first specify a bootstrap package.", + }, { name: "try to disable End User Authentication with Lock End User Info enabled", licenseTier: "premium",