Skip to content

Commit b2f2665

Browse files
authored
Merge pull request #4046 from telepresenceio/thallgren/macos-pkg-signing
Add macOS pkg signing and refocus install docs on native installers
2 parents 867087a + e7ce344 commit b2f2665

File tree

5 files changed

+627
-331
lines changed

5 files changed

+627
-331
lines changed

.github/workflows/release.yaml

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ jobs:
4141
if: runner.os == 'Windows' && matrix.arch == 'amd64'
4242
shell: pwsh
4343
run: |
44-
dotnet tool install --global wix
45-
wix extension add -g WixToolset.BootstrapperApplications.wixext
46-
wix extension add -g WixToolset.UI.wixext
47-
wix extension add -g WixToolset.Util.wixext
44+
dotnet tool install --global wix --version 6.0.2
45+
wix extension add -g WixToolset.BootstrapperApplications.wixext/6.0.2
46+
wix extension add -g WixToolset.UI.wixext/6.0.2
47+
wix extension add -g WixToolset.Util.wixext/6.0.2
4848
- name: Build WiX Installer
4949
if: runner.os == 'Windows' && matrix.arch == 'amd64'
5050
shell: bash
@@ -53,13 +53,6 @@ jobs:
5353
cd build-aux/wix-installer
5454
make bundle ARCH=${{ matrix.arch }}
5555
cp ../../build-output/bin/TelepresenceInstall.exe ../../build-output/release/telepresence-windows-${{ matrix.arch }}-setup.exe
56-
- name: Build macOS Installer
57-
if: runner.os == 'macOS'
58-
shell: bash
59-
run: |
60-
cd build-aux/pkg-installer
61-
VERSION="${TELEPRESENCE_VERSION#v}" ARCH=${{ matrix.arch }} ./build-pkg.sh
62-
cp ../../build-output/Telepresence.pkg ../../build-output/release/telepresence-darwin-${{ matrix.arch }}.pkg
6356
- name: Install nfpm
6457
if: runner.os == 'Linux'
6558
shell: bash
@@ -234,6 +227,84 @@ jobs:
234227
delete-partial-images: true
235228
delete-orphaned-images: true
236229

230+
build-macos-pkg:
231+
# This job builds signed and notarized macOS .pkg installers.
232+
# It uses a protected environment requiring approval from authorized reviewers.
233+
# If not approved, the release proceeds without .pkg installers (standalone binaries are still available).
234+
environment: macos-signing
235+
needs:
236+
- publish-release
237+
strategy:
238+
fail-fast: false
239+
matrix:
240+
arch:
241+
- amd64
242+
- arm64
243+
runs-on: macos-latest
244+
env:
245+
GOARCH: ${{ matrix.arch }}
246+
steps:
247+
- uses: actions/checkout@v4
248+
with:
249+
fetch-depth: 0
250+
- uses: ./.github/actions/install-dependencies
251+
name: install dependencies
252+
- name: set version
253+
shell: bash
254+
run: echo "TELEPRESENCE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
255+
- name: generate binaries
256+
run: make release-binary
257+
- name: Import Apple certificates
258+
env:
259+
MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
260+
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
261+
run: |
262+
# Create a temporary keychain
263+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
264+
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
265+
266+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
267+
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
268+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
269+
270+
# Import certificates from base64-encoded P12
271+
echo "$MACOS_CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
272+
security import $RUNNER_TEMP/certificate.p12 -P "$MACOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
273+
rm $RUNNER_TEMP/certificate.p12
274+
275+
# Add keychain to search list and set as default
276+
security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain
277+
security default-keychain -s "$KEYCHAIN_PATH"
278+
279+
# Allow codesign to access the keychain without prompting
280+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
281+
282+
# Store keychain path for cleanup
283+
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV
284+
285+
- name: Build signed macOS Installer
286+
env:
287+
MACOS_SIGN_APPLICATION: ${{ secrets.MACOS_SIGN_APPLICATION }}
288+
MACOS_SIGN_INSTALLER: ${{ secrets.MACOS_SIGN_INSTALLER }}
289+
MACOS_NOTARIZE_APPLE_ID: ${{ secrets.MACOS_NOTARIZE_APPLE_ID }}
290+
MACOS_NOTARIZE_TEAM_ID: ${{ secrets.MACOS_NOTARIZE_TEAM_ID }}
291+
MACOS_NOTARIZE_PASSWORD: ${{ secrets.MACOS_NOTARIZE_PASSWORD }}
292+
run: |
293+
cd build-aux/pkg-installer
294+
VERSION="${TELEPRESENCE_VERSION#v}" ARCH=${{ matrix.arch }} ./build-pkg.sh
295+
296+
- name: Cleanup macOS keychain
297+
if: always() && env.KEYCHAIN_PATH != ''
298+
run: |
299+
security delete-keychain "$KEYCHAIN_PATH" || true
300+
301+
- name: Add signed package to release
302+
env:
303+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
304+
run: |
305+
cp build-output/Telepresence.pkg telepresence-darwin-${{ matrix.arch }}.pkg
306+
gh release upload ${{ github.ref_name }} telepresence-darwin-${{ matrix.arch }}.pkg --clobber
307+
237308
test-release:
238309
needs:
239310
- push-images
@@ -286,5 +357,4 @@ jobs:
286357
else
287358
echo "Version does not match!"
288359
exit 1
289-
fi
290-
360+
fi

