GitHub
10/27/2026, 2:45 PMinstall wrote the enrollment secret with a mode that does nothing on Windows, so the file inherited the Program Files ACL (Users can read). After any upgrade restore, the restore code also applied a protected ACL that explicitly grants Users read and execute, undoing hardening.
• Windows ownership checks always passed. adminOwnedDir returned true and adminOwnedFile re-applied the ACL before trusting the file. With osquery pointing at a user-writable folder, a user could pre-create extensions.load.
• Writes were not atomic and followed symlinks. writeContentExists used os.WriteFile plus os.Chmod, so a symlink at the flagfile, certificate or secret path was followed, and a crash could leave a truncated file. Several writers did not fsync before rename.
• PowerShell path interpolation. The Authenticode check built Get-AuthenticodeSignature '<path>' by interpolating the path into a command string.
Change
• Secret ACL.
• The secret file gets a protected DACL: SYSTEM and Administrators full control, nothing else, nothing inheritable (D:P(A;;FA;;;SY)(A;;FA;;;BA)). osqueryd runs as SYSTEM, so it can still read it.
• On Windows the secret's temp file is created with that ACL in the same CreateFile call, so there is no window where it inherits the folder's ACL.
• install re-applies it when the content is unchanged, which fixes hosts installed earlier. If that fails it warns.
• Upgrade restore writes the secret through the same path and no longer hands it to the Users-readable owner ACL.
• The flagfile, certificate and autoload file keep their existing ACL. A portable rule (fileACLFor) decides which class applies to which path.
• Ownership checks. adminOnlyWrite decides from the owner and the DACL.
• The owner must be SYSTEM, Administrators or TrustedInstaller.
• No other principal may hold write, append, delete-child, delete, write-DAC, write-owner or generic write/all rights.
• Inheritable entries count, and CREATOR OWNER is accepted.
• adminOwnedDir now checks for a real directory and then the ACL, and refuses autoload writes when unsafe, with the same error as POSIX.
• adminOwnedFile checks for a regular file and then only reads owner and ACL. It no longer re-applies the ACL first.
• The default C:\Program Files\osquery ACL and osquery's own safe-permissions ACL pass.
• Atomic writes.
• writeContentExists goes through `writeFileAtomic`: a symlink at the target is replaced, not followed. Directories and devices are refused. The temp file is created with an unpredictable name and its mode is set before writing. It is fsynced, renamed over the target, and the directory is fsynced on POSIX. The POSIX owner of a replaced file is preserved.
• installExtension, updateAutoload and stashPackage also fsync before and after the rename.
• Authenticode. authenticodeCommand passes the path in the environment variable OSCTRLD_PKG_PATH and uses -LiteralPath, so the path is never part of the script and never treated as a wildcard. Failures now include the PowerShell output.
• ARCHITECTURE.md and README.md updated (file-write semantics, Windows ACL classes, ownership checks, hardening note with an icacls check).
Security / operational impact
• The secret is no longer readable by ordinary Windows users. Non-elevated tools running as a regular user can no longer read it.
• Behaviour changes:
• On Windows, a osquery folder whose ACL lets non-admins write, or that a user account owns, now refuses autoload writes. Extensions then report errors. The default MSI location passes. For a hand-created admin folder owned by a user SID, fix with icacls <dir> /setowner Administrators.
• Existing Windows extension files whose ACL is not admin-only are downloaded again instead of silently adopted.
• A symlink at the flagfile, certificate or secret path now needs --force and is replaced with a regular file. The daemon always forces, so it replaces it once and restarts osquery.
• Directories and devices at those paths are refused.
• On Windows, flags and certificate get the folder's inherited ACL on each rewrite.
• Non-root runs need write access to the directory, not only the file. The shipped units run as root.
• Not changed: uninstall (deletes the file, and unlinks a symlink instead of following it).
Validation
• go test ./... -count=1 -race, go vet for linux, darwin and windows, GOOS=windows go build, gofmt, golangci-lint (also for Windows and Linux).
• Tests seen failing first:
• a symlinked target was followed and chmodded by the old code;
• no partial file on a failed write;
• the secret temp file recorded as restricted;
• restore no longer hands the secret to the Users-readable owner ACL;
• the ACL rule and the secret SDDL;
• every unsafe case in the ownership table;
• the PowerShell path staying out of the script.
• No failing run: the umask test (the old code chmodded after writing as well), the fsync additions and POSIX owner preservation, which cannot be observed or need root. The new Windows-only tests compile and vet but have not run.
• Needs a Windows host: the 11-step checklist with icacls and PowerShell commands. It covers a fresh install, re-install on an existing host, osqueryd still enrolling, upgrade and rollback, a refused unsafe directory, flag rewrites, the Authenticode check with a quote in the path, and extension adoption.
Known limits
• Deny entries are ignored and object or callback ACEs count as untrusted, so an exotic but safe ACL can be judged unsafe.
• Rename over an existing file can fail if another process holds it open without delete sharing (for example antivirus). Needs a check on a real host.
• The secret is matched by path, so an 8.3 short name or a hard link is not recognised as the secret.
• The quick-add script's Set-SafePermissions is applied to the osqueryd subfolder, not C:\Program Files\osquery. The secret is readable because that folder inherits the Program Files ACL.
jmpsec/osctrldGitHub
10/27/2026, 3:34 PM