diff --git a/orbit/changes/47285-preserve-prepackaged-osquery-flags b/orbit/changes/47285-preserve-prepackaged-osquery-flags new file mode 100644 index 0000000000..5598608e21 --- /dev/null +++ b/orbit/changes/47285-preserve-prepackaged-osquery-flags @@ -0,0 +1 @@ +* Fixed fleetd clearing pre-packaged/user-provided osquery flagfiles (`osquery.flags`) when `command_line_flags` is unset in the agent settings. Setting `command_line_flags` to an empty document (`{}` or `null`) still explicitly clears the flagfile. diff --git a/orbit/pkg/update/flag_runner.go b/orbit/pkg/update/flag_runner.go index f12513bb44..b70d017bd3 100644 --- a/orbit/pkg/update/flag_runner.go +++ b/orbit/pkg/update/flag_runner.go @@ -45,9 +45,26 @@ func NewFlagReceiver(triggerOrbitRestart func(reason string), opt FlagUpdateOpti // It gets the flags from the Fleet server, and compares them to locally stored flagfile (if it exists) // If the flag comparison from disk and server are not equal, it writes the flags to disk, and returns true func (r *FlagRunner) Run(config *fleet.OrbitConfig) error { + // When command_line_flags is not set in the agent settings, the server omits + // the field and config.Flags is nil/empty. In that case Orbit leaves the + // osquery.flags file untouched, preserving any flags that were pre-packaged + // with fleetd or otherwise provided by the user. + // + // Explicitly setting command_line_flags to an empty document ({}) or "null" is + // distinct: those arrive as a non-empty payload that parses to an empty flag + // map, which reconciles (clears) the osquery.flags file below. + if len(config.Flags) == 0 { + return nil + } + + osqueryFlagMapFromFleet, err := getFlagsFromJSON(config.Flags) + if err != nil { + return fmt.Errorf("error parsing flags: %w", err) + } + flagFileExists := true - // first off try and read osquery.flags from disk + // try and read osquery.flags from disk osqueryFlagMapFromFile, err := readFlagFile(r.opt.RootDir) if err != nil { if !errors.Is(err, os.ErrNotExist) { @@ -57,16 +74,7 @@ func (r *FlagRunner) Run(config *fleet.OrbitConfig) error { flagFileExists = false } - // Nil/empty Flags is a valid state we must be able to reconcile TO - // (admin cleared command_line_flags, or server-side debug merge turned off). - osqueryFlagMapFromFleet := map[string]string{} - if len(config.Flags) > 0 { - osqueryFlagMapFromFleet, err = getFlagsFromJSON(config.Flags) - if err != nil { - return fmt.Errorf("error parsing flags: %w", err) - } - } - + // nothing on disk and nothing to write (e.g. {} with no existing file): no-op if !flagFileExists && len(osqueryFlagMapFromFleet) == 0 { return nil } diff --git a/orbit/pkg/update/flag_runner_test.go b/orbit/pkg/update/flag_runner_test.go index 2b3baba984..9fb5539c4b 100644 --- a/orbit/pkg/update/flag_runner_test.go +++ b/orbit/pkg/update/flag_runner_test.go @@ -78,8 +78,9 @@ func touchFile(t *testing.T, name string) { } // TestDoFlagsUpdateWithEmptyFlags tests the scenario of Fleet flag `command_line_flags` -// being set to an empty JSON document `{}` and Orbit osquery.flags file being -// an empty file. Such scenario should trigger no update of flags. +// being set to an empty JSON document `{}`. Setting it to `{}` is an explicit instruction +// to clear osquery flags, so Orbit reconciles the osquery.flags file to empty (distinct +// from command_line_flags being unset, which is covered by TestDoFlagsUpdateWithNilFlags). func TestDoFlagsUpdateWithEmptyFlags(t *testing.T) { rootDir := t.TempDir() osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags") @@ -102,13 +103,14 @@ func TestDoFlagsUpdateWithEmptyFlags(t *testing.T) { // Non-empty fleet flags and osquery.flags has empty flags. testConfig = &fleet.OrbitConfig{ - Flags: json.RawMessage(`{"--verbose": true}`), + Flags: json.RawMessage(`{"verbose": true}`), } err = fr.Run(testConfig) require.NoError(t, err) require.True(t, restartQueued) - // Empty Fleet flags and osquery.flags has non-empty flags. + // Empty Fleet flags ({}) and osquery.flags has non-empty flags: the file is + // cleared and a restart is triggered. restartQueued = false testConfig = &fleet.OrbitConfig{ Flags: json.RawMessage("{}"), @@ -118,8 +120,16 @@ func TestDoFlagsUpdateWithEmptyFlags(t *testing.T) { err = fr.Run(testConfig) require.NoError(t, err) require.True(t, restartQueued) + + contents, err := os.ReadFile(osqueryFlagsFile) + require.NoError(t, err) + require.Empty(t, string(contents)) } +// TestDoFlagsUpdateWithNilFlags verifies that when the server is not managing +// osquery command-line flags (command_line_flags unset, so config.Flags is +// nil/empty), Orbit leaves the osquery.flags file untouched, preserving any +// pre-packaged or user-provided flags. func TestDoFlagsUpdateWithNilFlags(t *testing.T) { rootDir := t.TempDir() osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags") @@ -133,11 +143,44 @@ func TestDoFlagsUpdateWithNilFlags(t *testing.T) { require.NoError(t, err) require.False(t, restartQueued) - // Nil Flags + existing file: reconcile to empty + restart. - err = os.WriteFile(osqueryFlagsFile, []byte("--verbose=true\n--tls_dump=true\n"), 0o644) + // Nil Flags + existing user-provided file: file is preserved, no restart. + userFlags := "--verbose=true\n--tls_dump=true\n" + err = os.WriteFile(osqueryFlagsFile, []byte(userFlags), 0o644) require.NoError(t, err) err = fr.Run(&fleet.OrbitConfig{Flags: nil}) require.NoError(t, err) + require.False(t, restartQueued) + + contents, err := os.ReadFile(osqueryFlagsFile) + require.NoError(t, err) + require.Equal(t, userFlags, string(contents)) + + // Empty (but non-nil) Flags is treated the same as nil: file preserved. + err = fr.Run(&fleet.OrbitConfig{Flags: json.RawMessage("")}) + require.NoError(t, err) + require.False(t, restartQueued) + + contents, err = os.ReadFile(osqueryFlagsFile) + require.NoError(t, err) + require.Equal(t, userFlags, string(contents)) +} + +// TestDoFlagsUpdateWithNullFlags verifies that the JSON literal "null" is an +// explicit instruction to clear osquery flags, distinct from command_line_flags +// being unset (which preserves the file). +func TestDoFlagsUpdateWithNullFlags(t *testing.T) { + rootDir := t.TempDir() + osqueryFlagsFile := filepath.Join(rootDir, "osquery.flags") + + var restartQueued bool + queueOrbitRestart := func(string) { restartQueued = true } + fr := NewFlagReceiver(queueOrbitRestart, FlagUpdateOptions{RootDir: rootDir}) + + // "null" + existing flags: the file is cleared and a restart is triggered. + err := os.WriteFile(osqueryFlagsFile, []byte("--verbose=true\n"), 0o644) + require.NoError(t, err) + err = fr.Run(&fleet.OrbitConfig{Flags: json.RawMessage("null")}) + require.NoError(t, err) require.True(t, restartQueued) contents, err := os.ReadFile(osqueryFlagsFile)