CLAUDE.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,108 @@ The documentation website at [telepresence.io](https://telepresence.io) is manag
305305
- `DOCS_VERSION` - Major.minor version to generate or update (e.g., `2.27`)
306306

307307
See the telepresence.io repository for full instructions.
308+
309+
### macOS Installer Signing and Notarization
310+
311+
The macOS `.pkg` installers are signed and notarized to pass Gatekeeper verification. The signing process uses a protected GitHub Environment to secure the signing credentials.
312+
313+
#### Environment Setup
314+
315+
The `build-macos-pkg` job uses the `macos-signing` environment, which must be configured in the repository settings:
316+
317+
1. Go to https://github.com/telepresenceio/telepresence/settings/environments
318+
2. Create an environment named `macos-signing`
319+
3. Enable "Required reviewers" and add authorized personnel
320+
4. Optionally restrict deployment branches to `release/*`
321+
5. Add the following secrets to the environment (not repository-level):
322+
323+
| Secret Name | Description |
324+
|-------------|-------------|
325+
| `MACOS_CERTIFICATE_P12` | Base64-encoded P12 file containing both Application and Developer ID Installer certificates |
326+
| `MACOS_CERTIFICATE_PASSWORD` | Password for the P12 file |
327+
| `MACOS_SIGN_APPLICATION` | Developer ID Application certificate name (e.g., `Developer ID Application: Your Name (TEAMID)`) |
328+
| `MACOS_SIGN_INSTALLER` | Developer ID Installer certificate name (e.g., `Developer ID Installer: Your Name (TEAMID)`) |
329+
| `MACOS_NOTARIZE_APPLE_ID` | Apple ID email for notarization |
330+
| `MACOS_NOTARIZE_TEAM_ID` | Apple Developer Team ID |
331+
| `MACOS_NOTARIZE_PASSWORD` | App-specific password for notarization |
332+
333+
#### Release Workflow
334+
335+
When a release tag is pushed:
336+
1. All platform binaries (Linux, Windows, macOS) are built immediately
337+
2. Linux `.deb`/`.rpm` and Windows `.exe` installers are built
338+
3. The release is published with all binaries and Linux/Windows installers
339+
4. The `build-macos-pkg` job waits for approval from a required reviewer
340+
5. Once approved, signed `.pkg` installers are built and added to the release
341+
342+
This design ensures:
343+
- **Emergency releases can proceed** without the signing approver being available (all binaries and Linux/Windows installers are released)
344+
- **Signing credentials are protected** by requiring explicit approval before they are exposed
345+
- **Signed packages are added later** when the approver reviews and approves the job
346+
347+
If the environment is not configured or never approved, the release will contain macOS standalone binaries but not `.pkg` installers.
348+
349+
#### Obtaining the Certificates
350+
351+
You need an [Apple Developer Program](https://developer.apple.com/programs/) membership ($99/year) to obtain signing certificates.
352+
353+
1. **Create certificates in Apple Developer Portal:**
354+
- Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/certificates/list)
355+
- Click the + button to create a new certificate
356+
- Create **Developer ID Application** certificate (for signing binaries)
357+
- Create **Developer ID Installer** certificate (for signing .pkg files)
358+
- Download both certificates and double-click to install in Keychain Access
359+
360+
2. **Find your Team ID:**
361+
- Go to [Membership Details](https://developer.apple.com/account#MembershipDetailsCard)
362+
- Copy the Team ID (10-character alphanumeric string)
363+
- Set as `MACOS_NOTARIZE_TEAM_ID`
364+
365+
3. **Find the certificate names:**
366+
- Open Keychain Access and look under "My Certificates"
367+
- The names will be like:
368+
- `Developer ID Application: Your Name (TEAMID)` → `MACOS_SIGN_APPLICATION`
369+
- `Developer ID Installer: Your Name (TEAMID)` → `MACOS_SIGN_INSTALLER`
370+
- You can also list them with: `security find-identity -v -p codesigning`
371+
372+
4. **Export certificates to P12:**
373+
```bash
374+
# Export each certificate from Keychain Access:
375+
# - Right-click certificate → Export
376+
# - Choose .p12 format
377+
# - Set a strong password (will be MACOS_CERTIFICATE_PASSWORD)
378+
379+
# If you have both in separate .p12 files, you can import them together
380+
# or export them together from Keychain Access by selecting both
381+
382+
# Base64-encode for GitHub secrets:
383+
base64 -i certificates.p12 | pbcopy
384+
# Paste as MACOS_CERTIFICATE_P12
385+
```
386+
387+
5. **Create app-specific password for notarization:**
388+
- Go to [appleid.apple.com](https://appleid.apple.com/) → Sign-In and Security → App-Specific Passwords
389+
- Generate a new password with a descriptive name (e.g., "GitHub Actions Notarization")
390+
- Copy the generated password → `MACOS_NOTARIZE_PASSWORD`
391+
- Use your Apple ID email → `MACOS_NOTARIZE_APPLE_ID`
392+
393+
#### Testing Locally
394+
395+
To test signing locally before configuring GitHub secrets:
396+
397+
```bash
398+
# Set environment variables
399+
export MACOS_SIGN_APPLICATION="Developer ID Application: Your Name (TEAMID)"
400+
export MACOS_SIGN_INSTALLER="Developer ID Installer: Your Name (TEAMID)"
401+
export MACOS_NOTARIZE_APPLE_ID="your@email.com"
402+
export MACOS_NOTARIZE_TEAM_ID="ABCD123456"
403+
export MACOS_NOTARIZE_PASSWORD="xxxx-xxxx-xxxx-xxxx"
404+
405+
# Build the signed and notarized package
406+
cd build-aux/pkg-installer
407+
VERSION=2.26.0 ./build-pkg.sh
408+
409+
# Verify the signature
410+
pkgutil --check-signature ../../build-output/Telepresence.pkg
411+
spctl --assess --type install ../../build-output/Telepresence.pkg
412+
```

build-aux/pkg-installer/build-pkg.sh

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,58 @@ if [[ "$(uname -s)" != "Darwin" ]]; then
1414
exit 1
1515
fi
1616

17+
# === Signing and Notarization Configuration ===
18+
# These environment variables enable code signing and notarization:
19+
# MACOS_SIGN_APPLICATION - Developer ID Application certificate name (for codesign)
20+
# MACOS_SIGN_INSTALLER - Developer ID Installer certificate name (for productsign)
21+
# MACOS_NOTARIZE_APPLE_ID - Apple ID for notarization
22+
# MACOS_NOTARIZE_TEAM_ID - Apple Developer Team ID
23+
# MACOS_NOTARIZE_PASSWORD - App-specific password for notarization
24+
#
25+
# If signing variables are not set, packages will be built unsigned (suitable for local development).
26+
27+
sign_binary() {
28+
local binary="$1"
29+
if [[ -n "${MACOS_SIGN_APPLICATION}" ]]; then
30+
echo "Signing binary: $binary"
31+
codesign --force --options runtime --timestamp --sign "${MACOS_SIGN_APPLICATION}" "$binary"
32+
codesign --verify --verbose "$binary"
33+
fi
34+
}
35+
36+
sign_package() {
37+
local unsigned_pkg="$1"
38+
local signed_pkg="$2"
39+
if [[ -n "${MACOS_SIGN_INSTALLER}" ]]; then
40+
echo "Signing package: $unsigned_pkg -> $signed_pkg"
41+
productsign --sign "${MACOS_SIGN_INSTALLER}" "$unsigned_pkg" "$signed_pkg"
42+
pkgutil --check-signature "$signed_pkg"
43+
else
44+
# No signing, just rename
45+
mv "$unsigned_pkg" "$signed_pkg"
46+
fi
47+
}
48+
49+
notarize_package() {
50+
local pkg="$1"
51+
if [[ -n "${MACOS_NOTARIZE_APPLE_ID}" && -n "${MACOS_NOTARIZE_TEAM_ID}" && -n "${MACOS_NOTARIZE_PASSWORD}" ]]; then
52+
echo "Submitting package for notarization: $pkg"
53+
# Use --timeout to prevent indefinite waiting (15 minutes should be plenty)
54+
xcrun notarytool submit "$pkg" \
55+
--apple-id "${MACOS_NOTARIZE_APPLE_ID}" \
56+
--team-id "${MACOS_NOTARIZE_TEAM_ID}" \
57+
--password "${MACOS_NOTARIZE_PASSWORD}" \
58+
--wait \
59+
--timeout 15m
60+
61+
echo "Stapling notarization ticket to: $pkg"
62+
xcrun stapler staple "$pkg"
63+
xcrun stapler validate "$pkg"
64+
else
65+
echo "Skipping notarization (credentials not provided)"
66+
fi
67+
}
68+
1769
# Allow ARCH override from environment (for CI cross-builds), otherwise detect
1870
if [[ -n "${ARCH}" ]]; then
1971
arch="${ARCH}"
@@ -56,6 +108,7 @@ cli_payload="$build_output/cli/Payload"
56108
mkdir -p "$cli_payload/usr/local/bin"
57109
cp "$telepresence_binary" "$cli_payload/usr/local/bin/telepresence"
58110
chmod +x "$cli_payload/usr/local/bin/telepresence"
111+
sign_binary "$cli_payload/usr/local/bin/telepresence"
59112

60113
cp uninstall "$cli_payload/usr/local/bin/telepresence-uninstall"
61114
chmod +x "$cli_payload/usr/local/bin/telepresence-uninstall"
@@ -103,6 +156,12 @@ productbuild --distribution "$product/Distribution.xml" \
103156
--package-path "$product" \
104157
--resources "$resources" \
105158
--version "$VERSION" \
106-
"$build_output/Telepresence.pkg"
159+
"$build_output/Telepresence-unsigned.pkg"
160+
161+
# Sign the package (or rename if no signing certificate)
162+
sign_package "$build_output/Telepresence-unsigned.pkg" "$build_output/Telepresence.pkg"
163+
164+
# Notarize the signed package (if credentials are provided)
165+
notarize_package "$build_output/Telepresence.pkg"
107166

108167
rm -rf cli rootd resources product

0 commit comments

Comments
 (0)