fleet/orbit/pkg/packaging/macos_templates.go
Nico b42fc182fe
Fix fleetd in-band upgrade on macOS hosts (#42187)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #32126

# Checklist for submitter


- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [ ] Added/updated automated tests

- [x] QA'd all new/changed functionality manually

Steps:
- Have fleetd installed on the host.
- `make build` and re-run the server.
- Generate a new fleetd package: `./build/fleetctl package --type=pkg
--enable-scripts --fleet-desktop --fleet-url=<URL>
--enroll-secret=<SECRET>`
- Upload the newly-generated `fleet-osquery.pkg` to Host details >
Software > Library.
- Click `Install`.
- When the install finishes, verify that the UI says `Installed`:

<img width="1433" height="392" alt="Screenshot 2026-03-20 at 4 42 19 PM"
src="https://github.com/user-attachments/assets/ec78b63e-e5c7-4b27-acde-4e4f63f5f7b2"
/>

- Verified logs:

`/var/log/orbit/orbit.stderr.log` logs after successful upgrade:

```
2026-03-20T16:24:58-03:00 INF hash(orbit)=4ba4729515dc6923cf54eaca610c6dbded344941a10e552579c19676b7419bc5643e98fd8cf404d8ed2cd6168d7b756b2df56997ff41b51b520fa6456b407979
2026-03-20T16:24:58-03:00 INF hash(osqueryd)=9d2ab3eb30537e38c78a089ae28196d34afc436030bca10ae60a06fd20e344bc911ab0e036e8abb44e401809b6056a04aa9dddf00d90386a451fe55ca3a0ffe8
2026-03-20T16:24:58-03:00 INF hash(desktop)=9317a1617709492dec2cb2ff3821412e5061c402b1c7988f16a99faa81b2c8dffa1fb038d5fb8c4dae67e5545a577bbe6b1a8c13adb39453b2ba7bddfb36dafa
2026-03-20T16:24:58-03:00 INF orbit version: 1.53.1
2026-03-20T16:25:00-03:00 INF Found osquery version: 5.21.0
2026-03-20T16:25:12-03:00 INF token rotation is enabled
2026-03-20T16:25:14-03:00 INF Found fleet-desktop version: 1.53.1
2026-03-20T16:25:14-03:00 INF checking for custom mdm enrollment profile with end user email
2026-03-20T16:25:14-03:00 INF get custom enrollment profile end user email: profile not found
2026-03-20T16:25:14-03:00 INF orbitClient.GetServerCapabilities() map[end_user_email:{} escrow_buddy:{} linux_disk_encryption_escrow:{} macos_web_setup_experience:{} orbit_endpoints:{} setup_experience:{} token_rotation:{} web_setup_experience:{}]
2026-03-20T16:25:14-03:00 INF opening path="/opt/orbit/bin/desktop/macos/stable/Fleet Desktop.app"
2026-03-20T16:25:14-03:00 INF start osqueryd cmd="/opt/orbit/bin/osqueryd/macos-app/stable/osquery.app/Contents/MacOS/osqueryd --pidfile=/opt/orbit/osquery.pid --extensions_socket=/opt/orbit/orbit-osquery.em --logger_path=/opt/orbit/osquery_log --enroll_secret_env ENROLL_SECRET --tls_hostname=nicofleet.ngrok.io --enroll_tls_endpoint=/api/v1/osquery/enroll --config_plugin=tls --config_tls_endpoint=/api/v1/osquery/config --config_refresh=60 --disable_distributed=false --distributed_plugin=tls --distributed_tls_max_attempts=10 --distributed_tls_read_endpoint=/api/v1/osquery/distributed/read --distributed_tls_write_endpoint=/api/v1/osquery/distributed/write --logger_plugin=tls,filesystem --logger_tls_endpoint=/api/v1/osquery/log --disable_carver=false --carver_disable_function=false --carver_start_endpoint=/api/v1/osquery/carve/begin --carver_continue_endpoint=/api/v1/osquery/carve/block --carver_block_size=8000000 --tls_accept_gzip=true --tls_server_certs /opt/orbit/certs.pem --augeas_lenses /opt/orbit/lenses --force --flagfile /opt/orbit/osquery.flags --host-identifier uuid --database_path /opt/orbit/osquery.db"
2026-03-20T16:25:14-03:00 INF killing any pre-existing fleet-desktop instances
I0320 16:25:20.108963 1878142976 interface.cpp:137] Registering extension (com.fleetdm.orbit.osquery_extension.v1, 45937, version=, sdk=)
I0320 16:25:30.446642 194764992 eventfactory.cpp:156] Event publisher not enabled: endpointsecurity: EndpointSecurity is disabled via configuration
I0320 16:25:30.474906 194764992 eventfactory.cpp:156] Event publisher not enabled: endpointsecurity_fim: EndpointSecurity is disabled via configuration
I0320 16:25:30.475134 194764992 eventfactory.cpp:156] Event publisher not enabled: openbsm: Publisher disabled via configuration
I0320 16:25:30.475183 194764992 eventfactory.cpp:156] Event publisher not enabled: scnetwork: Publisher not used
I0320 16:25:30.475217 194764992 eventfactory.cpp:156] Event publisher not enabled: event_tapping: Publisher disabled via configuration
2026-03-20T16:27:14-03:00 INF received notification for software installers: [147149e7-2634-4b23-b724-aafc995e3f09] runner=installer
2026-03-20T16:27:14-03:00 INF processing installerID=147149e7-2634-4b23-b724-aafc995e3f09 runner=installer
2026-03-20T16:27:14-03:00 INF fetching installer details installerID=147149e7-2634-4b23-b724-aafc995e3f09 runner=installer
2026-03-20T16:27:14-03:00 INF about to download software installer from Fleet installerID=147149e7-2634-4b23-b724-aafc995e3f09 runner=installer
2026-03-20T16:27:37-03:00 INF done downloading installerID=147149e7-2634-4b23-b724-aafc995e3f09 runner=installer
2026-03-20T16:27:37-03:00 INF software installer downloaded installerID=147149e7-2634-4b23-b724-aafc995e3f09 installerPath=/tmp/3354102551/fleet-osquery.pkg runner=installer
2026-03-20T16:27:37-03:00 INF about to run install script installerID=147149e7-2634-4b23-b724-aafc995e3f09 runner=installer
2026-03-20T16:27:40-03:00 INF install script exitCode=0 installerID=147149e7-2634-4b23-b724-aafc995e3f09 runner=installer
```

---------

Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
2026-03-27 09:04:14 -03:00

201 lines
7.1 KiB
Go

package packaging
import "text/template"
// Best reference I could find:
// http://s.sudre.free.fr/Stuff/Ivanhoe/FLAT.html
var macosPackageInfoTemplate = template.Must(template.New("").Option("missingkey=error").Parse(
`<pkg-info format-version="2" identifier="{{.Identifier}}.base.pkg" version="{{.Version}}" install-location="/" auth="root">
<payload/>
<scripts>
<postinstall file="./postinstall"/>
</scripts>
<bundle-version>
</bundle-version>
</pkg-info>
`))
// This template is used to generate a Distribution Definition file, which
// controls the experience of the installer (the default dir, what options the
// user has, etc.)
//
// Reference:
// https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html
var macosDistributionTemplate = template.Must(template.New("").Option("missingkey=error").Parse(
`<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="2">
<title>Fleet osquery</title>
<choices-outline>
<line choice="choiceBase"/>
</choices-outline>
<choice id="choiceBase" title="Fleet osquery" enabled="false" selected="true" description="Standard installation for Fleet osquery.">
<pkg-ref id="{{.Identifier}}.base.pkg"/>
</choice>
{{/* base.pkg specified here is the foldername that contains the package contents */}}
<pkg-ref id="{{.Identifier}}.base.pkg" version="{{.Version}}" auth="root">#base.pkg</pkg-ref>
{{/* this ref is collapsed with the previous, having a bundle version helps our notarization tools */}}
<pkg-ref id="{{.Identifier}}.base.pkg">
<bundle-version>
<bundle id="{{.Identifier}}" path="" />
</bundle-version>
</pkg-ref>
<options customize="never" hostArchitectures="arm64,x86_64" require-scripts="false" allow-external-scripts="false" />
</installer-gui-script>
`))
var macosPostinstallTemplate = template.Must(template.New("").Option("missingkey=error").Parse(
`#!/bin/bash
ln -sf /opt/orbit/bin/orbit/macos/{{.OrbitChannel}}/orbit /opt/orbit/bin/orbit/orbit
ln -sf /opt/orbit/bin/orbit/orbit /usr/local/bin/orbit
{{ if .LegacyVarLibSymlink }}
# Symlink needed to support old versions of orbit.
ln -sf /opt/orbit /var/lib/orbit
{{- end }}
{{ if .StartService -}}
DAEMON_LABEL="com.fleetdm.orbit"
DAEMON_PLIST="/Library/LaunchDaemons/${DAEMON_LABEL}.plist"
if [ -f /opt/orbit/.inband_upgrade ]; then
rm -f /opt/orbit/.inband_upgrade
# In-band upgrade: orbit is installing an update to itself.
# We must not stop orbit now because it is managing this installation.
# Instead, schedule a delayed bootout/bootstrap so the package install
# can finish first. A full bootout+bootstrap (not just kickstart) ensures
# any plist changes from the upgrade are picked up.
echo "Detected in-band upgrade (orbit upgrading orbit). Delaying service"
echo "restart to prevent orbit from being stopped mid-install."
(sleep 30; pkill fleet-desktop || true; launchctl bootout "system/${DAEMON_LABEL}" || true; launchctl enable "system/${DAEMON_LABEL}"; count=0; while ! launchctl bootstrap system "${DAEMON_PLIST}"; do sleep 1; count=$((count+1)); if [ "$count" -eq 30 ]; then echo "Failed to bootstrap system ${DAEMON_PLIST}"; exit 1; fi; echo "Retrying launchctl bootstrap..."; done; launchctl kickstart "system/${DAEMON_LABEL}") &>/dev/null &
exit 0
fi
# Stop the previous desktop agent
pkill fleet-desktop || true
# Remove any pre-existing version of the config
launchctl bootout "system/${DAEMON_LABEL}"
# Make sure the launch daemon is enabled before we try to bootstrap it
launchctl enable "system/${DAEMON_LABEL}"
# Add the daemon to the launchd system.
#
# We add retries because we've seen "launchctl bootstrap" fail
# if the service is still running after bootout (in case the
# service takes a bit longer to terminate gracefully).
# We've seen this when deploying the package via an MDM server.
#
count=0
while ! launchctl bootstrap system "${DAEMON_PLIST}"; do
sleep 1
((count++))
if [[ $count -eq 30 ]]; then
echo "Failed to bootstrap system ${DAEMON_PLIST}"
exit 1
fi
echo "Retrying launchctl bootstrap..."
done
echo "Successfully bootstrap system ${DAEMON_PLIST}"
# Force the daemon to start
launchctl kickstart "system/${DAEMON_LABEL}"
{{- end }}
`))
// Note it's important not to start the orbit binary in
// `/usr/local/bin/orbit` because this is a path that users usually have write
// access to, and running that binary with launchd can become a privilege
// escalation vector.
var macosLaunchdTemplate = template.Must(template.New("").Option("missingkey=error").Parse(
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
{{- if .Debug }}
<key>ORBIT_DEBUG</key>
<string>true</string>
{{- end }}
{{- if .Insecure }}
<key>ORBIT_INSECURE</key>
<string>true</string>
{{- end }}
{{- if .FleetCertificate }}
<key>ORBIT_FLEET_CERTIFICATE</key>
<string>/opt/orbit/fleet.pem</string>
{{- end }}
{{- if .EnrollSecret }}
<key>ORBIT_ENROLL_SECRET_PATH</key>
<string>/opt/orbit/secret.txt</string>
{{- end }}
{{- if .FleetURL }}
<key>ORBIT_FLEET_URL</key>
<string>{{ .FleetURL }}</string>
{{- end }}
{{- if .UseSystemConfiguration }}
<key>ORBIT_USE_SYSTEM_CONFIGURATION</key>
<string>{{ .UseSystemConfiguration }}</string>
{{- end }}
{{- if .EnableScripts }}
<key>ORBIT_ENABLE_SCRIPTS</key>
<string>{{ .EnableScripts }}</string>
{{- end }}
{{- if .DisableUpdates }}
<key>ORBIT_DISABLE_UPDATES</key>
<string>true</string>
{{- end }}
<key>ORBIT_ORBIT_CHANNEL</key>
<string>{{ .OrbitChannel }}</string>
<key>ORBIT_OSQUERYD_CHANNEL</key>
<string>{{ .OsquerydChannel }}</string>
<key>ORBIT_UPDATE_URL</key>
<string>{{ .UpdateURL }}</string>
{{- if .UpdateTLSServerCertificate }}
<key>ORBIT_UPDATE_TLS_CERTIFICATE</key>
<string>/opt/orbit/update.pem</string>
{{- end }}
{{- if .Desktop }}
<key>ORBIT_FLEET_DESKTOP</key>
<string>true</string>
<key>ORBIT_DESKTOP_CHANNEL</key>
<string>{{ .DesktopChannel }}</string>
{{- if .FleetDesktopAlternativeBrowserHost }}
<key>ORBIT_FLEET_DESKTOP_ALTERNATIVE_BROWSER_HOST</key>
<string>{{ .FleetDesktopAlternativeBrowserHost }}</string>
{{- end }}
{{- end }}
<key>ORBIT_UPDATE_INTERVAL</key>
<string>{{ .OrbitUpdateInterval }}</string>
{{- if and (ne .HostIdentifier "") (ne .HostIdentifier "uuid") }}
<key>ORBIT_HOST_IDENTIFIER</key>
<string>{{ .HostIdentifier }}</string>
{{- end }}
{{- if .DisableKeystore }}
<key>ORBIT_DISABLE_KEYSTORE</key>
<string>true</string>
{{- end }}
{{- if .OsqueryDB }}
<key>ORBIT_OSQUERY_DB</key>
<string>{{ .OsqueryDB }}</string>
{{- end }}
</dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.fleetdm.orbit</string>
<key>ProgramArguments</key>
<array>
<string>/opt/orbit/bin/orbit/orbit</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/var/log/orbit/orbit.stderr.log</string>
<key>StandardOutPath</key>
<string>/var/log/orbit/orbit.stdout.log</string>
<key>ThrottleInterval</key>
<integer>10</integer>
</dict>
</plist>
`))