Fix fleetd with pre-packaged osquery flags (#47305)

Resolves #47285.

Final behavior (matches pre-1.56):
```
┌────────────────────┬─────────────────────────┬───────────────────────────────────────┐
│ command_line_flags │    wire config.Flags    │             Orbit action              │
├────────────────────┼─────────────────────────┼───────────────────────────────────────┤
│ Unset              │ omitted (nil, len == 0) │ Preserve osquery.flags — return early │
├────────────────────┼─────────────────────────┼───────────────────────────────────────┤
│ null               │ null (len == 4)         │ Clear (parses to empty map)           │
├────────────────────┼─────────────────────────┼───────────────────────────────────────┤
│ {}                 │ {} (len == 2)           │ Clear (parses to empty map)           │
├────────────────────┼─────────────────────────┼───────────────────────────────────────┤
│ {"verbose": true}  │ non-empty               │ Write flags                           │
└────────────────────┴─────────────────────────┴───────────────────────────────────────┘
```

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [X] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [x] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [x] Verified that fleetd runs on macOS, Linux and Windows
- [x] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Preserve existing pre-packaged or user-provided osquery flagfiles when
the server omits flag configuration (no unintended clears or restarts).
* Treat an explicit empty flag payload (including JSON empty document or
JSON "null") as an instruction to clear flagfiles and queue a restart.

* **Tests**
* Expanded coverage to validate the preserve-vs-clear behavior for
unset, empty, and "null" flag payloads.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Lucas Manuel Rodriguez
2026-06-10 17:13:26 -03:00
committed by GitHub
parent afbbc447bb
commit 4991b3c685
3 changed files with 69 additions and 17 deletions
@@ -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.
+19 -11
View File
@@ -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
}
+49 -6
View File
@@ -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)