<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #45524 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [ ] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). ## New Fleet configuration settings - [ ] Setting(s) is/are explicitly excluded from GitOps If you didn't check the box above, follow this checklist for GitOps-enabled settings: - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [x] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Apple Platform SSO (PSSO) for macOS with device registration, sign-in, and public discovery (JWKS + Apple app-site association) protected by single-use nonces. * Added Apple account provisioning (Platform SSO password sync) configuration with masked client-secret handling and GitOps support. * Added a host-scoped PSSO device registration token variable for Apple MDM profile generation. * **Bug Fixes** * Fixed macOS packaging to correctly build, embed, and sign the Platform SSO extension. * Resetting device Apple MDM data now also clears stored PSSO enrollment records. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Magnus Jensen <magnus@fleetdm.com>
230 lines
11 KiB
YAML
230 lines
11 KiB
YAML
name: Build Fleet Desktop (macOS)
|
|
|
|
# Builds the native macOS Fleet Desktop app (apps/fleet-desktop-macos/) and its
|
|
# embedded Platform SSO extension (FleetPSSOExtension.appex), code signs and
|
|
# notarizes them with Fleet's Developer ID certificates, and uploads the signed
|
|
# .pkg as a workflow artifact. No GitHub Release is created.
|
|
#
|
|
# The app and extension carry managed Associated Domains entitlements
|
|
# (com.apple.developer.associated-domains{,.mdm-managed}). Those are restricted
|
|
# entitlements: codesign only honors them when a Developer ID provisioning
|
|
# profile that grants them is embedded in the bundle. The profiles are provided
|
|
# as base64 repo secrets (never committed) and embedded at sign time — see the
|
|
# README's "Signing secrets" section.
|
|
#
|
|
# This workflow always signs and notarizes. If the certs or provisioning
|
|
# profiles are unavailable (e.g. a fork PR that can't read secrets), it fails
|
|
# loudly rather than producing an unsigned artifact.
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
paths:
|
|
- 'apps/fleet-desktop-macos/**'
|
|
- '.github/workflows/fleet-desktop-macos-build.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'apps/fleet-desktop-macos/**'
|
|
- '.github/workflows/fleet-desktop-macos-build.yml'
|
|
workflow_dispatch:
|
|
|
|
# Cancel superseded runs on the same ref.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
cancel-in-progress: true
|
|
|
|
defaults:
|
|
run:
|
|
# fail-fast using bash -eo pipefail.
|
|
shell: bash
|
|
working-directory: apps/fleet-desktop-macos
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
# Fleet's Developer ID certificate identities (SHA-1). Same team as the rest
|
|
# of Fleet's macOS artifacts (orbit Fleet Desktop, fleetd-base.pkg).
|
|
APPLICATION_SIGNING_IDENTITY_SHA1: 604D877399AAEB7630A78B84F288E2D28A2EDE42
|
|
INSTALLER_SIGNING_IDENTITY_SHA1: 4608F71FB42E1845C7FC9B2D2B6A7A8D11BBD940
|
|
# Embedded SSO extension bundle (relative to Fleet Desktop.app/Contents).
|
|
APPEX_REL_PATH: PlugIns/FleetPSSOExtension.appex
|
|
|
|
jobs:
|
|
build:
|
|
name: Build, sign, and notarize Fleet Desktop (macOS)
|
|
runs-on: macos-latest
|
|
steps:
|
|
- name: Harden Runner
|
|
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Build app (with embedded extension) and create pkg
|
|
run: |
|
|
chmod +x build.sh build-pkg.sh
|
|
./build-pkg.sh
|
|
|
|
- name: Import Developer ID certificates
|
|
env:
|
|
APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }}
|
|
APPLE_APPLICATION_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APPLICATION_CERTIFICATE_PASSWORD }}
|
|
APPLE_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_INSTALLER_CERTIFICATE }}
|
|
APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
|
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
|
run: |
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
|
security default-keychain -s build.keychain
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
|
|
|
# Developer ID Application certificate — signs the .app/.appex (codesign).
|
|
echo "$APPLE_APPLICATION_CERTIFICATE" | base64 --decode > application.p12
|
|
security import application.p12 -k build.keychain -P "$APPLE_APPLICATION_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
|
rm application.p12
|
|
|
|
# Developer ID Installer certificate — signs the .pkg (productsign).
|
|
echo "$APPLE_INSTALLER_CERTIFICATE" | base64 --decode > installer.p12
|
|
security import installer.p12 -k build.keychain -P "$APPLE_INSTALLER_CERTIFICATE_PASSWORD" -T /usr/bin/productsign
|
|
rm installer.p12
|
|
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign:,productsign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
|
security find-identity -vv
|
|
|
|
- name: Embed provisioning profiles
|
|
env:
|
|
APPLE_FLEET_DESKTOP_APP_PROFILE_B64: ${{ secrets.APPLE_FLEET_DESKTOP_APP_PROFILE_B64 }}
|
|
APPLE_PSSO_EXT_PROFILE_B64: ${{ secrets.APPLE_PSSO_EXT_PROFILE_B64 }}
|
|
run: |
|
|
APP="build/Fleet Desktop.app"
|
|
APPEX="$APP/Contents/$APPEX_REL_PATH"
|
|
|
|
if [ -z "$APPLE_FLEET_DESKTOP_APP_PROFILE_B64" ] || [ -z "$APPLE_PSSO_EXT_PROFILE_B64" ]; then
|
|
echo "::error::Missing provisioning profile secrets (APPLE_FLEET_DESKTOP_APP_PROFILE_B64 / APPLE_PSSO_EXT_PROFILE_B64). The app and extension carry restricted Associated Domains entitlements that codesign cannot honor without them."
|
|
exit 1
|
|
fi
|
|
|
|
# Developer ID profiles authorizing the restricted entitlements.
|
|
echo "$APPLE_PSSO_EXT_PROFILE_B64" | base64 --decode > "$APPEX/Contents/embedded.provisionprofile"
|
|
echo "$APPLE_FLEET_DESKTOP_APP_PROFILE_B64" | base64 --decode > "$APP/Contents/embedded.provisionprofile"
|
|
|
|
- name: Verify profiles authorize the signing certificate
|
|
run: |
|
|
APP="build/Fleet Desktop.app"
|
|
APPEX="$APP/Contents/$APPEX_REL_PATH"
|
|
|
|
# AMFI requires the signing certificate to be listed in the embedded
|
|
# profile's DeveloperCertificates, or it SIGKILLs the app at launch.
|
|
# codesign, Gatekeeper, and notarization all pass regardless — so
|
|
# without this check a profile cut against the wrong cert produces a
|
|
# signed, notarized pkg that silently won't launch. Even two certs from
|
|
# the same team will result in a broken, unusable app - they must be the
|
|
# same cert
|
|
check='import sys,plistlib,hashlib; pl=plistlib.loads(sys.stdin.buffer.read()); h=[hashlib.sha1(bytes(c)).hexdigest().upper() for c in pl.get("DeveloperCertificates",[])]; print(" authorizes:",h); sys.exit(0 if sys.argv[1].upper() in h else 1)'
|
|
for prof in "$APPEX/Contents/embedded.provisionprofile" "$APP/Contents/embedded.provisionprofile"; do
|
|
echo "Checking $prof"
|
|
if ! security cms -D -i "$prof" | python3 -c "$check" "$APPLICATION_SIGNING_IDENTITY_SHA1"; then
|
|
echo "::error::$prof does not authorize signing certificate $APPLICATION_SIGNING_IDENTITY_SHA1. AMFI will SIGKILL the app at launch (notarization does NOT catch this). Regenerate the Developer ID provisioning profile selecting that certificate."
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
- name: Code sign app and extension
|
|
run: |
|
|
APP="build/Fleet Desktop.app"
|
|
APPEX="$APP/Contents/$APPEX_REL_PATH"
|
|
|
|
# Sign inside-out: the embedded extension first, then the host app.
|
|
# Each bundle is sealed with its own entitlements + embedded profile.
|
|
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
|
|
--options runtime --timestamp "$APPEX/Contents/MacOS/FleetPSSOExtension"
|
|
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
|
|
--options runtime --timestamp \
|
|
--entitlements FleetPSSOExtension/FleetPSSOExtension.entitlements "$APPEX"
|
|
|
|
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
|
|
--options runtime --timestamp "$APP/Contents/MacOS/FleetDesktop"
|
|
codesign --force --sign "$APPLICATION_SIGNING_IDENTITY_SHA1" \
|
|
--options runtime --timestamp \
|
|
--entitlements FleetDesktop/FleetDesktop.entitlements "$APP"
|
|
|
|
codesign --verify --deep --strict --verbose=2 "$APP"
|
|
codesign --display --verbose=4 "$APP"
|
|
codesign --display --entitlements - "$APPEX"
|
|
|
|
- name: Rebuild pkg with signed app
|
|
run: |
|
|
# build-pkg.sh reuses the already-signed app (ditto preserves the
|
|
# signature and the embedded, signed appex).
|
|
./build-pkg.sh
|
|
|
|
- name: Sign pkg
|
|
run: |
|
|
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "FleetDesktop/Info.plist")
|
|
UNSIGNED_PKG="build/dist/fleet_desktop-v${VERSION}.pkg"
|
|
SIGNED_PKG="build/dist/fleet_desktop-v${VERSION}-signed.pkg"
|
|
|
|
if [ ! -f "$UNSIGNED_PKG" ]; then
|
|
echo "Error: package not found: $UNSIGNED_PKG"
|
|
ls -la build/dist/ || true
|
|
exit 1
|
|
fi
|
|
|
|
productsign --sign "$INSTALLER_SIGNING_IDENTITY_SHA1" --timestamp \
|
|
"$UNSIGNED_PKG" "$SIGNED_PKG"
|
|
mv "$SIGNED_PKG" "$UNSIGNED_PKG"
|
|
pkgutil --check-signature "$UNSIGNED_PKG"
|
|
|
|
- name: Notarize pkg
|
|
env:
|
|
AC_USERNAME: ${{ secrets.APPLE_USERNAME }}
|
|
AC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
AC_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "FleetDesktop/Info.plist")
|
|
PKG_PATH="build/dist/fleet_desktop-v${VERSION}.pkg"
|
|
|
|
SUBMISSION_OUTPUT=$(xcrun notarytool submit "$PKG_PATH" \
|
|
--apple-id "$AC_USERNAME" \
|
|
--password "$AC_PASSWORD" \
|
|
--team-id "$AC_TEAM_ID" \
|
|
--wait --timeout 30m 2>&1) || NOTARIZATION_FAILED=true
|
|
echo "$SUBMISSION_OUTPUT"
|
|
|
|
SUBMISSION_ID=$(echo "$SUBMISSION_OUTPUT" | grep -i "id:" | head -1 | awk '{print $NF}' | tr -d ',' || echo "")
|
|
STATUS=$(echo "$SUBMISSION_OUTPUT" | grep -i "status:" | tail -1 | awk '{print $NF}' || echo "")
|
|
|
|
# Fail closed: only an explicit "Accepted" passes. notarytool statuses
|
|
# are Accepted / In Progress / Invalid / Rejected — a broad grep for
|
|
# "failed|error" would let a "Rejected" submission slip through as
|
|
# success, and notarytool can exit 0 even on a rejected package.
|
|
if [ "${NOTARIZATION_FAILED:-false}" = "true" ] || [ "$STATUS" != "Accepted" ]; then
|
|
echo "::error::Notarization failed (status: ${STATUS:-unknown})"
|
|
if [ -n "$SUBMISSION_ID" ]; then
|
|
xcrun notarytool log "$SUBMISSION_ID" \
|
|
--apple-id "$AC_USERNAME" --password "$AC_PASSWORD" --team-id "$AC_TEAM_ID" || true
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
xcrun stapler staple "$PKG_PATH"
|
|
xcrun stapler validate "$PKG_PATH"
|
|
spctl --assess --type install --verbose "$PKG_PATH"
|
|
|
|
- name: Cleanup keychain
|
|
if: always()
|
|
run: security delete-keychain build.keychain || true
|
|
|
|
- name: Upload pkg artifact
|
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
|
with:
|
|
name: fleet_desktop-pkg
|
|
path: ./apps/fleet-desktop-macos/build/dist/fleet_desktop-v*.pkg
|
|
retention-days: 30
|
|
if-no-files-found: error
|