Remotely configure fleetd update channels (#15848)

#13825

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [X] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).

---------

Co-authored-by: Victor Lyuboslavsky <victor.lyuboslavsky@gmail.com>
This commit is contained in:
Lucas Manuel Rodriguez 2024-01-02 17:59:40 -03:00 committed by GitHub
parent 38b8c9cc58
commit d2015d1a36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 593 additions and 60 deletions

View File

@ -0,0 +1 @@
* Remotely configure fleetd update channels in agent options (Fleet Premium only, and requires fleetd >= 1.20.0).

View File

@ -0,0 +1,2 @@
* Allow configuring TUF channels of `orbit`, `osqueryd` and `desktop` from Fleet agent settings.
* Add `uptime` column to `orbit_info` table.

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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.

View File

@ -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
}