Ship signed universal macOS probo-agent pkg
Publish a notarized arm64+x86_64 .pkg from CI with the CGO tray binary, Probo Agent.app, and global LaunchAgent. Keep the LaunchDaemon enrollment-gated, align its plist path with the launchd label, and document the Apple signing secrets. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
100
.github/workflows/release-probo-agent.yaml
vendored
100
.github/workflows/release-probo-agent.yaml
vendored
@@ -20,8 +20,6 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- { goos: linux, goarch: amd64 }
|
- { goos: linux, goarch: amd64 }
|
||||||
- { goos: linux, goarch: arm64 }
|
- { goos: linux, goarch: arm64 }
|
||||||
- { goos: darwin, goarch: amd64 }
|
|
||||||
- { goos: darwin, goarch: arm64 }
|
|
||||||
- { goos: windows, goarch: amd64 }
|
- { goos: windows, goarch: amd64 }
|
||||||
- { goos: windows, goarch: arm64 }
|
- { goos: windows, goarch: arm64 }
|
||||||
- { goos: freebsd, goarch: amd64 }
|
- { goos: freebsd, goarch: amd64 }
|
||||||
@@ -53,7 +51,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
case "$GOOS" in
|
case "$GOOS" in
|
||||||
linux) OS="Linux" ;;
|
linux) OS="Linux" ;;
|
||||||
darwin) OS="Darwin" ;;
|
|
||||||
windows) OS="Windows" ;;
|
windows) OS="Windows" ;;
|
||||||
freebsd) OS="Freebsd" ;;
|
freebsd) OS="Freebsd" ;;
|
||||||
esac
|
esac
|
||||||
@@ -82,9 +79,96 @@ jobs:
|
|||||||
path: "archives/"
|
path: "archives/"
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
name: "macos (universal pkg + darwin archives)"
|
||||||
|
runs-on: "macos-26"
|
||||||
|
permissions:
|
||||||
|
contents: "read"
|
||||||
|
steps:
|
||||||
|
- uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- uses: "./.github/actions/setup"
|
||||||
|
with:
|
||||||
|
node: "false"
|
||||||
|
- name: "Select Xcode version"
|
||||||
|
run: |
|
||||||
|
sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
||||||
|
xcode-select -p
|
||||||
|
swift --version
|
||||||
|
- name: "Import signing certificate"
|
||||||
|
env:
|
||||||
|
CERTIFICATE_BASE64: "${{ secrets.APPLE_CERTIFICATE }}"
|
||||||
|
CERTIFICATE_PASSWORD: "${{ secrets.APPLE_CERTIFICATE_PASSWORD }}"
|
||||||
|
KEYCHAIN_PASSWORD: "${{ secrets.KEYCHAIN_PASSWORD }}"
|
||||||
|
run: |
|
||||||
|
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
echo "$CERTIFICATE_BASE64" | base64 --decode > certificate.p12
|
||||||
|
security import certificate.p12 -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||||
|
rm certificate.p12
|
||||||
|
- name: "Build CGO darwin binaries and universal pkg"
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "1"
|
||||||
|
CODESIGN_IDENTITY: "${{ secrets.CODESIGN_IDENTITY }}"
|
||||||
|
INSTALLER_IDENTITY: "${{ secrets.INSTALLER_IDENTITY }}"
|
||||||
|
APPLE_ID: "${{ secrets.APPLE_ID }}"
|
||||||
|
APPLE_ID_PASSWORD: "${{ secrets.APPLE_ID_PASSWORD }}"
|
||||||
|
APPLE_TEAM_ID: "${{ secrets.APPLE_TEAM_ID }}"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${GITHUB_REF_NAME##*/v}"
|
||||||
|
LDFLAGS="-s -w -X 'main.version=${VERSION}'"
|
||||||
|
|
||||||
|
mkdir -p dist archives staging
|
||||||
|
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" \
|
||||||
|
-gcflags="-e" -o dist/probo-agent_arm64 ./cmd/probo-agent
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" \
|
||||||
|
-gcflags="-e" -o dist/probo-agent_amd64 ./cmd/probo-agent
|
||||||
|
|
||||||
|
lipo -create \
|
||||||
|
dist/probo-agent_arm64 \
|
||||||
|
dist/probo-agent_amd64 \
|
||||||
|
-output dist/probo-agent_universal
|
||||||
|
|
||||||
|
for pair in "arm64:arm64" "amd64:x86_64"; do
|
||||||
|
GOARCH="${pair%%:*}"
|
||||||
|
ARCH="${pair##*:}"
|
||||||
|
AGENT_DIR="probo-agent_Darwin_${ARCH}"
|
||||||
|
mkdir -p "staging/${AGENT_DIR}"
|
||||||
|
cp "dist/probo-agent_${GOARCH}" "staging/${AGENT_DIR}/probo-agent"
|
||||||
|
cp README.md LICENSE "staging/${AGENT_DIR}/"
|
||||||
|
if [ -f cmd/probo-agent/CHANGELOG.md ]; then
|
||||||
|
cp cmd/probo-agent/CHANGELOG.md "staging/${AGENT_DIR}/"
|
||||||
|
fi
|
||||||
|
tar -czf "archives/${AGENT_DIR}.tar.gz" -C staging "${AGENT_DIR}"
|
||||||
|
done
|
||||||
|
|
||||||
|
chmod +x cmd/probo-agent/installer/macos/build.sh \
|
||||||
|
cmd/probo-agent/installer/macos/enroll-ui/build-app.sh \
|
||||||
|
cmd/probo-agent/installer/macos/scripts/preinstall \
|
||||||
|
cmd/probo-agent/installer/macos/scripts/postinstall
|
||||||
|
|
||||||
|
cmd/probo-agent/installer/macos/build.sh \
|
||||||
|
--binary dist/probo-agent_universal \
|
||||||
|
--arch universal \
|
||||||
|
--version "${VERSION}" \
|
||||||
|
--output "archives/probo-agent_${VERSION}_darwin_universal.pkg"
|
||||||
|
- uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7
|
||||||
|
with:
|
||||||
|
name: "archive-darwin-universal"
|
||||||
|
path: "archives/"
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
github-release:
|
github-release:
|
||||||
name: "github-release"
|
name: "github-release"
|
||||||
needs: [build-binary]
|
needs: [build-binary, build-macos]
|
||||||
runs-on: "runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache"
|
runs-on: "runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache"
|
||||||
permissions:
|
permissions:
|
||||||
contents: "write"
|
contents: "write"
|
||||||
@@ -104,7 +188,7 @@ jobs:
|
|||||||
- name: "Generate checksums and sign"
|
- name: "Generate checksums and sign"
|
||||||
run: |
|
run: |
|
||||||
cd archives
|
cd archives
|
||||||
sha256sum *.tar.gz *.zip > checksums.txt
|
sha256sum *.tar.gz *.zip *.pkg > checksums.txt
|
||||||
cosign sign-blob --bundle="checksums.txt.bundle" checksums.txt --yes
|
cosign sign-blob --bundle="checksums.txt.bundle" checksums.txt --yes
|
||||||
- name: "Inject release checksums into install.sh"
|
- name: "Inject release checksums into install.sh"
|
||||||
run: |
|
run: |
|
||||||
@@ -148,12 +232,12 @@ jobs:
|
|||||||
- name: "Attest SBOM for archives"
|
- name: "Attest SBOM for archives"
|
||||||
uses: "actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e" # v4
|
uses: "actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e" # v4
|
||||||
with:
|
with:
|
||||||
subject-path: "archives/*.tar.gz, archives/*.zip"
|
subject-path: "archives/*.tar.gz, archives/*.zip, archives/*.pkg"
|
||||||
sbom-path: "sbom.json"
|
sbom-path: "sbom.json"
|
||||||
- name: "Attest build provenance for archives"
|
- name: "Attest build provenance for archives"
|
||||||
uses: "actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373" # v4
|
uses: "actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373" # v4
|
||||||
with:
|
with:
|
||||||
subject-path: "archives/*.tar.gz, archives/*.zip"
|
subject-path: "archives/*.tar.gz, archives/*.zip, archives/*.pkg"
|
||||||
- name: "Extract release notes"
|
- name: "Extract release notes"
|
||||||
run: |
|
run: |
|
||||||
VERSION="${GITHUB_REF_NAME##*/v}"
|
VERSION="${GITHUB_REF_NAME##*/v}"
|
||||||
@@ -167,7 +251,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: "Create GitHub release"
|
- name: "Create GitHub release"
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
run: |
|
run: |
|
||||||
PRERELEASE_FLAG=""
|
PRERELEASE_FLAG=""
|
||||||
if echo "${GITHUB_REF_NAME}" | grep -qE '(alpha|beta|rc)'; then
|
if echo "${GITHUB_REF_NAME}" | grep -qE '(alpha|beta|rc)'; then
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
Placeholders are substituted by build.sh:
|
Placeholders are substituted by build.sh:
|
||||||
|
|
||||||
@@VERSION@@ agent version, e.g. 0.1.0
|
@@VERSION@@ agent version, e.g. 0.1.0
|
||||||
@@PKG_ARCH@@ payload pkgbuild architecture (x86_64 or arm64)
|
@@PKG_ARCH@@ reserved (legacy; host filter uses @@HOST_ARCHS@@)
|
||||||
@@HOST_ARCHS@@ host arch filter used by Installer.app
|
@@HOST_ARCHS@@ host arch filter used by Installer.app
|
||||||
|
(e.g. arm64 or arm64,x86_64 for universal)
|
||||||
-->
|
-->
|
||||||
<installer-gui-script minSpecVersion="2">
|
<installer-gui-script minSpecVersion="2">
|
||||||
<title>Probo Device Posture Agent @@VERSION@@</title>
|
<title>Probo Device Posture Agent @@VERSION@@</title>
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
<choice id="default"/>
|
<choice id="default"/>
|
||||||
<choice id="com.getprobo.agent"
|
<choice id="com.getprobo.agent"
|
||||||
title="Probo Device Posture Agent"
|
title="Probo Device Posture Agent"
|
||||||
description="Installs probo-agent to /usr/local/bin and the launchd unit to /Library/LaunchDaemons.">
|
description="Installs probo-agent, the menu bar helper LaunchAgent, and Probo Agent.app for probo:// enrollment. The LaunchDaemon starts after enrollment.">
|
||||||
<pkg-ref id="com.getprobo.agent"/>
|
<pkg-ref id="com.getprobo.agent"/>
|
||||||
</choice>
|
</choice>
|
||||||
</installer-gui-script>
|
</installer-gui-script>
|
||||||
|
|||||||
@@ -5,16 +5,34 @@
|
|||||||
#
|
#
|
||||||
# Required arguments:
|
# Required arguments:
|
||||||
# --binary PATH Path to a compiled probo-agent binary.
|
# --binary PATH Path to a compiled probo-agent binary.
|
||||||
# --arch ARCH Target architecture: amd64 or arm64.
|
# --arch ARCH Target architecture: amd64, arm64, or universal.
|
||||||
# --version VER Agent version, e.g. 0.1.0. Defaults to the
|
# --version VER Agent version, e.g. 0.1.0. Defaults to the
|
||||||
# content of cmd/probo-agent/VERSION.
|
# content of cmd/probo-agent/VERSION.
|
||||||
# --output PATH Output .pkg path. Defaults to
|
# --output PATH Output .pkg path. Defaults to
|
||||||
# dist/probo-agent_${VER}_${OS}.pkg.
|
# dist/probo-agent_${VER}_darwin_${ARCH}.pkg.
|
||||||
#
|
#
|
||||||
# The resulting flat distribution package is unsigned. Apple
|
# Optional environment variables (auditor-mode compatible):
|
||||||
# Developer ID signing + notarization are out of scope for this
|
# CODESIGN_IDENTITY Developer ID Application identity. When
|
||||||
# script; consumers can chain `productsign` and `xcrun notarytool`
|
# set, signs the agent binary and Probo
|
||||||
# afterwards.
|
# Agent.app with hardened runtime before
|
||||||
|
# packaging.
|
||||||
|
# INSTALLER_IDENTITY Developer ID Installer identity. When
|
||||||
|
# set, passes --sign to productbuild.
|
||||||
|
# APPLE_ID Apple ID for notarytool store-credentials.
|
||||||
|
# APPLE_ID_PASSWORD App-specific password; used only to
|
||||||
|
# populate a keychain profile (not passed
|
||||||
|
# to long-lived notarytool submit).
|
||||||
|
# APPLE_TEAM_ID Apple Developer Team ID.
|
||||||
|
# NOTARYTOOL_KEYCHAIN_PROFILE Existing notarytool keychain profile.
|
||||||
|
# Defaults to probo-agent-notary when
|
||||||
|
# storing from APPLE_ID / APPLE_ID_PASSWORD.
|
||||||
|
#
|
||||||
|
# Notarization is enabled when NOTARYTOOL_KEYCHAIN_PROFILE is set, or
|
||||||
|
# when APPLE_ID and APPLE_ID_PASSWORD are both set (APPLE_TEAM_ID also
|
||||||
|
# required). CODESIGN_IDENTITY and INSTALLER_IDENTITY are then required.
|
||||||
|
# The script stores credentials into the keychain profile when a
|
||||||
|
# password is provided, then notarizes and staples the .app before
|
||||||
|
# packaging and the signed .pkg via --keychain-profile.
|
||||||
#
|
#
|
||||||
# Must run on macOS: pkgbuild, productbuild, and swift build are
|
# Must run on macOS: pkgbuild, productbuild, and swift build are
|
||||||
# Apple-only tools. The build also compiles Probo Agent.app (the
|
# Apple-only tools. The build also compiles Probo Agent.app (the
|
||||||
@@ -30,6 +48,12 @@ ARCH=""
|
|||||||
VERSION=""
|
VERSION=""
|
||||||
OUTPUT=""
|
OUTPUT=""
|
||||||
IDENTIFIER="com.getprobo.agent"
|
IDENTIFIER="com.getprobo.agent"
|
||||||
|
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:-}"
|
||||||
|
INSTALLER_IDENTITY="${INSTALLER_IDENTITY:-}"
|
||||||
|
APPLE_ID="${APPLE_ID:-}"
|
||||||
|
APPLE_ID_PASSWORD="${APPLE_ID_PASSWORD:-}"
|
||||||
|
APPLE_TEAM_ID="${APPLE_TEAM_ID:-}"
|
||||||
|
NOTARYTOOL_KEYCHAIN_PROFILE="${NOTARYTOOL_KEYCHAIN_PROFILE:-}"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
sed -ne '/^#/!q; s/^# \{0,1\}//; 2,$ p' < "$0"
|
sed -ne '/^#/!q; s/^# \{0,1\}//; 2,$ p' < "$0"
|
||||||
@@ -52,17 +76,60 @@ if [ -z "${BINARY}" ] || [ ! -x "${BINARY}" ]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
case "${ARCH}" in
|
case "${ARCH}" in
|
||||||
amd64) PKG_ARCH="x86_64" ;;
|
amd64)
|
||||||
arm64) PKG_ARCH="arm64" ;;
|
PKG_ARCH="x86_64"
|
||||||
"") echo "error: --arch (amd64|arm64) is required" >&2; exit 2 ;;
|
HOST_ARCHS="x86_64"
|
||||||
*) echo "error: unsupported --arch '${ARCH}' (want amd64 or arm64)" >&2; exit 2 ;;
|
OUTPUT_ARCH="x86_64"
|
||||||
|
;;
|
||||||
|
arm64)
|
||||||
|
PKG_ARCH="arm64"
|
||||||
|
HOST_ARCHS="arm64"
|
||||||
|
OUTPUT_ARCH="arm64"
|
||||||
|
;;
|
||||||
|
universal)
|
||||||
|
PKG_ARCH="arm64"
|
||||||
|
HOST_ARCHS="arm64,x86_64"
|
||||||
|
OUTPUT_ARCH="universal"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
echo "error: --arch (amd64|arm64|universal) is required" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "error: unsupported --arch '${ARCH}' (want amd64, arm64, or universal)" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# --arch universal advertises both hostArchitectures in Distribution.xml.
|
||||||
|
# Refuse a single-slice binary so Installer cannot install on a CPU the
|
||||||
|
# agent cannot run on.
|
||||||
|
if [ "${ARCH}" = "universal" ]; then
|
||||||
|
if ! command -v lipo >/dev/null 2>&1; then
|
||||||
|
echo "error: lipo is required to validate a universal --binary (run on macOS)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BINARY_ARCHS="$(lipo -archs "${BINARY}")"
|
||||||
|
has_arm64=false
|
||||||
|
has_x86_64=false
|
||||||
|
for arch_slice in ${BINARY_ARCHS}; do
|
||||||
|
case "${arch_slice}" in
|
||||||
|
arm64) has_arm64=true ;;
|
||||||
|
x86_64) has_x86_64=true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ "${has_arm64}" != true ] || [ "${has_x86_64}" != true ]; then
|
||||||
|
echo "error: --arch universal requires a fat binary with arm64 and x86_64 slices (got: ${BINARY_ARCHS}); use lipo -create" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "${VERSION}" ]; then
|
if [ -z "${VERSION}" ]; then
|
||||||
VERSION="$(cat "${REPO_ROOT}/cmd/probo-agent/VERSION")"
|
VERSION="$(cat "${REPO_ROOT}/cmd/probo-agent/VERSION")"
|
||||||
fi
|
fi
|
||||||
if [ -z "${OUTPUT}" ]; then
|
if [ -z "${OUTPUT}" ]; then
|
||||||
mkdir -p "${REPO_ROOT}/dist"
|
mkdir -p "${REPO_ROOT}/dist"
|
||||||
OUTPUT="${REPO_ROOT}/dist/probo-agent_${VERSION}_darwin_${PKG_ARCH}.pkg"
|
OUTPUT="${REPO_ROOT}/dist/probo-agent_${VERSION}_darwin_${OUTPUT_ARCH}.pkg"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v pkgbuild >/dev/null 2>&1 || ! command -v productbuild >/dev/null 2>&1; then
|
if ! command -v pkgbuild >/dev/null 2>&1 || ! command -v productbuild >/dev/null 2>&1; then
|
||||||
@@ -74,6 +141,98 @@ if ! command -v swift >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
notarize_enabled=false
|
||||||
|
if [ -n "${NOTARYTOOL_KEYCHAIN_PROFILE}" ]; then
|
||||||
|
notarize_enabled=true
|
||||||
|
elif [ -n "${APPLE_ID}" ] && [ -n "${APPLE_ID_PASSWORD}" ] && [ -n "${APPLE_TEAM_ID}" ]; then
|
||||||
|
NOTARYTOOL_KEYCHAIN_PROFILE="probo-agent-notary"
|
||||||
|
notarize_enabled=true
|
||||||
|
fi
|
||||||
|
if [ "${notarize_enabled}" = true ]; then
|
||||||
|
if [ -z "${CODESIGN_IDENTITY}" ]; then
|
||||||
|
echo "error: notarization requires CODESIGN_IDENTITY" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [ -z "${INSTALLER_IDENTITY}" ]; then
|
||||||
|
echo "error: notarization requires INSTALLER_IDENTITY" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sign_macho() {
|
||||||
|
local path="$1"
|
||||||
|
if [ -z "${CODESIGN_IDENTITY}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
codesign \
|
||||||
|
--force \
|
||||||
|
--options runtime \
|
||||||
|
--timestamp \
|
||||||
|
--sign "${CODESIGN_IDENTITY}" \
|
||||||
|
"${path}"
|
||||||
|
codesign --verify --verbose=2 "${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_app_bundle() {
|
||||||
|
local app_path="$1"
|
||||||
|
if [ -z "${CODESIGN_IDENTITY}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
codesign \
|
||||||
|
--force \
|
||||||
|
--options runtime \
|
||||||
|
--timestamp \
|
||||||
|
--sign "${CODESIGN_IDENTITY}" \
|
||||||
|
"${app_path}/Contents/MacOS/probo-agent-url-handler"
|
||||||
|
codesign \
|
||||||
|
--force \
|
||||||
|
--options runtime \
|
||||||
|
--timestamp \
|
||||||
|
--sign "${CODESIGN_IDENTITY}" \
|
||||||
|
"${app_path}"
|
||||||
|
codesign --verify --verbose=2 "${app_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_notarytool_credentials() {
|
||||||
|
if [ -z "${APPLE_ID_PASSWORD}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -z "${APPLE_ID}" ]; then
|
||||||
|
echo "error: APPLE_ID_PASSWORD requires APPLE_ID to store notarytool credentials" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
# Password appears on argv only for this short-lived store. Submits
|
||||||
|
# use --keychain-profile so concurrent processes cannot read it.
|
||||||
|
xcrun notarytool store-credentials "${NOTARYTOOL_KEYCHAIN_PROFILE}" \
|
||||||
|
--apple-id "${APPLE_ID}" \
|
||||||
|
--password "${APPLE_ID_PASSWORD}" \
|
||||||
|
--team-id "${APPLE_TEAM_ID}"
|
||||||
|
}
|
||||||
|
|
||||||
|
notarytool_submit() {
|
||||||
|
local path="$1"
|
||||||
|
xcrun notarytool submit "${path}" \
|
||||||
|
--keychain-profile "${NOTARYTOOL_KEYCHAIN_PROFILE}" \
|
||||||
|
--wait
|
||||||
|
}
|
||||||
|
|
||||||
|
notarize_and_staple_app() {
|
||||||
|
local app_path="$1"
|
||||||
|
local zip_path
|
||||||
|
|
||||||
|
zip_path="${STAGE}/probo-agent-app.zip"
|
||||||
|
ditto -c -k --keepParent "${app_path}" "${zip_path}"
|
||||||
|
notarytool_submit "${zip_path}"
|
||||||
|
rm -f "${zip_path}"
|
||||||
|
xcrun stapler staple "${app_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
notarize_and_staple_pkg() {
|
||||||
|
local pkg_path="$1"
|
||||||
|
notarytool_submit "${pkg_path}"
|
||||||
|
xcrun stapler staple "${pkg_path}"
|
||||||
|
}
|
||||||
|
|
||||||
STAGE="$(mktemp -d -t probo-agent-pkg)"
|
STAGE="$(mktemp -d -t probo-agent-pkg)"
|
||||||
trap 'rm -rf "${STAGE}"' EXIT
|
trap 'rm -rf "${STAGE}"' EXIT
|
||||||
|
|
||||||
@@ -83,6 +242,7 @@ RESOURCES="${STAGE}/Resources"
|
|||||||
mkdir -p "${PAYLOAD}/usr/local/bin" "${SCRIPTS}" "${RESOURCES}"
|
mkdir -p "${PAYLOAD}/usr/local/bin" "${SCRIPTS}" "${RESOURCES}"
|
||||||
|
|
||||||
install -m 0755 "${BINARY}" "${PAYLOAD}/usr/local/bin/probo-agent"
|
install -m 0755 "${BINARY}" "${PAYLOAD}/usr/local/bin/probo-agent"
|
||||||
|
sign_macho "${PAYLOAD}/usr/local/bin/probo-agent"
|
||||||
|
|
||||||
mkdir -p "${PAYLOAD}/Applications"
|
mkdir -p "${PAYLOAD}/Applications"
|
||||||
"${SCRIPT_DIR}/enroll-ui/build-app.sh" \
|
"${SCRIPT_DIR}/enroll-ui/build-app.sh" \
|
||||||
@@ -90,11 +250,34 @@ mkdir -p "${PAYLOAD}/Applications"
|
|||||||
--version "${VERSION}" \
|
--version "${VERSION}" \
|
||||||
--output "${PAYLOAD}/Applications"
|
--output "${PAYLOAD}/Applications"
|
||||||
|
|
||||||
install -m 0755 "${SCRIPT_DIR}/scripts/postinstall" "${SCRIPTS}/postinstall"
|
APP_PATH="${PAYLOAD}/Applications/Probo Agent.app"
|
||||||
|
sign_app_bundle "${APP_PATH}"
|
||||||
|
|
||||||
cp "${SCRIPT_DIR}/Resources/welcome.html" "${RESOURCES}/welcome.html"
|
if [ "${notarize_enabled}" = true ]; then
|
||||||
cp "${SCRIPT_DIR}/Resources/conclusion.html" "${RESOURCES}/conclusion.html"
|
ensure_notarytool_credentials
|
||||||
cp "${REPO_ROOT}/LICENSE" "${RESOURCES}/license.txt"
|
echo "Notarizing Probo Agent.app before packaging..."
|
||||||
|
notarize_and_staple_app "${APP_PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Avoid AppleDouble (._*) and resource-fork noise in the package.
|
||||||
|
export COPYFILE_DISABLE=1
|
||||||
|
|
||||||
|
# ditto --norsrc/--noextattr copies without resource forks / xattrs.
|
||||||
|
ditto --norsrc --noextattr "${SCRIPT_DIR}/scripts/preinstall" "${SCRIPTS}/preinstall"
|
||||||
|
ditto --norsrc --noextattr "${SCRIPT_DIR}/scripts/postinstall" "${SCRIPTS}/postinstall"
|
||||||
|
ditto --norsrc --noextattr \
|
||||||
|
"${REPO_ROOT}/pkg/deviceagent/tray/launchagent.plist.tmpl" \
|
||||||
|
"${SCRIPTS}/launchagent.plist.tmpl"
|
||||||
|
chmod 0755 "${SCRIPTS}/preinstall" "${SCRIPTS}/postinstall"
|
||||||
|
chmod 0644 "${SCRIPTS}/launchagent.plist.tmpl"
|
||||||
|
|
||||||
|
ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/welcome.html" "${RESOURCES}/welcome.html"
|
||||||
|
ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/conclusion.html" "${RESOURCES}/conclusion.html"
|
||||||
|
ditto --norsrc --noextattr "${REPO_ROOT}/LICENSE" "${RESOURCES}/license.txt"
|
||||||
|
|
||||||
|
# Strip any xattrs that tools may have reattached (codesign, etc.).
|
||||||
|
xattr -cr "${PAYLOAD}" "${SCRIPTS}" "${RESOURCES}" 2>/dev/null || true
|
||||||
|
find "${PAYLOAD}" "${SCRIPTS}" "${RESOURCES}" -name '._*' -delete 2>/dev/null || true
|
||||||
|
|
||||||
# Component package: payload + scripts only.
|
# Component package: payload + scripts only.
|
||||||
COMPONENT_PKG="${STAGE}/probo-agent-component.pkg"
|
COMPONENT_PKG="${STAGE}/probo-agent-component.pkg"
|
||||||
@@ -106,19 +289,63 @@ pkgbuild \
|
|||||||
--install-location "/" \
|
--install-location "/" \
|
||||||
"${COMPONENT_PKG}"
|
"${COMPONENT_PKG}"
|
||||||
|
|
||||||
|
# pkgbuild records protected com.apple.provenance xattrs as empty
|
||||||
|
# AppleDouble (._*) Bom entries. Rewrite the Bom with mkbom so the
|
||||||
|
# installer does not lay down those stubs next to real files.
|
||||||
|
rewrite_component_bom() {
|
||||||
|
local pkg="$1"
|
||||||
|
local expand_dir root_dir flat_pkg
|
||||||
|
|
||||||
|
expand_dir="${STAGE}/component-expand"
|
||||||
|
root_dir="${STAGE}/component-root"
|
||||||
|
flat_pkg="${STAGE}/probo-agent-component-clean.pkg"
|
||||||
|
rm -rf "${expand_dir}" "${root_dir}" "${flat_pkg}"
|
||||||
|
# pkgutil --expand creates the destination directory itself.
|
||||||
|
pkgutil --expand "${pkg}" "${expand_dir}"
|
||||||
|
find "${expand_dir}/Scripts" -name '._*' -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
mkdir -p "${root_dir}"
|
||||||
|
(
|
||||||
|
cd "${root_dir}"
|
||||||
|
gzip -dc "${expand_dir}/Payload" | cpio -idmu 2>/dev/null
|
||||||
|
)
|
||||||
|
find "${root_dir}" -name '._*' -delete 2>/dev/null || true
|
||||||
|
mkbom "${root_dir}" "${expand_dir}/Bom"
|
||||||
|
if lsbom "${expand_dir}/Bom" | grep -q '/\._'; then
|
||||||
|
echo "error: rewritten Bom still contains AppleDouble entries" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
pkgutil --flatten "${expand_dir}" "${flat_pkg}"
|
||||||
|
mv "${flat_pkg}" "${pkg}"
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite_component_bom "${COMPONENT_PKG}"
|
||||||
|
|
||||||
# Render Distribution.xml from its template.
|
# Render Distribution.xml from its template.
|
||||||
DISTRIBUTION="${STAGE}/Distribution.xml"
|
DISTRIBUTION="${STAGE}/Distribution.xml"
|
||||||
sed \
|
sed \
|
||||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||||
-e "s|@@PKG_ARCH@@|${PKG_ARCH}|g" \
|
-e "s|@@PKG_ARCH@@|${PKG_ARCH}|g" \
|
||||||
-e "s|@@HOST_ARCHS@@|${PKG_ARCH}|g" \
|
-e "s|@@HOST_ARCHS@@|${HOST_ARCHS}|g" \
|
||||||
"${SCRIPT_DIR}/Distribution.xml.tmpl" > "${DISTRIBUTION}"
|
"${SCRIPT_DIR}/Distribution.xml.tmpl" > "${DISTRIBUTION}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "${OUTPUT}")"
|
mkdir -p "$(dirname "${OUTPUT}")"
|
||||||
productbuild \
|
|
||||||
--distribution "${DISTRIBUTION}" \
|
PRODUCTBUILD_ARGS=(
|
||||||
--package-path "${STAGE}" \
|
--distribution "${DISTRIBUTION}"
|
||||||
--resources "${RESOURCES}" \
|
--package-path "${STAGE}"
|
||||||
"${OUTPUT}"
|
--resources "${RESOURCES}"
|
||||||
|
)
|
||||||
|
if [ -n "${INSTALLER_IDENTITY}" ]; then
|
||||||
|
PRODUCTBUILD_ARGS+=(--sign "${INSTALLER_IDENTITY}")
|
||||||
|
fi
|
||||||
|
PRODUCTBUILD_ARGS+=("${OUTPUT}")
|
||||||
|
|
||||||
|
productbuild "${PRODUCTBUILD_ARGS[@]}"
|
||||||
|
|
||||||
|
if [ "${notarize_enabled}" = true ]; then
|
||||||
|
echo "Notarizing ${OUTPUT}..."
|
||||||
|
notarize_and_staple_pkg "${OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Built ${OUTPUT}"
|
echo "Built ${OUTPUT}"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# the probo:// URL scheme and forwards enrollment links to probo-agent.
|
# the probo:// URL scheme and forwards enrollment links to probo-agent.
|
||||||
#
|
#
|
||||||
# Required arguments:
|
# Required arguments:
|
||||||
# --arch amd64 or arm64
|
# --arch amd64, arm64, or universal
|
||||||
# --version Agent version, e.g. 0.1.0
|
# --version Agent version, e.g. 0.1.0
|
||||||
# --output Parent directory; creates "Probo Agent.app" inside it
|
# --output Parent directory; creates "Probo Agent.app" inside it
|
||||||
#
|
#
|
||||||
@@ -35,13 +35,17 @@ while [ $# -gt 0 ]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "${ARCH}" ]; then
|
if [ -z "${ARCH}" ]; then
|
||||||
echo "error: --arch (amd64|arm64) is required" >&2
|
echo "error: --arch (amd64|arm64|universal) is required" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
case "${ARCH}" in
|
case "${ARCH}" in
|
||||||
amd64) SWIFT_ARCH="x86_64" ;;
|
amd64) SWIFT_ARCH_ARGS=(--arch x86_64) ;;
|
||||||
arm64) SWIFT_ARCH="arm64" ;;
|
arm64) SWIFT_ARCH_ARGS=(--arch arm64) ;;
|
||||||
*) echo "error: unsupported --arch '${ARCH}' (want amd64 or arm64)" >&2; exit 2 ;;
|
universal) SWIFT_ARCH_ARGS=(--arch arm64 --arch x86_64) ;;
|
||||||
|
*)
|
||||||
|
echo "error: unsupported --arch '${ARCH}' (want amd64, arm64, or universal)" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
if [ -z "${VERSION}" ]; then
|
if [ -z "${VERSION}" ]; then
|
||||||
echo "error: --version is required" >&2
|
echo "error: --version is required" >&2
|
||||||
@@ -61,8 +65,8 @@ BUILD_DIR="$(mktemp -d -t probo-agent-url-handler-build)"
|
|||||||
trap 'rm -rf "${BUILD_DIR}"' EXIT
|
trap 'rm -rf "${BUILD_DIR}"' EXIT
|
||||||
|
|
||||||
pushd "${SCRIPT_DIR}" >/dev/null
|
pushd "${SCRIPT_DIR}" >/dev/null
|
||||||
swift build -c release --arch "${SWIFT_ARCH}" --scratch-path "${BUILD_DIR}"
|
swift build -c release "${SWIFT_ARCH_ARGS[@]}" --scratch-path "${BUILD_DIR}"
|
||||||
BIN_DIR="$(swift build -c release --arch "${SWIFT_ARCH}" --scratch-path "${BUILD_DIR}" --show-bin-path)"
|
BIN_DIR="$(swift build -c release "${SWIFT_ARCH_ARGS[@]}" --scratch-path "${BUILD_DIR}" --show-bin-path)"
|
||||||
BINARY="${BIN_DIR}/${EXECUTABLE_NAME}"
|
BINARY="${BIN_DIR}/${EXECUTABLE_NAME}"
|
||||||
popd >/dev/null
|
popd >/dev/null
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ BINARY="/usr/local/bin/probo-agent"
|
|||||||
STATE_DIR="/var/lib/probo-agent"
|
STATE_DIR="/var/lib/probo-agent"
|
||||||
RUN_DIR="/var/run/probo-agent"
|
RUN_DIR="/var/run/probo-agent"
|
||||||
CONF_FILE="/tmp/probo-agent.conf"
|
CONF_FILE="/tmp/probo-agent.conf"
|
||||||
|
DAEMON_PLIST="/Library/LaunchDaemons/com.probo.agent.plist"
|
||||||
TRAY_LABEL="com.probo.agent.tray"
|
TRAY_LABEL="com.probo.agent.tray"
|
||||||
TRAY_PLIST_NAME="${TRAY_LABEL}.plist"
|
TRAY_PLIST_NAME="${TRAY_LABEL}.plist"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
TRAY_PLIST_TMPL="${SCRIPT_DIR}/launchagent.plist.tmpl"
|
||||||
|
|
||||||
# Mirror everything to the install log. We keep stdout/stderr open
|
# Mirror everything to the install log. We keep stdout/stderr open
|
||||||
# too so failures still surface in macOS Installer.app's log pane.
|
# too so failures still surface in macOS Installer.app's log pane.
|
||||||
@@ -46,35 +49,33 @@ mkdir -p "${RUN_DIR}"
|
|||||||
chown root:wheel "${RUN_DIR}"
|
chown root:wheel "${RUN_DIR}"
|
||||||
chmod 0755 "${RUN_DIR}"
|
chmod 0755 "${RUN_DIR}"
|
||||||
|
|
||||||
|
# Render the shared Go LaunchAgent template (pkg/deviceagent/tray/
|
||||||
|
# launchagent.plist.tmpl) with fixed install paths. Values are
|
||||||
|
# installer constants, so XML metacharacters are not expected.
|
||||||
|
render_tray_plist() {
|
||||||
|
local tmpl="$1"
|
||||||
|
local out="$2"
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e "s|{{xml \.Label}}|${TRAY_LABEL}|g" \
|
||||||
|
-e "s|{{xml \.ExePath}}|${BINARY}|g" \
|
||||||
|
-e "s|{{xml \.RunDir}}|${RUN_DIR}|g" \
|
||||||
|
"${tmpl}" > "${out}"
|
||||||
|
}
|
||||||
|
|
||||||
register_tray_launchagent() {
|
register_tray_launchagent() {
|
||||||
local current_user user_uid agents_dir plist_path
|
local current_user user_uid agents_dir plist_path
|
||||||
|
|
||||||
agents_dir="/Library/LaunchAgents"
|
agents_dir="/Library/LaunchAgents"
|
||||||
plist_path="${agents_dir}/${TRAY_PLIST_NAME}"
|
plist_path="${agents_dir}/${TRAY_PLIST_NAME}"
|
||||||
|
|
||||||
|
if [ ! -f "${TRAY_PLIST_TMPL}" ]; then
|
||||||
|
echo "error: tray LaunchAgent template missing at ${TRAY_PLIST_TMPL}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "${agents_dir}"
|
mkdir -p "${agents_dir}"
|
||||||
cat > "${plist_path}" <<EOF
|
render_tray_plist "${TRAY_PLIST_TMPL}" "${plist_path}"
|
||||||
<?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>Label</key>
|
|
||||||
<string>${TRAY_LABEL}</string>
|
|
||||||
<key>ProgramArguments</key>
|
|
||||||
<array>
|
|
||||||
<string>${BINARY}</string>
|
|
||||||
<string>tray</string>
|
|
||||||
<string>--run-dir</string>
|
|
||||||
<string>${RUN_DIR}</string>
|
|
||||||
</array>
|
|
||||||
<key>RunAtLoad</key>
|
|
||||||
<true/>
|
|
||||||
<key>KeepAlive</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 0644 "${plist_path}"
|
chmod 0644 "${plist_path}"
|
||||||
echo "Installed tray LaunchAgent at ${plist_path}."
|
echo "Installed tray LaunchAgent at ${plist_path}."
|
||||||
@@ -154,6 +155,24 @@ register_enrollment_url_scheme() {
|
|||||||
echo "Registered probo:// URL scheme."
|
echo "Registered probo:// URL scheme."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Restart a previously enrolled LaunchDaemon after upgrades. Preinstall
|
||||||
|
# boots it out so the binary can be replaced; without /tmp/probo-agent.conf
|
||||||
|
# enrollment is skipped and nothing else would load it again.
|
||||||
|
restart_existing_daemon() {
|
||||||
|
if [ ! -f "${DAEMON_PLIST}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
launchctl bootout system "${DAEMON_PLIST}" 2>/dev/null || true
|
||||||
|
if ! launchctl bootstrap system "${DAEMON_PLIST}"; then
|
||||||
|
echo "warning: could not start LaunchDaemon at ${DAEMON_PLIST}; it may start after reboot."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Started LaunchDaemon at ${DAEMON_PLIST}."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# An admin (or MDM) may stage /tmp/probo-agent.conf to drive an
|
# An admin (or MDM) may stage /tmp/probo-agent.conf to drive an
|
||||||
# unattended enrollment. Recognized keys (shell-style):
|
# unattended enrollment. Recognized keys (shell-style):
|
||||||
#
|
#
|
||||||
@@ -227,6 +246,7 @@ else
|
|||||||
echo "No ${CONF_FILE} found; enrollment can be completed from the menu bar icon."
|
echo "No ${CONF_FILE} found; enrollment can be completed from the menu bar icon."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
restart_existing_daemon
|
||||||
register_tray_launchagent
|
register_tray_launchagent
|
||||||
register_enrollment_url_scheme
|
register_enrollment_url_scheme
|
||||||
|
|
||||||
|
|||||||
59
cmd/probo-agent/installer/macos/scripts/preinstall
Normal file → Executable file
59
cmd/probo-agent/installer/macos/scripts/preinstall
Normal file → Executable file
@@ -2,8 +2,61 @@
|
|||||||
#
|
#
|
||||||
# probo-agent macOS PKG preinstall script.
|
# probo-agent macOS PKG preinstall script.
|
||||||
#
|
#
|
||||||
# Enrollment is handled by the menu bar helper after installation.
|
# Runs as root before the payload is laid down. Used to stop previous
|
||||||
# MDM may still pre-stage /tmp/probo-agent.conf for unattended
|
# LaunchAgent / LaunchDaemon instances so upgrades replace cleanly.
|
||||||
# enrollment in postinstall.
|
# Failures here are non-fatal: a stuck launchctl must not block install.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
LOG_FILE="/var/log/probo-agent-install.log"
|
||||||
|
TRAY_LABEL="com.probo.agent.tray"
|
||||||
|
TRAY_PLIST="/Library/LaunchAgents/${TRAY_LABEL}.plist"
|
||||||
|
DAEMON_PLIST="/Library/LaunchDaemons/com.probo.agent.plist"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${LOG_FILE}")"
|
||||||
|
exec > >(tee -a "${LOG_FILE}") 2>&1
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== probo-agent preinstall $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
|
||||||
|
|
||||||
|
bootout_tray_for_user() {
|
||||||
|
local username="$1"
|
||||||
|
local user_uid
|
||||||
|
|
||||||
|
if [ -z "${username}" ] || \
|
||||||
|
[ "${username}" = "root" ] || \
|
||||||
|
[ "${username}" = "loginwindow" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
user_uid="$(id -u "${username}" 2>/dev/null || true)"
|
||||||
|
if [ -z "${user_uid}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
launchctl bootout "gui/${user_uid}/${TRAY_LABEL}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
seen_users=" "
|
||||||
|
for username in $(users 2>/dev/null || true); do
|
||||||
|
case "${seen_users}" in
|
||||||
|
*" ${username} "*) continue ;;
|
||||||
|
esac
|
||||||
|
seen_users="${seen_users}${username} "
|
||||||
|
bootout_tray_for_user "${username}"
|
||||||
|
done
|
||||||
|
|
||||||
|
console_user=$(stat -f "%Su" /dev/console 2>/dev/null || true)
|
||||||
|
bootout_tray_for_user "${console_user}"
|
||||||
|
|
||||||
|
if [ -f "${DAEMON_PLIST}" ]; then
|
||||||
|
launchctl bootout system "${DAEMON_PLIST}" 2>/dev/null || true
|
||||||
|
echo "Booted out LaunchDaemon at ${DAEMON_PLIST}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${TRAY_PLIST}" ]; then
|
||||||
|
echo "Existing tray LaunchAgent will be replaced by postinstall."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== preinstall done ==="
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ stay pure Go (no tray).
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
CI builds binaries for 8 OS/arch targets (linux, darwin, and windows on
|
CI builds binaries for linux, windows, and freebsd (amd64/arm64) on
|
||||||
amd64 and arm64; freebsd on amd64 and arm64), publishes a GitHub
|
Linux runners, and builds **CGO-enabled** darwin archives plus a
|
||||||
Release with signed checksums, SBOM, and build attestations. The agent
|
signed/notarized **universal** `.pkg` on a macOS runner. The GitHub
|
||||||
|
Release includes those archives, `probo-agent_*_darwin_universal.pkg`,
|
||||||
|
`install.sh`, signed checksums, SBOM, and build attestations. The agent
|
||||||
auto-update path downloads the matching archive plus `checksums.txt` and
|
auto-update path downloads the matching archive plus `checksums.txt` and
|
||||||
verifies the cosign bundle before installing.
|
verifies the cosign bundle before installing.
|
||||||
|
|
||||||
@@ -45,14 +47,65 @@ Linux and FreeBSD use `probo-agent install --server …
|
|||||||
--enrollment-token …` from the shell, or the curl-to-sh installer
|
--enrollment-token …` from the shell, or the curl-to-sh installer
|
||||||
documented below. Windows release binaries are
|
documented below. Windows release binaries are
|
||||||
cross-compiled from Linux with MinGW (CGO). macOS release binaries and
|
cross-compiled from Linux with MinGW (CGO). macOS release binaries and
|
||||||
`.pkg` installers must be built on macOS with `CGO_ENABLED=1`.
|
the `.pkg` are built on macOS with `CGO_ENABLED=1`.
|
||||||
|
|
||||||
macOS `.pkg` installers are built locally with
|
### macOS `.pkg` (MDM / GUI install)
|
||||||
|
|
||||||
|
Release and local builds use
|
||||||
`cmd/probo-agent/installer/macos/build.sh` (requires macOS, a
|
`cmd/probo-agent/installer/macos/build.sh` (requires macOS, a
|
||||||
pre-built binary, and the Swift toolchain). The script also compiles
|
pre-built binary — preferably universal via `lipo` — and the Swift
|
||||||
`Probo Agent.app` — the headless `probo://` URL handler installed to
|
toolchain). The script compiles `Probo Agent.app` (the headless
|
||||||
`/Applications` — from `cmd/probo-agent/installer/macos/enroll-ui/`.
|
`probo://` URL handler) from
|
||||||
They are not part of the GitHub Release workflow yet.
|
`cmd/probo-agent/installer/macos/enroll-ui/`, signs the binary and app
|
||||||
|
when `CODESIGN_IDENTITY` is set, signs the product with
|
||||||
|
`INSTALLER_IDENTITY`, and notarizes/staples when
|
||||||
|
`NOTARYTOOL_KEYCHAIN_PROFILE` is set, or when `APPLE_ID`,
|
||||||
|
`APPLE_ID_PASSWORD`, and `APPLE_TEAM_ID` are set (password is stored
|
||||||
|
into a keychain profile; submits use `--keychain-profile` so the secret
|
||||||
|
is not on `notarytool submit` argv).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Local unsigned universal pkg (example)
|
||||||
|
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -o dist/probo-agent_arm64 ./cmd/probo-agent
|
||||||
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 go build -o dist/probo-agent_amd64 ./cmd/probo-agent
|
||||||
|
lipo -create dist/probo-agent_arm64 dist/probo-agent_amd64 -output dist/probo-agent_universal
|
||||||
|
cmd/probo-agent/installer/macos/build.sh \
|
||||||
|
--binary dist/probo-agent_universal \
|
||||||
|
--arch universal \
|
||||||
|
--version "$(cat cmd/probo-agent/VERSION)"
|
||||||
|
```
|
||||||
|
|
||||||
|
PKG postinstall always installs the global tray LaunchAgent and
|
||||||
|
registers `probo://`. The LaunchDaemon for `probo-agent run` is created
|
||||||
|
only after enrollment (`probo-agent install`, deep link, or MDM
|
||||||
|
`/tmp/probo-agent.conf`).
|
||||||
|
|
||||||
|
### Apple signing secrets (GitHub)
|
||||||
|
|
||||||
|
The `build-macos` job in `release-probo-agent.yaml` expects the same
|
||||||
|
secret names as the auditor-mode release workflow. Configure these on
|
||||||
|
the probo GitHub repository (or org) before tagging a release:
|
||||||
|
|
||||||
|
| Secret | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `APPLE_CERTIFICATE` | Base64-encoded `.p12` (Developer ID) |
|
||||||
|
| `APPLE_CERTIFICATE_PASSWORD` | `.p12` password |
|
||||||
|
| `KEYCHAIN_PASSWORD` | Ephemeral CI keychain password |
|
||||||
|
| `CODESIGN_IDENTITY` | e.g. `Developer ID Application: Probo Inc (TEAMID)` |
|
||||||
|
| `INSTALLER_IDENTITY` | e.g. `Developer ID Installer: Probo Inc (TEAMID)` |
|
||||||
|
| `APPLE_ID` | Apple ID email for `notarytool store-credentials` |
|
||||||
|
| `APPLE_ID_PASSWORD` | App-specific password (stored into a keychain profile; not passed to `submit`) |
|
||||||
|
| `APPLE_TEAM_ID` | 10-character Team ID |
|
||||||
|
|
||||||
|
Local notarization can reuse a pre-stored profile instead of putting the
|
||||||
|
password in the environment:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
xcrun notarytool store-credentials probo-agent-notary \
|
||||||
|
--apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID"
|
||||||
|
# prompts for the app-specific password once
|
||||||
|
export NOTARYTOOL_KEYCHAIN_PROFILE=probo-agent-notary
|
||||||
|
```
|
||||||
|
|
||||||
Windows enrollment is browser-driven: the console issues a
|
Windows enrollment is browser-driven: the console issues a
|
||||||
`probo://enroll?server=...&token=...` deep link handled by
|
`probo://enroll?server=...&token=...` deep link handled by
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
const plistPath = "/Library/LaunchDaemons/com.getprobo.agent.plist"
|
const (
|
||||||
|
plistPath = "/Library/LaunchDaemons/com.probo.agent.plist"
|
||||||
|
)
|
||||||
|
|
||||||
const launchdPlistTmpl = `<?xml version="1.0" encoding="UTF-8"?>
|
const launchdPlistTmpl = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
@@ -72,6 +74,15 @@ func xmlEscape(v string) (string, error) {
|
|||||||
return sb.String(), nil
|
return sb.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func removeLaunchDaemonPlist(path string) error {
|
||||||
|
_ = exec.Command("launchctl", "bootout", "system", path).Run()
|
||||||
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("cannot remove plist %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Install writes and boots the launchd plist.
|
// Install writes and boots the launchd plist.
|
||||||
func Install(cfg Config) error {
|
func Install(cfg Config) error {
|
||||||
if cfg.ExePath == "" {
|
if cfg.ExePath == "" {
|
||||||
@@ -117,10 +128,7 @@ func Install(cfg Config) error {
|
|||||||
|
|
||||||
// Uninstall bootouts and removes the launchd plist.
|
// Uninstall bootouts and removes the launchd plist.
|
||||||
func Uninstall(cfg Config) error {
|
func Uninstall(cfg Config) error {
|
||||||
_ = exec.Command("launchctl", "bootout", "system", plistPath).Run()
|
_ = cfg
|
||||||
if err := os.Remove(plistPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return fmt.Errorf("cannot remove plist: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return removeLaunchDaemonPlist(plistPath)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user