GitHub
10/16/2026, 9:50 PMFlagsHandler and VerifyHandler now use the new NodeFlags. It serves the environment's stored, operator-edited flags with the secret and certificate paths filled in. Environments with no stored flags still get the generated defaults.
• FillFlagPaths is shared. It replaces __SECRET_FILE__ and `__CERT_FILE__`; an empty path keeps its placeholder, the same as the generated flags. osctrl-api's per-platform flag substitution now uses it too.
• Verify compares against the same flags the flags endpoint serves, so osctrld's verify no longer reports a mismatch against defaults nobody deployed.
• osctrld_flags_test.go covers four cases: stored flags are served with paths filled, placeholders are kept when no paths are given, generated flags are the fallback when nothing is stored, and a wrong secret is rejected.
Client side: this PR (osctrld)
Problems
• Linux restart only tried systemctl, so hosts without systemd never applied new flags.
• Windows restart wasn't implemented, even though install already managed the service through the Windows service manager.
• The packaged systemd unit never started. It required /opt/osctrld, but the deb, rpm and apk packages install to /usr/bin/osctrld.
• The unit ran as User=osctrld, which can't write /etc/osquery or restart osqueryd, the daemon's whole job.
• The macOS plist shipped with /path/to/ placeholders.
• Extension names went unchecked into filepath.Join, so a manifest entry named ../x could write anywhere the daemon can.
• HTTP requests had no timeout, so a server that never answered hung the service loop forever.
Changes
• Restart:
• Linux tries systemctl, then service, then /etc/init.d/osqueryd, the same chain install uses for stop and start.
• Windows stops the service, waits until it has stopped, then starts it.
• macOS is unchanged.
• Extensions: a manifest entry must be a plain file name. Others are logged and skipped, and the rest still install.
• HTTP: a 10-minute timeout per request, body included. It's that long because the same client downloads osquery packages of around 100 MB.
• systemd unit: runs /usr/bin/osctrld as root after network-online.target, and logs to journald.
• macOS plist: runs /usr/local/bin/osctrld and logs to /var/log/osctrld.log.
• Packaging and README: /etc/osctrld/osctrld.yaml is installed 0600, since it holds the osctrl secret. The README install steps match the packaged paths.
Security and operational impact
• Root: osctrld runs as root under systemd. It already needed equivalent privileges on macOS and Windows.
• Path traversal: extension names can no longer write outside the extensions directory.
• Manual installs: hosts installed to /opt/osctrld per the old README must move the binary to /usr/bin/osctrld or edit the unit.
• Restart behaviour: Windows hosts and hosts without systemd now restart osqueryd when the flags, certificate or extensions change. Before, the restart failed and logged an error.
Validation
• `go test ./cmd/osctrld/ -race`: all 109 tests ran, 0 failures
• go vet and golangci-lint run ./... for GOOS=darwin, linux and `windows`: clean, 0 issues
• plutil -lint on the plist: OK
• New tests:
• TestOsqueryRestartCandidates, TestRestartOsqueryDarwin, TestSyncExtensions_RejectsNamesOutsideTheDirectory and TestSendRequestTimesOut pass.
• TestRestartOsqueryFallsBackWithoutSystemd is Linux-only; it runs in CI on ubuntu-latest and skips on macOS.
• `goreleaser check`: the config is valid. The check still fails on the existing brews deprecation, which this PR doesn't touch.
jmpsec/osctrldGitHub
10/16/2026, 9:54 PM