From 9ae4373f890a8c4b5049feba06c8fda75e896d30 Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Mon, 27 Apr 2026 14:31:58 -0700 Subject: [PATCH] Don't ignore GitOps secrets on free tier (#44148) **Related issue:** Resolves #44118 # Details On free tier, ignore exceptions and always apply enroll secrets when present. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. n/a, unreleased ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [X] QA'd all new/changed functionality manually @AndreyKizimenko QA'd manually For unreleased bug fixes in a release candidate, one of: - [X] Confirmed that the fix is not expected to adversely impact load test results ## Summary by CodeRabbit * **Bug Fixes** * Fixed GitOps to correctly apply enrollment secrets and labels on free tier licenses, even when exception flags are configured. * **Tests** * Added tests validating that GitOps properly applies secrets and labels for free tier customers. --- cmd/fleetctl/fleetctl/gitops_test.go | 115 +++++++++++++++++++++++++++ server/service/client.go | 4 +- 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/cmd/fleetctl/fleetctl/gitops_test.go b/cmd/fleetctl/fleetctl/gitops_test.go index 9511117454..d2859ff861 100644 --- a/cmd/fleetctl/fleetctl/gitops_test.go +++ b/cmd/fleetctl/fleetctl/gitops_test.go @@ -716,6 +716,121 @@ software: } } +// TestGitOpsSecretsAppliedOnFreeTierDespiteException verifies that on free tier, secrets +// in a GitOps file are applied even when the server has GitOpsExceptions.Secrets=true +// (the default for new installs). Free-tier users can't toggle the exception off, so +// the exception flag must not cause secrets to be silently dropped. +func TestGitOpsSecretsAppliedOnFreeTierDespiteException(t *testing.T) { + // Cannot run t.Parallel() because it sets environment variables + license := &fleet.LicenseInfo{Tier: fleet.TierFree} + _, ds := testing_utils.RunServerWithMockedDS( + t, &service.TestServerOpts{ + License: license, + KeyValueStore: testing_utils.NewMemKeyValueStore(), + }, + ) + setupEmptyGitOpsMocks(ds) + + // Server returns Exceptions.Secrets=true (the default for new installs). + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ + GitOpsConfig: fleet.GitOpsConfig{ + Exceptions: fleet.GitOpsExceptions{Secrets: true}, + }, + }, nil + } + ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error { return nil } + + var appliedSecrets []*fleet.EnrollSecret + ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error { + appliedSecrets = secrets + return nil + } + ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) { + return true, nil + } + + globalFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = globalFile.WriteString(` +org_settings: + server_settings: + server_url: https://fleet.example.com + org_info: + org_name: Test + secrets: + - secret: free-tier-secret-value +controls: +policies: +agent_options: +`) + require.NoError(t, err) + + _, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name()}) + require.NoError(t, err) + + require.Len(t, appliedSecrets, 1, "secrets in YAML should be applied on free tier even when Exceptions.Secrets=true") + assert.Equal(t, "free-tier-secret-value", appliedSecrets[0].Secret) +} + +// TestGitOpsLabelsAppliedOnFreeTierDespiteException verifies that on free tier, labels +// in a GitOps file are applied even when the server has GitOpsExceptions.Labels=true +// (the value set by the exceptions migration for existing instances). Free-tier users +// can't toggle the exception off, so applying labels via GitOps must still work. +func TestGitOpsLabelsAppliedOnFreeTierDespiteException(t *testing.T) { + // Cannot run t.Parallel() because it sets environment variables + license := &fleet.LicenseInfo{Tier: fleet.TierFree} + _, ds := testing_utils.RunServerWithMockedDS( + t, &service.TestServerOpts{ + License: license, + KeyValueStore: testing_utils.NewMemKeyValueStore(), + }, + ) + setupEmptyGitOpsMocks(ds) + + // Server returns Exceptions.Labels=true (the value set by the migration for existing instances). + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ + GitOpsConfig: fleet.GitOpsConfig{ + Exceptions: fleet.GitOpsExceptions{Labels: true}, + }, + }, nil + } + ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error { return nil } + + var appliedLabelSpecs []*fleet.LabelSpec + ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorID *uint) error { + appliedLabelSpecs = specs + return nil + } + ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) { + return nil, nil + } + + globalFile, err := os.CreateTemp(t.TempDir(), "*.yml") + require.NoError(t, err) + _, err = globalFile.WriteString(` +org_settings: + server_settings: + server_url: https://fleet.example.com + org_info: + org_name: Test +labels: + - name: free-tier-label + query: SELECT 1 +controls: +policies: +agent_options: +`) + require.NoError(t, err) + + _, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name()}) + require.NoError(t, err) + + require.Len(t, appliedLabelSpecs, 1, "labels in YAML should be applied on free tier even when Exceptions.Labels=true") + assert.Equal(t, "free-tier-label", appliedLabelSpecs[0].Name) +} + // TestGitOpsExceptionsPreserveOmittedKeys verifies that when exceptions are ON, // omitting the excepted keys from YAML preserves existing data. func TestGitOpsExceptionsPreserveOmittedKeys(t *testing.T) { diff --git a/server/service/client.go b/server/service/client.go index f092f95d76..5c085ac327 100644 --- a/server/service/client.go +++ b/server/service/client.go @@ -1940,7 +1940,7 @@ func (c *Client) DoGitOps( if !exceptions.Secrets && !incoming.SecretsPresent { incoming.OrgSettings["secrets"] = make([]*fleet.EnrollSecret, 0) } - if orgSecrets, ok := incoming.OrgSettings["secrets"]; ok && !exceptions.Secrets { + if orgSecrets, ok := incoming.OrgSettings["secrets"]; ok && (!exceptions.Secrets || !appConfig.License.IsPremium()) { group.EnrollSecret = &fleet.EnrollSecretSpec{Secrets: orgSecrets.([]*fleet.EnrollSecret)} } delete(incoming.OrgSettings, "secrets") @@ -2184,7 +2184,7 @@ func (c *Client) DoGitOps( } incoming.TeamSettings["secrets"] = make([]*fleet.EnrollSecret, 0) } - if teamSecrets, ok := incoming.TeamSettings["secrets"]; ok && !exceptions.Secrets { + if teamSecrets, ok := incoming.TeamSettings["secrets"]; ok && (!exceptions.Secrets || !appConfig.License.IsPremium()) { team["secrets"] = teamSecrets }