GitHub
10/27/2026, 10:50 AM/usr/local/bin/osctrld as root with its config in /usr/local/etc/osctrld/. On Intel Macs with Homebrew, /usr/local and the Cellar belong to the Homebrew user, and the README installed files with no sudo or chown. Anyone who owns those directories could swap the binary or the config, and launchd would run it as root. The config controls every path root writes to and the extensions directory. osctrld never checked the owner or mode of its config file.
Change
• Startup warning (Linux and macOS). When osctrld runs as root with a config file, warnUnsafeConfigFile warns, once per problem and naming only paths, if:
• the file is not owned by root, or is group- or world-writable;
• the file's directory is not owned by root, or is group- or world-writable;
• the config is a symlink: the resolved target and both directories are checked, with duplicates skipped (macOS /var to /private/var does not double-warn).
• The check is a pure function (ownershipProblems) plus seams for the effective user and file owner. It runs after the logger is set up, so check-config shows it too. It is a no-op on Windows and for non-root runs.
• macOS deployment. The sample launchd plist now uses /Library/osctrld/osctrld and /Library/osctrld/osctrld.yaml, with a comment on ownership. The README macOS section installs with sudo install -o root -g wheel (binary 0755, config 0600, plist 0644), warns that Intel Homebrew /usr/local and the Cellar are user-owned, gives the commands to copy a Homebrew-installed binary to a root-owned location (re-copy after brew upgrade), and explains how to migrate from /usr/local. A new Hardening subsection describes the warning.
• `ARCHITECTURE.md`: startup checks, macOS deployment, and a Known Constraints entry.
Security / operational impact
• Makes the root-via-config path visible on every command, including check-config, and fixes the documented install.
• Warn only: nothing is refused and exit codes are unchanged, so existing fleets keep starting. Root-run daemons whose config is not root-owned or is group/world-writable will log warnings at every start. A config owned by a root group such as root:wheel 0660 also warns.
• Already-installed plists are untouched. Operators copying the new sample must install to /Library/osctrld.
• Not checked: the binary itself, ancestors above the immediate directory, and intermediate symlink directories. The docs steer away from user-owned locations, but code cannot detect a swapped binary.
• The flagfile, secret, certificate and extension directories have no equivalent helper and were not added. Existing checks cover the upgrade stash and the autoload file.
Validation
• go test ./... -count=1 -race, go vet for linux, darwin and windows, gofmt, golangci-lint (also for Windows), plutil -lint on the plist.
• Tests run on any OS, with the real app capturing stderr:
• a table test of the ownership and mode rules;
• a world-writable file warns (group and world each once) and no secret appears in the output;
• a good 0600 file gives no warning, and a non-root user gives none;
• a file and directory owned by another user warn;
• a symlink to a 0666 target warns, and a 0777 parent directory warns.
• Seen failing first: a compile failure, then behavioural failures in the table test and 4 of the 6 wiring tests. The good-file and non-root tests passed trivially until the check existed.
• Not verified on a macOS host: loading the new plist with launchd, running the README install commands, and the warning on an Intel Homebrew /usr/local install.
Not in this PR
• Windows secret ACL, atomic file writes, and Windows ownership checks.
• security_engineer.md says the systemd unit runs as a dedicated osctrld user, but service/linux/systemd.service has User=root. A doc fix is needed there.
jmpsec/osctrldGitHub
10/27/2026, 10:54 AM