diff --git a/changes/13825-remotely-configure-fleetd-update-channels b/changes/13825-remotely-configure-fleetd-update-channels new file mode 100644 index 000000000..528de00d6 --- /dev/null +++ b/changes/13825-remotely-configure-fleetd-update-channels @@ -0,0 +1 @@ +* Remotely configure fleetd update channels in agent options (Fleet Premium only, and requires fleetd >= 1.20.0). diff --git a/orbit/changes/13825-remotely-configure-fleetd-update-channels b/orbit/changes/13825-remotely-configure-fleetd-update-channels new file mode 100644 index 000000000..d53aadb5e --- /dev/null +++ b/orbit/changes/13825-remotely-configure-fleetd-update-channels @@ -0,0 +1,2 @@ +* Allow configuring TUF channels of `orbit`, `osqueryd` and `desktop` from Fleet agent settings. +* Add `uptime` column to `orbit_info` table. diff --git a/orbit/cmd/orbit/orbit.go b/orbit/cmd/orbit/orbit.go index accbd7eb9..e8417beff 100644 --- a/orbit/cmd/orbit/orbit.go +++ b/orbit/cmd/orbit/orbit.go @@ -213,6 +213,7 @@ func main() { fmt.Println("orbit " + build.Version) return nil } + startTime := time.Now() var logFile io.Writer if logf := c.String("log-file"); logf != "" { @@ -247,6 +248,12 @@ func main() { zerolog.SetGlobalLevel(zerolog.DebugLevel) } + // Override flags with values retrieved from Fleet. + fallbackServerOverridesCfg := setServerOverrides(c) + if !fallbackServerOverridesCfg.empty() { + log.Debug().Msgf("fallback settings: %+v", fallbackServerOverridesCfg) + } + if c.Bool("insecure") && c.String("fleet-certificate") != "" { return errors.New("insecure and fleet-certificate may not be specified together") } @@ -433,25 +440,11 @@ func main() { // restart. This was changed to have control over // how/when we want to retry to download the packages. err = retrypkg.Do(func() error { - osquerydLocalTarget, err := updater.Get("osqueryd") + var err error + osquerydPath, desktopPath, err = getFleetdComponentPaths(c, updater, fallbackServerOverridesCfg) if err != nil { - log.Info().Err(err).Msg("get osqueryd target failed") - return fmt.Errorf("get osqueryd target: %w", err) + return err } - osquerydPath = osquerydLocalTarget.ExecPath - if c.Bool("fleet-desktop") { - fleetDesktopLocalTarget, err := updater.Get("desktop") - if err != nil { - log.Info().Err(err).Msg("get desktop target failed") - return fmt.Errorf("get desktop target: %w", err) - } - if runtime.GOOS == "darwin" { - desktopPath = fleetDesktopLocalTarget.DirPath - } else { - desktopPath = fleetDesktopLocalTarget.ExecPath - } - } - return nil }, // retry every 5 minutes to not flood the logs, @@ -724,6 +717,32 @@ func main() { } g.Add(flagRunner.Execute, flagRunner.Interrupt) + if !c.Bool("disable-updates") { + const serverOverridesInterval = 30 * time.Second + serverOverridesRunner := newServerOverridesRunner( + configFetcher, + c.String("root-dir"), + serverOverridesInterval, + fallbackServerOverridesConfig{ + OsquerydPath: osquerydPath, + DesktopPath: desktopPath, + }, + c.Bool("fleet-desktop"), + ) + // Perform initial run to update overrides as soon as possible. + didUpdate, err := serverOverridesRunner.run() + if err != nil { + // Just log, OK to continue, since serverOverridesRunner will retry + // in serverOverridesRunner.Execute. + log.Debug().Err(err).Msg("initial flags update failed") + } + if didUpdate { + log.Info().Msg("exiting due to early update of server overrides") + return nil + } + g.Add(serverOverridesRunner.Execute, serverOverridesRunner.Interrupt) + } + // only setup extensions autoupdate if we have enabled updates // for extensions autoupdate, we can only proceed after orbit is enrolled in fleet // and all relevant things for it (like certs, enroll secrets, tls proxy, etc) is configured @@ -943,6 +962,7 @@ func main() { c.String("osqueryd-channel"), c.String("desktop-channel"), trw, + startTime, )), ) @@ -1022,6 +1042,79 @@ func main() { } } +// setServerOverrides overrides specific variables in c with values fetched from Fleet. +func setServerOverrides(c *cli.Context) fallbackServerOverridesConfig { + overrideCfg, err := loadServerOverrides(c.String("root-dir")) + if err != nil { + log.Error().Err(err).Msg("failed to load server overrides") + return fallbackServerOverridesConfig{} + } + if overrideCfg.OrbitChannel != "" { + if err := c.Set("orbit-channel", overrideCfg.OrbitChannel); err != nil { + log.Error().Err(err).Str("component", "orbit").Msg("failed to set server overrides") + } + } + if overrideCfg.OsquerydChannel != "" { + if err := c.Set("osqueryd-channel", overrideCfg.OsquerydChannel); err != nil { + log.Error().Err(err).Str("component", "osqueryd").Msg("failed to set server overrides") + } + } + if overrideCfg.DesktopChannel != "" { + if err := c.Set("desktop-channel", overrideCfg.DesktopChannel); err != nil { + log.Error().Err(err).Str("component", "desktop").Msg("failed to set server overrides") + } + } + + return overrideCfg.fallbackServerOverridesConfig +} + +// getFleetdComponentPaths returns the paths of the fleetd components. +// If the path to the component cannot be fetched using the updater (e.g. channel doesn't exist yet) +// then it will use the fallbackCfg's paths (if set). +func getFleetdComponentPaths( + c *cli.Context, + updater *update.Updater, + fallbackCfg fallbackServerOverridesConfig, +) (osquerydPath string, desktopPath string, err error) { + if err := updater.UpdateMetadata(); err != nil { + log.Error().Err(err).Msg("update metadata before getting components") + } + + // osqueryd + osquerydLocalTarget, err := updater.Get("osqueryd") + if err != nil { + if fallbackCfg.OsquerydPath == "" { + log.Info().Err(err).Msg("get osqueryd target failed") + return "", "", fmt.Errorf("get osqueryd target: %w", err) + } + log.Info().Err(err).Msgf("get osqueryd target failed, fallback to using %s", fallbackCfg.OsquerydPath) + osquerydPath = fallbackCfg.OsquerydPath + } else { + osquerydPath = osquerydLocalTarget.ExecPath + } + + // Fleet Desktop + if c.Bool("fleet-desktop") { + fleetDesktopLocalTarget, err := updater.Get("desktop") + if err != nil { + if fallbackCfg.DesktopPath == "" { + log.Info().Err(err).Msg("get desktop target failed") + return "", "", fmt.Errorf("get desktop target: %w", err) + } + log.Info().Err(err).Msgf("get desktop target failed, fallback to using %s", fallbackCfg.DesktopPath) + desktopPath = fallbackCfg.DesktopPath + } else { + if runtime.GOOS == "darwin" { + desktopPath = fleetDesktopLocalTarget.DirPath + } else { + desktopPath = fleetDesktopLocalTarget.ExecPath + } + } + } + + return osquerydPath, desktopPath, nil +} + func registerExtensionRunner(g *run.Group, extSockPath string, opts ...table.Opt) { ext := table.NewRunner(extSockPath, opts...) g.Add(ext.Execute, ext.Interrupt) @@ -1378,3 +1471,190 @@ func writeSecret(enrollSecret string, orbitRoot string) error { return nil } + +// serverOverridesRunner is a oklog.Group runner that polls for configuration overrides from Fleet. +type serverOverridesRunner struct { + configFetcher update.OrbitConfigFetcher + interval time.Duration + rootDir string + fallbackCfg fallbackServerOverridesConfig + desktopEnabled bool + cancel chan struct{} +} + +// newServerOverridesRunner creates a runner for updating server overrides configuration with values fetched from Fleet. +func newServerOverridesRunner( + configFetcher update.OrbitConfigFetcher, + rootDir string, + interval time.Duration, + fallbackCfg fallbackServerOverridesConfig, + desktopEnabled bool, +) *serverOverridesRunner { + return &serverOverridesRunner{ + configFetcher: configFetcher, + interval: interval, + rootDir: rootDir, + fallbackCfg: fallbackCfg, + desktopEnabled: desktopEnabled, + cancel: make(chan struct{}), + } +} + +// Execute starts the loop that polls for server overrides configuration from Fleet. +func (r *serverOverridesRunner) Execute() error { + log.Debug().Msg("starting server overrides runner") + + ticker := time.NewTicker(r.interval) + defer ticker.Stop() + + for { + select { + case <-r.cancel: + return nil + case <-ticker.C: + log.Debug().Msg("calling server overrides run") + didUpdate, err := r.run() + if err != nil { + logging.LogErrIfEnvNotSet(constant.SilenceEnrollLogErrorEnvVar, err, "server overrides run failed") + } + if didUpdate { + log.Info().Msg("server overrides updated, exiting") + return nil + } + } + } +} + +// Interrupt is the oklog/run interrupt method that stops orbit when interrupt is received +func (r *serverOverridesRunner) Interrupt(err error) { + close(r.cancel) + log.Error().Err(err).Msg("interrupt for server overrides runner") +} + +func (r *serverOverridesRunner) run() (bool, error) { + overrideCfg, err := loadServerOverrides(r.rootDir) + if err != nil { + return false, err + } + + orbitCfg, err := r.configFetcher.GetConfig() + if err != nil { + return false, err + } + if orbitCfg.UpdateChannels == nil { + // Server is not setting or doesn't know of + // this feature (old server version), so nothing to do. + return false, nil + } + + if cfgsDiffer(overrideCfg, orbitCfg, r.desktopEnabled) { + if err := r.updateServerOverrides(orbitCfg); err != nil { + return false, err + } + return true, nil + } + + return false, nil +} + +// cfgsDiffer returns whether the local server overrides differ from the fetched remotely. +func cfgsDiffer(overrideCfg *serverOverridesConfig, orbitCfg *fleet.OrbitConfig, desktopEnabled bool) bool { + localUpdateChannelsCfg := &fleet.OrbitUpdateChannels{ + Orbit: overrideCfg.OrbitChannel, + Osqueryd: overrideCfg.OsquerydChannel, + Desktop: overrideCfg.DesktopChannel, + } + remoteUpdateChannelsCfg := orbitCfg.UpdateChannels + + setStableAsDefault := func(cfg *fleet.OrbitUpdateChannels) { + if cfg.Orbit == "" { + cfg.Orbit = "stable" + } + if cfg.Osqueryd == "" { + cfg.Osqueryd = "stable" + } + if cfg.Desktop == "" { + cfg.Desktop = "stable" + } + } + setStableAsDefault(localUpdateChannelsCfg) + setStableAsDefault(remoteUpdateChannelsCfg) + + local := *localUpdateChannelsCfg + remote := *remoteUpdateChannelsCfg + + if !desktopEnabled { + local.Desktop = "" + remote.Desktop = "" + } + + return local != remote +} + +// serverOverridesConfig holds the currently supported fields that can be +// overriden by server configuration. +type serverOverridesConfig struct { + // OrbitChannel defines the override for the orbit's channel. + OrbitChannel string `json:"orbit-channel"` + // OsquerydChannel defines the override for the osqueryd's channel. + OsquerydChannel string `json:"osqueryd-channel"` + // DesktopChannel defines the override for the Fleet Desktop's channel. + DesktopChannel string `json:"desktop-channel"` + + fallbackServerOverridesConfig +} + +// fallbackServerOverridesConfig contains fallback configuration in case the server +// settings are invalid (e.g. invalid update channels that don't exist). +// Whenever the user sets an invalid channel, then the fallback paths are used to get +// fleetd up and running with the last known good configuration. +// +// NOTE: We don't need orbit's path because the `orbit` component is a special case that uses +// a symlink to define the last known valid version. +type fallbackServerOverridesConfig struct { + // OsquerydPath contains the path of the osqueryd executable last known to be valid. + OsquerydPath string `json:"fallback-osqueryd-path"` + // DesktopPath contains the path of the Fleet Desktop executable last known to be valid. + DesktopPath string `json:"fallback-desktop-path"` +} + +func (f fallbackServerOverridesConfig) empty() bool { + return f.OsquerydPath == "" && f.DesktopPath == "" +} + +// updateServerOverrides updates the server override local file with the configuration fetched from Fleet. +func (r *serverOverridesRunner) updateServerOverrides(remoteCfg *fleet.OrbitConfig) error { + overrideCfg := serverOverridesConfig{ + OrbitChannel: remoteCfg.UpdateChannels.Orbit, + OsquerydChannel: remoteCfg.UpdateChannels.Osqueryd, + DesktopChannel: remoteCfg.UpdateChannels.Desktop, + fallbackServerOverridesConfig: r.fallbackCfg, + } + + data, err := json.MarshalIndent(overrideCfg, "", " ") + if err != nil { + return fmt.Errorf("marshal override config: %w", err) + } + serverOverridesPath := filepath.Join(r.rootDir, constant.ServerOverridesFileName) + if err := os.WriteFile(serverOverridesPath, data, constant.DefaultFileMode); err != nil { + return fmt.Errorf("write override config: %w", err) + } + return nil +} + +// loadServerOverrides loads the server overrides from the local file. +func loadServerOverrides(rootDir string) (*serverOverridesConfig, error) { + serverOverridesPath := filepath.Join(rootDir, constant.ServerOverridesFileName) + data, err := os.ReadFile(serverOverridesPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return &serverOverridesConfig{}, nil + } + return nil, err + } + var cfg serverOverridesConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/orbit/cmd/orbit/orbit_test.go b/orbit/cmd/orbit/orbit_test.go new file mode 100644 index 000000000..42f42aaf8 --- /dev/null +++ b/orbit/cmd/orbit/orbit_test.go @@ -0,0 +1,133 @@ +package main + +import ( + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/stretchr/testify/require" +) + +func TestCfgsDiffer(t *testing.T) { + for _, tc := range []struct { + name string + overrideCfg *serverOverridesConfig + orbitConfig *fleet.OrbitConfig + desktopEnabled bool + expected bool + }{ + { + name: "initial set of remote configuration", + overrideCfg: &serverOverridesConfig{}, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Orbit: "stable", + Osqueryd: "stable", + Desktop: "stable", + }, + }, + desktopEnabled: false, + expected: false, + }, + { + name: "initial set of remote configuration, omit some channels", + overrideCfg: &serverOverridesConfig{}, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Orbit: "stable", + }, + }, + desktopEnabled: false, + expected: false, + }, + { + name: "initial set of remote configuration, change orbit and omit some channels", + overrideCfg: &serverOverridesConfig{}, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Orbit: "edge", + }, + }, + desktopEnabled: false, + expected: true, + }, + { + name: "initial set of remote configuration, set desktop when Fleet Desktop disabled", + overrideCfg: &serverOverridesConfig{}, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Desktop: "foobar", + }, + }, + desktopEnabled: false, + expected: false, + }, + { + name: "initial set of remote configuration, set desktop with Fleet Desktop enabled", + overrideCfg: &serverOverridesConfig{}, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Desktop: "foobar", + }, + }, + desktopEnabled: true, + expected: true, + }, + { + name: "overrides update, set desktop with Fleet Desktop enabled", + overrideCfg: &serverOverridesConfig{ + DesktopChannel: "other", + }, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Desktop: "foobar", + }, + }, + desktopEnabled: true, + expected: true, + }, + { + name: "overrides update, change orbit", + overrideCfg: &serverOverridesConfig{ + OrbitChannel: "first", + }, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Orbit: "second", + }, + }, + desktopEnabled: false, + expected: true, + }, + { + name: "overrides update, change osqueryd", + overrideCfg: &serverOverridesConfig{ + OsquerydChannel: "first", + }, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{ + Osqueryd: "second", + }, + }, + desktopEnabled: false, + expected: true, + }, + { + name: "overrides update, empty means stable", + overrideCfg: &serverOverridesConfig{ + OrbitChannel: "stable", + OsquerydChannel: "stable", + DesktopChannel: "stable", + }, + orbitConfig: &fleet.OrbitConfig{ + UpdateChannels: &fleet.OrbitUpdateChannels{}, + }, + desktopEnabled: true, + expected: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + v := cfgsDiffer(tc.overrideCfg, tc.orbitConfig, tc.desktopEnabled) + require.Equal(t, tc.expected, v) + }) + } +} diff --git a/orbit/pkg/constant/constant.go b/orbit/pkg/constant/constant.go index c8ab2776b..931b48e23 100644 --- a/orbit/pkg/constant/constant.go +++ b/orbit/pkg/constant/constant.go @@ -49,4 +49,7 @@ const ( UpdateTLSClientKeyFileName = "update_client.key" // SilenceEnrollLogErrorEnvVer is an environment variable name for disabling enroll log errors SilenceEnrollLogErrorEnvVar = "FLEETD_SILENCE_ENROLL_ERROR" + // ServerOverridesFileName is the name of the file in the root directory + // that specifies the override configuration fetched from the server. + ServerOverridesFileName = "server-overrides.json" ) diff --git a/orbit/pkg/table/orbit_info/orbit_info.go b/orbit/pkg/table/orbit_info/orbit_info.go index 78bc4a69f..95b426a2e 100644 --- a/orbit/pkg/table/orbit_info/orbit_info.go +++ b/orbit/pkg/table/orbit_info/orbit_info.go @@ -3,6 +3,7 @@ package orbit_info import ( "context" "strconv" + "time" "github.com/fleetdm/fleet/v4/orbit/pkg/build" orbit_table "github.com/fleetdm/fleet/v4/orbit/pkg/table" @@ -13,6 +14,7 @@ import ( // Extension implements an extension table that provides info about Orbit. type Extension struct { + startTime time.Time orbitClient *service.OrbitClient orbitChannel string osquerydChannel string @@ -22,8 +24,9 @@ type Extension struct { var _ orbit_table.Extension = (*Extension)(nil) -func New(orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, trw *token.ReadWriter) *Extension { +func New(orbitClient *service.OrbitClient, orbitChannel, osquerydChannel, desktopChannel string, trw *token.ReadWriter, startTime time.Time) *Extension { return &Extension{ + startTime: startTime, orbitClient: orbitClient, orbitChannel: orbitChannel, osquerydChannel: osquerydChannel, @@ -47,6 +50,7 @@ func (o Extension) Columns() []table.ColumnDefinition { table.TextColumn("orbit_channel"), table.TextColumn("osqueryd_channel"), table.TextColumn("desktop_channel"), + table.BigIntColumn("uptime"), } } @@ -77,5 +81,6 @@ func (o Extension) GenerateFunc(_ context.Context, _ table.QueryContext) ([]map[ "orbit_channel": o.orbitChannel, "osqueryd_channel": o.osquerydChannel, "desktop_channel": o.desktopChannel, + "uptime": strconv.FormatInt(int64(time.Since(o.startTime).Seconds()), 10), }}, nil } diff --git a/orbit/pkg/update/runner.go b/orbit/pkg/update/runner.go index 1b64ae7c7..d5d70c917 100644 --- a/orbit/pkg/update/runner.go +++ b/orbit/pkg/update/runner.go @@ -14,6 +14,7 @@ import ( "github.com/fleetdm/fleet/v4/orbit/pkg/platform" "github.com/rs/zerolog/log" + "github.com/theupdateframework/go-tuf/client" ) // RunnerOptions is options provided for the update runner. @@ -103,6 +104,12 @@ func NewRunner(updater *Updater, opt RunnerOptions) (*Runner, error) { // (knowing that they are not expected to change during the execution of the runner). for _, target := range opt.Targets { if err := runner.StoreLocalHash(target); err != nil { + var tufFileNotFoundErr client.ErrNotFound + if errors.As(err, &tufFileNotFoundErr) { + // This can happen if the remote channel doesn't exist for a target. + // We don't want to error out, so we skip such target. + continue + } return nil, err } } @@ -234,7 +241,6 @@ func (r *Runner) UpdateAction() (bool, error) { // Performing update if either the binary is not updated // or the symlink needs to be updated and binary is not updated. if localBinaryNotUpdated || needsSymlinkUpdate { - // Update detected log.Info().Str("target", target).Msg("update detected") if err := r.updateTarget(target); err != nil { return didUpdate, fmt.Errorf("update %s: %w", target, err) diff --git a/server/fleet/agent_options.go b/server/fleet/agent_options.go index 84312f02c..c67afe106 100644 --- a/server/fleet/agent_options.go +++ b/server/fleet/agent_options.go @@ -18,6 +18,8 @@ type AgentOptions struct { CommandLineStartUpFlags json.RawMessage `json:"command_line_flags,omitempty"` // Extensions are the orbit managed extensions Extensions json.RawMessage `json:"extensions,omitempty"` + // UpdateChannels holds the configured channels for fleetd components. + UpdateChannels json.RawMessage `json:"update_channels,omitempty"` } type AgentOptionsOverrides struct { @@ -61,6 +63,23 @@ func ValidateJSONAgentOptions(ctx context.Context, ds Datastore, rawJSON json.Ra } } + if len(opts.UpdateChannels) > 0 { + if !isPremium { + // The update_channels feature is premium only. + return ErrMissingLicense + } + if string(opts.UpdateChannels) == "null" { + return errors.New("update_channels cannot be null") + } + if err := checkEmptyFields("update_channels", opts.UpdateChannels); err != nil { + return err + } + var updateChannels OrbitUpdateChannels + if err := JSONStrictDecode(bytes.NewReader(opts.UpdateChannels), &updateChannels); err != nil { + return fmt.Errorf("update_channels: %w", err) + } + } + if len(opts.Config) > 0 { if err := validateJSONAgentOptionsSet(opts.Config); err != nil { return fmt.Errorf("common config: %w", err) @@ -88,6 +107,22 @@ func ValidateJSONAgentOptions(ctx context.Context, ds Datastore, rawJSON json.Ra return nil } +func checkEmptyFields(prefix string, data json.RawMessage) error { + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { + return fmt.Errorf("unmarshal data: %w", err) + } + for k, v := range m { + if v == nil { + return fmt.Errorf("%s.%s is defined but not set", prefix, k) + } + if s, ok := v.(string); ok && s == "" { + return fmt.Errorf("%s.%s is set to an empty string", prefix, k) + } + } + return nil +} + func validateJSONAgentOptionsExtensions(ctx context.Context, ds Datastore, optsExtensions json.RawMessage, isPremium bool) error { var extensions map[string]ExtensionInfo if err := json.Unmarshal(optsExtensions, &extensions); err != nil { diff --git a/server/fleet/agent_options_test.go b/server/fleet/agent_options_test.go index ddff3b1dd..0db4bf056 100644 --- a/server/fleet/agent_options_test.go +++ b/server/fleet/agent_options_test.go @@ -10,37 +10,38 @@ import ( func TestValidateAgentOptions(t *testing.T) { cases := []struct { - desc string - in string - wantErr string + desc string + in string + isPremium bool + wantErr string }{ - {"empty object", "{}", ""}, - {"empty config", `{"config":{}}`, ""}, - {"empty overrides", `{"overrides":{}}`, ""}, + {"empty object", "{}", true, ""}, + {"empty config", `{"config":{}}`, true, ""}, + {"empty overrides", `{"overrides":{}}`, true, ""}, - {"unknown top-level key", `{"foo":1}`, `unknown field "foo"`}, - {"unknown config key", `{"config":{"foo":1}}`, `unknown field "foo"`}, - {"unknown overrides key", `{"overrides": {"foo": 1}}`, `unknown field "foo"`}, + {"unknown top-level key", `{"foo":1}`, true, `unknown field "foo"`}, + {"unknown config key", `{"config":{"foo":1}}`, true, `unknown field "foo"`}, + {"unknown overrides key", `{"overrides": {"foo": 1}}`, true, `unknown field "foo"`}, {"unknown overrides config key", `{"overrides": { "platforms": { "linux": {"foo":1} } - }}`, `unknown field "foo"`}, + }}`, true, `unknown field "foo"`}, {"overrides.platform is null", `{"overrides": { "platforms": { "darwin": null } - }}`, `platforms cannot be null. To remove platform overrides omit overrides from agent options.`}, + }}`, true, `platforms cannot be null. To remove platform overrides omit overrides from agent options.`}, - {"extra top-level bytes", `{}true`, `extra bytes`}, - {"extra config bytes", `{"config":{}true}`, `invalid character 't' after object`}, - {"extra overrides bytes", `{"overrides":{}true}`, `invalid character 't' after object`}, + {"extra top-level bytes", `{}true`, true, `extra bytes`}, + {"extra config bytes", `{"config":{}true}`, true, `invalid character 't' after object`}, + {"extra overrides bytes", `{"overrides":{}true}`, true, `invalid character 't' after object`}, {"valid config", `{"config":{ "options": {"aws_debug": true, "events_max": 3}, "views": {"view1": "select 1"} - }}`, ""}, + }}`, true, ""}, {"valid overrides", `{"overrides":{ "platforms": { "linux": { @@ -52,27 +53,27 @@ func TestValidateAgentOptions(t *testing.T) { "views": {"view2": "select 2"} } } - }}`, ""}, + }}`, true, ""}, {"invalid config value", `{"config":{ "events": { "disable_subscribers": true }, "options": {"aws_debug": 1} - }}`, "cannot unmarshal bool into Go struct field .events.disable_subscribers of type []string"}, + }}`, true, "cannot unmarshal bool into Go struct field .events.disable_subscribers of type []string"}, {"invalid overrides value", `{"overrides":{ "platforms": { "linux": { "options": {"aws_debug": true, "events_max": "nope"} } } - }}`, `cannot unmarshal string into Go struct field osqueryOptions.options.events_max of type uint64`}, + }}`, true, `cannot unmarshal string into Go struct field osqueryOptions.options.events_max of type uint64`}, {"valid packs string", `{"config":{ "packs": { "pack1": "ok" } - }}`, ""}, + }}`, true, ""}, {"valid packs object", `{"config":{ "packs": { "pack1": { @@ -84,7 +85,7 @@ func TestValidateAgentOptions(t *testing.T) { "platform": "darwin" } } - }}`, ""}, + }}`, true, ""}, {"invalid packs object key is accepted as we do not validate packs", `{"config":{ "packs": { "pack1": { @@ -97,57 +98,90 @@ func TestValidateAgentOptions(t *testing.T) { "platform": "darwin" } } - }}`, ``}, + }}`, true, ``}, {"invalid packs type is accepted as we do not validate packs", `{"config":{ "packs": { "pack1": 1 } - }}`, ``}, + }}`, true, ``}, {"invalid schedule type is accepted as we do not validate schedule", `{"config":{ "schedule": { "foo": 1 } - }}`, ``}, + }}`, true, ``}, {"option added in osquery 5.5.1", `{"config":{ "options": { "malloc_trim_threshold": 100 } - }}`, ``}, + }}`, true, ``}, {"option removed in osquery 5.5.1", `{"config":{ "options": { "yara_malloc_trim": true } - }}`, `unknown field "yara_malloc_trim"`}, + }}`, true, `unknown field "yara_malloc_trim"`}, {"valid command-line flag", `{"command_line_flags":{ "alarm_timeout": 1 - }}`, ``}, + }}`, true, ``}, {"invalid command-line flag", `{"command_line_flags":{ "no_such_flag": true - }}`, `unknown field "no_such_flag"`}, + }}`, true, `unknown field "no_such_flag"`}, {"invalid command-line value", `{"command_line_flags":{ "enable_tables": 123 - }}`, `cannot unmarshal number into Go struct field osqueryCommandLineFlags.enable_tables of type string`}, + }}`, true, `cannot unmarshal number into Go struct field osqueryCommandLineFlags.enable_tables of type string`}, {"setting a valid os-specific flag", `{"command_line_flags":{ "users_service_delay": 123 - }}`, ``}, + }}`, true, ``}, {"setting a valid os-specific option", `{"config":{ "options": { "users_service_delay": 123 } - }}`, ``}, + }}`, true, ``}, {"setting an invalid value for an os-specific flag", `{"command_line_flags":{ "disable_endpointsecurity": "ok" - }}`, `command-line flags: json: cannot unmarshal string into Go struct field osqueryCommandLineFlags.disable_endpointsecurity of type bool`}, + }}`, true, `command-line flags: json: cannot unmarshal string into Go struct field osqueryCommandLineFlags.disable_endpointsecurity of type bool`}, {"setting an invalid value for an os-specific option", `{"config":{ "options": { "disable_endpointsecurity": "ok" } - }}`, `common config: json: cannot unmarshal string into Go struct field osqueryOptions.options.disable_endpointsecurity of type bool`}, + }}`, true, `common config: json: cannot unmarshal string into Go struct field osqueryOptions.options.disable_endpointsecurity of type bool`}, + {"setting an empty update_channels", `{ + "update_channels": null + }`, true, `update_channels cannot be null`}, + {"setting a empty channel in update_channels", `{ + "update_channels": { + "osqueryd": "5.10.2", + "orbit": null + } + }`, true, `update_channels.orbit is defined but not set`}, + {"setting a channel in update_channels to empty string", `{ + "update_channels": { + "osqueryd": "5.10.2", + "orbit": "" + } + }`, true, `update_channels.orbit is set to an empty string`}, + {"setting a channel to unknown component in update_channels", `{ + "update_channels": { + "osqueryd": "5.10.2", + "unknown": "foobar" + } + }`, true, `update_channels: json: unknown field "unknown"`}, + {"setting update_channels non-premium", `{ + "update_channels": { + "osqueryd": "5.10.2", + "orbit": "foobar" + } + }`, false, `Requires Fleet Premium license`}, + {"setting update_channels", `{ + "update_channels": { + "osqueryd": "5.10.2", + "orbit": "foobar" + } + }`, true, ``}, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { - err := ValidateJSONAgentOptions(context.Background(), nil, []byte(c.in), true) + err := ValidateJSONAgentOptions(context.Background(), nil, []byte(c.in), c.isPremium) t.Logf("%T", errors.Unwrap(err)) if c.wantErr != "" { require.ErrorContains(t, err, c.wantErr) diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go index 23a1953b6..db7e73d2a 100644 --- a/server/fleet/orbit.go +++ b/server/fleet/orbit.go @@ -40,6 +40,20 @@ type OrbitConfig struct { Extensions json.RawMessage `json:"extensions,omitempty"` NudgeConfig *NudgeConfig `json:"nudge_config,omitempty"` Notifications OrbitConfigNotifications `json:"notifications,omitempty"` + // UpdateChannels contains the TUF channels to use on fleetd components. + // + // If UpdateChannels is nil it means the server isn't using/setting this feature. + UpdateChannels *OrbitUpdateChannels `json:"update_channels,omitempty"` +} + +// OrbitUpdateChannels hold the update channels that can be configured in fleetd agents. +type OrbitUpdateChannels struct { + // Orbit holds the orbit channel. + Orbit string `json:"orbit"` + // Osqueryd holds the osqueryd channel. + Osqueryd string `json:"osqueryd"` + // Desktop holds the Fleet Desktop channel. + Desktop string `json:"desktop"` } // OrbitHostInfo holds device information used during Orbit enroll. diff --git a/server/service/orbit.go b/server/service/orbit.go index 19e57b9e0..a74472de4 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -280,11 +280,21 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro notifs.EnforceBitLockerEncryption = true } + var updateChannels *fleet.OrbitUpdateChannels + if len(opts.UpdateChannels) > 0 { + var uc fleet.OrbitUpdateChannels + if err := json.Unmarshal(opts.UpdateChannels, &uc); err != nil { + return fleet.OrbitConfig{}, err + } + updateChannels = &uc + } + return fleet.OrbitConfig{ - Flags: opts.CommandLineStartUpFlags, - Extensions: extensionsFiltered, - Notifications: notifs, - NudgeConfig: nudgeConfig, + Flags: opts.CommandLineStartUpFlags, + Extensions: extensionsFiltered, + Notifications: notifs, + NudgeConfig: nudgeConfig, + UpdateChannels: updateChannels, }, nil } @@ -316,11 +326,21 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro notifs.EnforceBitLockerEncryption = true } + var updateChannels *fleet.OrbitUpdateChannels + if len(opts.UpdateChannels) > 0 { + var uc fleet.OrbitUpdateChannels + if err := json.Unmarshal(opts.UpdateChannels, &uc); err != nil { + return fleet.OrbitConfig{}, err + } + updateChannels = &uc + } + return fleet.OrbitConfig{ - Flags: opts.CommandLineStartUpFlags, - Extensions: extensionsFiltered, - Notifications: notifs, - NudgeConfig: nudgeConfig, + Flags: opts.CommandLineStartUpFlags, + Extensions: extensionsFiltered, + Notifications: notifs, + NudgeConfig: nudgeConfig, + UpdateChannels: updateChannels, }, nil }