diff --git a/changes/6365-global-enroll-secret b/changes/6365-global-enroll-secret new file mode 100644 index 0000000000..13789c5c94 --- /dev/null +++ b/changes/6365-global-enroll-secret @@ -0,0 +1 @@ +* Added a Fleet server config to provide a default global enroll secret. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index f23456a487..1fe73c01d3 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -263,6 +263,37 @@ the way that the Fleet server works. } } + if config.Packaging.GlobalEnrollSecret != "" { + secrets, err := ds.GetEnrollSecrets(cmd.Context(), nil) + if err != nil { + initFatal(err, "loading enroll secrets") + } + + var globalEnrollSecret string + for _, secret := range secrets { + if secret.TeamID == nil { + globalEnrollSecret = secret.Secret + break + } + } + + if globalEnrollSecret != "" { + if globalEnrollSecret != config.Packaging.GlobalEnrollSecret { + fmt.Printf("################################################################################\n" + + "# WARNING:\n" + + "# You have provided a global enroll secret config, but there's\n" + + "# already one set up for your application.\n" + + "#\n" + + "# This is generally an error and the provided value will be\n" + + "# ignored, if you really need to configure an enroll secret please\n" + + "# remove the global enroll secret from the database manually.\n" + + "################################################################################\n") + } + } else { + ds.ApplyEnrollSecrets(cmd.Context(), nil, []*fleet.EnrollSecret{{Secret: config.Packaging.GlobalEnrollSecret}}) + } + } + redisPool, err := redis.NewPool(redis.PoolConfig{ Server: config.Redis.Address, Password: config.Redis.Password, diff --git a/docs/Deploying/Configuration.md b/docs/Deploying/Configuration.md index d96371c125..da318fd31e 100644 --- a/docs/Deploying/Configuration.md +++ b/docs/Deploying/Configuration.md @@ -2522,3 +2522,34 @@ If not set then the Prometheus `/metrics` endpoint is disabled. basic_auth: password: "bar" ``` + +#### Packaging + +Configurations used to control how Fleet interacts with the (coming soon) +packaging server. These features are currently only intended to be used within +Fleet Sandbox, but this is subject to change. + +##### packaging.global_enroll_secret + +Enroll secret to use for adding hosts to the global scope. If this value is +set, the server won't allow changes to the enroll secret via the config +endpoints. + +This value should be treated as a secret, we recommend using a +cryptographically secure pseudo random string. For example, using `openssl`: + +``` +openssl rand -base64 24 +``` + +This config only takes effect if you don't have a global enroll secret already +stored in your database. + +- Default value: `""` +- Environment variable: `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` +- Config file format: + + ```yaml + packaging: + global_enroll_secret: "xyz" + ``` diff --git a/server/config/config.go b/server/config/config.go index 1973e822cf..94abe68431 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -306,6 +306,13 @@ type HTTPBasicAuthConfig struct { Password string `json:"password" yaml:"password"` } +// PackagingConfig holds configuration to build and retrieve Fleet packages +type PackagingConfig struct { + // GlobalEnrollSecret is the enroll secret that will be used to enroll + // hosts in the global scope + GlobalEnrollSecret string `yaml:"global_enroll_secret"` +} + // FleetConfig stores the application configuration. Each subcategory is // broken up into it's own struct, defined above. When editing any of these // structs, Manager.addConfigs and Manager.LoadConfig should be @@ -333,6 +340,7 @@ type FleetConfig struct { Sentry SentryConfig GeoIP GeoIPConfig Prometheus PrometheusConfig + Packaging PackagingConfig } type TLS struct { @@ -643,6 +651,9 @@ func (man Manager) addConfigs() { // Prometheus man.addConfigString("prometheus.basic_auth.username", "", "Prometheus username for HTTP Basic Auth") man.addConfigString("prometheus.basic_auth.password", "", "Prometheus password for HTTP Basic Auth") + + // Packaging config + man.addConfigString("packaging.global_enroll_secret", "", "Enroll secret to be used for the global domain (instead of randomly generating one)") } // LoadConfig will load the config variables into a fully initialized @@ -836,6 +847,9 @@ func (man Manager) LoadConfig() FleetConfig { Password: man.getConfigString("prometheus.basic_auth.password"), }, }, + Packaging: PackagingConfig{ + GlobalEnrollSecret: man.getConfigString("packaging.global_enroll_secret"), + }, } // ensure immediately that the async config is valid for all known tasks diff --git a/server/service/appconfig.go b/server/service/appconfig.go index 3a69d23abd..cc182c77d6 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -370,6 +370,10 @@ func (svc *Service) ApplyEnrollSecretSpec(ctx context.Context, spec *fleet.Enrol } } + if svc.config.Packaging.GlobalEnrollSecret != "" { + return ctxerr.New(ctx, "enroll secret cannot be changed when fleet_packaging.global_enroll_secret is set") + } + return svc.ds.ApplyEnrollSecrets(ctx, nil, spec.Secrets) } diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go index 4f328d228e..aa0f80f04d 100644 --- a/server/service/appconfig_test.go +++ b/server/service/appconfig_test.go @@ -11,10 +11,12 @@ import ( "net/url" "testing" + "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" + "github.com/fleetdm/fleet/v4/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -180,6 +182,28 @@ func TestEnrollSecretAuth(t *testing.T) { } } +func TestApplyEnrollSecretWithGlobalEnrollConfig(t *testing.T) { + ds := new(mock.Store) + ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error { + return nil + } + + cfg := config.TestConfig() + svc := newTestServiceWithConfig(t, ds, cfg, nil, nil) + ctx := test.UserContext(test.UserAdmin) + err := svc.ApplyEnrollSecretSpec(ctx, &fleet.EnrollSecretSpec{Secrets: []*fleet.EnrollSecret{{Secret: "ABC"}}}) + require.True(t, ds.ApplyEnrollSecretsFuncInvoked) + require.NoError(t, err) + + // try to change the enroll secret with the config set + ds.ApplyEnrollSecretsFuncInvoked = false + cfg.Packaging.GlobalEnrollSecret = "xyz" + svc = newTestServiceWithConfig(t, ds, cfg, nil, nil) + err = svc.ApplyEnrollSecretSpec(ctx, &fleet.EnrollSecretSpec{Secrets: []*fleet.EnrollSecret{{Secret: "DEF"}}}) + require.Error(t, err) + require.False(t, ds.ApplyEnrollSecretsFuncInvoked) +} + func TestCertificateChain(t *testing.T) { server, teardown := setupCertificateChain(t) defer teardown() diff --git a/server/service/service_appconfig.go b/server/service/service_appconfig.go index 402dc007e1..fbb8dab2a0 100644 --- a/server/service/service_appconfig.go +++ b/server/service/service_appconfig.go @@ -42,9 +42,12 @@ func (svc *Service) NewAppConfig(ctx context.Context, p fleet.AppConfig) (*fleet } // Set up a default enroll secret - secret, err := server.GenerateRandomText(fleet.EnrollSecretDefaultLength) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "generate enroll secret string") + secret := svc.config.Packaging.GlobalEnrollSecret + if secret == "" { + secret, err = server.GenerateRandomText(fleet.EnrollSecretDefaultLength) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "generate enroll secret string") + } } secrets := []*fleet.EnrollSecret{ { diff --git a/server/service/service_appconfig_test.go b/server/service/service_appconfig_test.go index 16c725d3bc..752e4961aa 100644 --- a/server/service/service_appconfig_test.go +++ b/server/service/service_appconfig_test.go @@ -123,6 +123,30 @@ func TestEmptyEnrollSecret(t *testing.T) { require.NoError(t, err) } +func TestNewAppConfigWithGlobalEnrollConfig(t *testing.T) { + ds := new(mock.Store) + cfg := config.TestConfig() + cfg.Packaging.GlobalEnrollSecret = "xyz" + svc := newTestServiceWithConfig(t, ds, cfg, nil, nil) + + ds.NewAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) (*fleet.AppConfig, error) { + return config, nil + } + + var gotSecrets []*fleet.EnrollSecret + ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error { + gotSecrets = secrets + return nil + } + + ctx := test.UserContext(test.UserAdmin) + _, err := svc.NewAppConfig(ctx, fleet.AppConfig{ServerSettings: fleet.ServerSettings{ServerURL: "https://acme.co"}}) + require.NoError(t, err) + require.NotNil(t, gotSecrets) + require.Len(t, gotSecrets, 1) + require.Equal(t, gotSecrets[0].Secret, "xyz") +} + func TestService_LoggingConfig(t *testing.T) { logFile := "/dev/null" if runtime.GOOS == "windows" {