You've built a Tauri desktop app. It compiles, it runs, your friends are impressed. Nice! Then you send the .dmg to someone and macOS says "this app is damaged and can't be opened." Or Windows SmartScreen blocks the .exe entirely. Welcome to the world of code signing.
This two-part guide walks you through the entire release pipeline for a Tauri v2 application -- from code signing certificates to automated cross-platform builds on GitHub Actions. It's based on real-world experience shipping Fortuna, an open-source offline wealth management desktop app built with Tauri v2, React, and Rust.
Part 1 (this article) covers code signing setup for macOS and Windows.
Part 2 covers GitHub Actions CI/CD, release automation scripts, and the updater.
By the end, pushing a git tag will automatically build signed .dmg and .exe installers and publish them as a GitHub Release.
Table of Contents
- Prerequisites
- Project Configuration Baseline
- macOS Code Signing
- Windows Code Signing
- Tauri Updater Signing
- Summary of Secrets
- What's Next
Prerequisites
Before you start, make sure you have:
- A working Tauri v2 project that builds locally (
npm run tauri build) - Node.js (LTS)
- Rust (stable toolchain)
- A macOS machine (required for Apple certificate creation)
- A GitHub repository for your project
- Some patience -- there are a lot of accounts and portals involved
Project Configuration Baseline
Your src-tauri/tauri.conf.json should already have the basic bundle configuration. Here's what the relevant skeleton looks like before we add signing:
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "YourApp",
"version": "0.1.0",
"identifier": "com.yourcompany.yourapp",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"bundle": {
"active": true,
"targets": ["app", "dmg", "nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"category": "Finance",
"shortDescription": "Your app description",
"macOS": {},
"windows": {}
}
}
The targets array tells Tauri what to produce:
-
"app"-- the.appbundle on macOS -
"dmg"-- the macOS disk image installer -
"nsis"-- the Windows.exeinstaller (NSIS-based)
We'll fill in the macOS and windows sections as we go.
macOS Code Signing
Apple requires apps distributed outside the App Store to be code signed and notarized. Without both, macOS will either show scary warnings or outright refuse to open your app.
1. Apple Developer Account
You need a paid Apple Developer account ($99/year) from developer.apple.com. The free tier lets you develop and test but cannot notarize apps -- meaning users will see the "damaged app" dialog.
Enroll at: developer.apple.com/programs/enroll
2. Create a Developer ID Certificate
A Developer ID Application certificate is what you need for apps distributed outside the Mac App Store.
Only the Account Holder role can create Developer ID certificates. If you're on a team, the account holder needs to do this step.
Steps:
- On your Mac, open Keychain Access
- Go to Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority
- Enter your email, leave CA Email blank, select Saved to disk, and save the
.certSigningRequestfile - Go to Apple Developer > Certificates
- Click the + button to create a new certificate
- Select Developer ID Application and click Continue
- Upload your
.certSigningRequestfile - Download the generated
.cerfile - Double-click the
.cerfile to install it in your keychain (make sure the login keychain is selected)
3. Find Your Signing Identity
After installing the certificate, find the exact identity string:
security find-identity -v -p codesigning
You'll see output like:
1) ABC123DEF456... "Developer ID Application: Your Name (TEAMID)"
The quoted string is your signing identity. Note it down -- you'll need it for tauri.conf.json and as a GitHub secret.
4. Create an Entitlements File
Tauri apps use a WebView that requires JIT compilation. Create src-tauri/Entitlements.plist:
<?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>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
These two entitlements are required for the WebView to function:
-
allow-jit-- enables Just-In-Time compilation -
allow-unsigned-executable-memory-- allows the JavaScript engine to allocate executable memory
5. Configure tauri.conf.json for macOS
Add the macOS signing configuration to your bundle section:
{
"bundle": {
"macOS": {
"signingIdentity": "Developer ID Application: Your Name (TEAMID)",
"entitlements": "./Entitlements.plist",
"minimumSystemVersion": "11.0",
"dmg": {
"appPosition": { "x": 180, "y": 170 },
"applicationFolderPosition": { "x": 480, "y": 170 }
}
}
}
}
Field breakdown:
| Field | Purpose |
|---|---|
signingIdentity |
The identity string from step 3. Can also be set via APPLE_SIGNING_IDENTITY env var. |
entitlements |
Path to the entitlements plist (relative to src-tauri/). |
minimumSystemVersion |
Minimum macOS version. "11.0" (Big Sur) is a reasonable floor for modern apps. |
dmg.appPosition |
Where the app icon sits in the DMG installer window. |
dmg.applicationFolderPosition |
Where the "Applications" shortcut sits in the DMG. |
The DMG position values control the drag-to-install layout that users see when they open the .dmg file.
6. Set Up Notarization
Notarization is Apple's automated security check. It's separate from code signing -- after Tauri signs your app, it uploads the binary to Apple's servers for analysis. If it passes, Apple staples a "ticket" to your app so macOS trusts it.
Tauri handles notarization automatically during the build process. You just need to provide credentials via environment variables.
Option A: Apple ID + App-Specific Password (simpler)
This is the approach most indie developers use.
- Go to appleid.apple.com > Sign-In and Security > App-Specific Passwords
- Generate a new app-specific password (name it something like "Tauri Notarization")
- Find your Team ID at developer.apple.com/account under Membership Details
The environment variables you'll need:
| Variable | Value |
|---|---|
APPLE_ID |
Your Apple account email |
APPLE_PASSWORD |
The app-specific password (NOT your Apple ID password) |
APPLE_TEAM_ID |
Your 10-character Team ID |
Option B: App Store Connect API Key (more secure, recommended for teams)
- Go to App Store Connect > Users and Access > Integrations > Keys
- Click + to create a new key with Developer access
- Download the
.p8private key file (you can only download it once) - Note the Key ID and Issuer ID
| Variable | Value |
|---|---|
APPLE_API_ISSUER |
The Issuer ID shown above the keys table |
APPLE_API_KEY |
The Key ID from the table |
APPLE_API_KEY_PATH |
Path to the downloaded .p8 key file |
This guide uses Option A for simplicity. Either way works with Tauri's built-in notarization.
7. Export Your Certificate for CI
Your Mac has the certificate in its keychain, but GitHub Actions runners need it too. You'll export it as a base64-encoded .p12 file.
- Open Keychain Access
- Click My Certificates in the left sidebar (under "login" keychain)
- Find your "Developer ID Application" certificate
- Expand it (click the arrow) -- you should see a private key underneath
- Right-click the private key and select Export
- Save as
.p12format and set a strong password - Convert to base64:
base64 -i certificate.p12 -o certificate-base64.txt
- Open
certificate-base64.txt-- this is the value for theAPPLE_CERTIFICATEsecret
Security note: Delete the
.p12andcertificate-base64.txtfiles after you've stored them as GitHub secrets. Never commit these files to your repository.
macOS secrets summary:
| GitHub Secret | Value |
|---|---|
APPLE_CERTIFICATE |
Base64 content of the exported .p12 file |
APPLE_CERTIFICATE_PASSWORD |
Password you set during .p12 export |
APPLE_SIGNING_IDENTITY |
e.g., Developer ID Application: Your Name (TEAMID)
|
APPLE_TEAM_ID |
Your 10-character Team ID |
APPLE_ID |
Your Apple account email |
APPLE_PASSWORD |
App-specific password for notarization |
Windows Code Signing
Windows code signing prevents SmartScreen from blocking your installer and tells users that your app comes from a verified publisher.
Why Azure Key Vault?
Since June 2023, certificate authorities no longer issue OV (Organization Validation) code signing certificates on exportable files. New certificates must be stored on hardware security modules (HSMs). The most accessible option for indie developers and small teams is Azure Key Vault, which acts as a cloud-based HSM.
We'll use relic, an open-source signing tool that can authenticate to Azure Key Vault and sign Windows executables.
Alternative: Azure Trusted Signing is another option, but it requires a more involved setup with Azure Code Signing accounts and profiles. The Key Vault approach is more straightforward.
1. Create an Azure Account
Sign up at portal.azure.com. You'll need an active subscription -- the Pay-As-You-Go plan works fine. Key Vault costs are minimal (a few cents per signing operation).
2. Set Up Azure Key Vault
- In the Azure Portal, search for Key Vault and create one
- Pick a name (e.g.,
app-signing-tauri), choose your region, select the Standard pricing tier - Create the vault
- Navigate to your vault > Objects > Certificates
- Click Generate/Import to create a new certificate:
- Method: Generate
-
Certificate Name: e.g.,
your-app-signing - Type of CA: Self-signed (or integrate with a CA if you have one)
-
Subject:
CN=Your Company Name - Validity Period: 12 months (or your preference)
- Content Type: PKCS #12
- Click Create and wait for it to provision
Note on self-signed vs CA-issued: A self-signed certificate from Azure Key Vault will still trigger SmartScreen warnings initially. To avoid this entirely, you need an EV (Extended Validation) certificate from a trusted CA stored in Key Vault. For most indie apps, SmartScreen reputation builds over time as more users install your app. You can also manually submit your binary to Microsoft's file submission portal to speed this up.
3. Create an App Registration
Azure Key Vault uses Azure Active Directory for authentication. You need an "App Registration" -- essentially a service account.
- In the Azure Portal, go to Microsoft Entra ID (formerly Azure Active Directory)
- Navigate to App registrations > New registration
- Name it (e.g.,
tauri-code-signing) - Leave the redirect URI blank
- Click Register
- Note the Application (client) ID -- this is your
AZURE_CLIENT_ID - Note the Directory (tenant) ID -- this is your
AZURE_TENANT_ID - Go to Certificates & secrets > Client secrets > New client secret
- Set a description and expiration
- Click Add and immediately copy the Value -- this is your
AZURE_CLIENT_SECRET
The client secret value is only shown once. Copy it now or you'll need to create a new one.
4. Assign Permissions
Your app registration needs permission to use the Key Vault for signing.
- Go to your Key Vault in the Azure Portal
- Navigate to Access control (IAM)
- Click Add role assignment
- Assign these two roles to your app registration:
- Key Vault Certificate User -- allows reading the certificate
- Key Vault Crypto User -- allows signing operations
Without both roles, signing will fail with a permissions error.
5. Install relic
Relic is a Go-based signing tool that bridges Azure Key Vault and the code signing process.
go install github.com/sassoftware/relic/v8@latest
Make sure $GOPATH/bin is in your PATH. Verify with:
relic --version
On the GitHub Actions Windows runner, Go is pre-installed. You'll install relic as a build step (covered in Part 2).
6. Create the relic Configuration
Create src-tauri/relic.conf:
tokens:
azure:
type: azure
keys:
azure:
token: azure
id: https://<YOUR_VAULT_NAME>.vault.azure.net/certificates/<YOUR_CERTIFICATE_NAME>
Replace:
-
<YOUR_VAULT_NAME>with your Key Vault name (e.g.,app-signing-tauri) -
<YOUR_CERTIFICATE_NAME>with your certificate name (e.g.,your-app-signing)
This file is safe to commit -- it contains no secrets, only the vault and certificate identifiers. Authentication happens via environment variables at runtime.
7. Configure tauri.conf.json for Windows
Add the Windows signing configuration to your bundle section:
{
"bundle": {
"windows": {
"signCommand": "relic sign --file %1 --key azure --config relic.conf",
"webviewInstallMode": {
"type": "downloadBootstrapper"
},
"nsis": {
"installMode": "both"
}
}
}
}
Field breakdown:
| Field | Purpose |
|---|---|
signCommand |
Custom signing command. %1 is replaced by the file path to sign. Tauri calls this for every binary and the installer. |
webviewInstallMode |
How the installer handles the WebView2 runtime. "downloadBootstrapper" downloads it on demand, keeping your installer small. |
nsis.installMode |
"both" means the installer can run as both per-user and per-machine. |
The signCommand approach is the most flexible -- Tauri will invoke this command for every file that needs signing, passing the file path as %1. Relic then authenticates to Azure using the AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET environment variables and signs the file using the Key Vault certificate.
Windows secrets summary:
| GitHub Secret | Value |
|---|---|
AZURE_CLIENT_ID |
Application (client) ID from App Registration |
AZURE_TENANT_ID |
Directory (tenant) ID from App Registration |
AZURE_CLIENT_SECRET |
Client secret value from App Registration |
Tauri Updater Signing
If you plan to use Tauri's built-in auto-updater, you need a separate signing keypair. This is unrelated to OS-level code signing -- it's used to verify that update payloads come from you and haven't been tampered with.
1. Generate an Update Signing Keypair
Run the Tauri CLI to generate a keypair:
npx tauri signer generate -w ~/.tauri/myapp.key
This creates two files:
-
~/.tauri/myapp.key-- your private key (keep this secret) -
~/.tauri/myapp.key.pub-- your public key (embed this in your app)
The CLI will prompt you for an optional password. If you set one, you'll need to provide it as TAURI_SIGNING_PRIVATE_KEY_PASSWORD.
Store the private key securely and never commit it. The public key is safe to embed in your app config.
2. Configure the Updater
Add the updater plugin configuration to tauri.conf.json:
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"endpoints": [
"https://github.com/YOUR_USERNAME/YOUR_REPO/releases/latest/download/latest.json"
],
"pubkey": "YOUR_PUBLIC_KEY_CONTENT_HERE"
}
}
}
Field breakdown:
| Field | Purpose |
|---|---|
createUpdaterArtifacts |
Tells the build to generate the .sig signature files and latest.json manifest alongside the installer. |
endpoints |
Where your app checks for updates. The GitHub Releases URL is the simplest approach. |
pubkey |
The content of your .key.pub file. Used by the app to verify update signatures. |
The tauri-action in your CI workflow will automatically upload the latest.json file to the GitHub Release, which the updater plugin reads to detect available updates.
Updater secrets:
| GitHub Secret | Value |
|---|---|
TAURI_SIGNING_PRIVATE_KEY |
Content of the private key file (~/.tauri/myapp.key) |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Password for the key (if you set one) |
Summary of Secrets
Here's the complete list of GitHub secrets you'll need to configure in your repository (Settings > Secrets and variables > Actions):
macOS Signing + Notarization
| Secret | Description |
|---|---|
APPLE_CERTIFICATE |
Base64-encoded .p12 certificate |
APPLE_CERTIFICATE_PASSWORD |
Password for the .p12 file |
APPLE_SIGNING_IDENTITY |
e.g., Developer ID Application: Your Name (TEAMID)
|
APPLE_TEAM_ID |
10-character Team ID from Apple Developer portal |
APPLE_ID |
Apple account email for notarization |
APPLE_PASSWORD |
App-specific password for notarization |
Windows Signing (Azure Key Vault)
| Secret | Description |
|---|---|
AZURE_CLIENT_ID |
App Registration client ID |
AZURE_TENANT_ID |
Azure directory tenant ID |
AZURE_CLIENT_SECRET |
App Registration client secret |
Tauri Updater
| Secret | Description |
|---|---|
TAURI_SIGNING_PRIVATE_KEY |
Private key for update signing |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Password for the key (if set) |
Total: 11 secrets. Yes, it's a lot. The good news is you only set them up once.
Complete tauri.conf.json
Here's what the full bundle section looks like with everything configured:
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "YourApp",
"version": "0.1.0",
"identifier": "com.yourcompany.yourapp",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "YourApp",
"width": 1200,
"height": 800,
"resizable": true
}
],
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'"
}
},
"bundle": {
"createUpdaterArtifacts": true,
"active": true,
"targets": ["app", "dmg", "nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"category": "Finance",
"shortDescription": "Your app description",
"macOS": {
"signingIdentity": "Developer ID Application: Your Name (TEAMID)",
"entitlements": "./Entitlements.plist",
"minimumSystemVersion": "11.0",
"dmg": {
"appPosition": { "x": 180, "y": 170 },
"applicationFolderPosition": { "x": 480, "y": 170 }
}
},
"windows": {
"signCommand": "relic sign --file %1 --key azure --config relic.conf",
"webviewInstallMode": { "type": "downloadBootstrapper" },
"nsis": { "installMode": "both" }
}
},
"plugins": {
"updater": {
"endpoints": [
"https://github.com/YOUR_USERNAME/YOUR_REPO/releases/latest/download/latest.json"
],
"pubkey": "YOUR_PUBLIC_KEY_HERE"
}
}
}
What's Next
At this point you have:
- A macOS signing certificate ready for CI
- Azure Key Vault configured for Windows signing
- Updater signing keys generated
- All 11 secrets identified and ready to add to GitHub
In Part 2, we'll wire all of this into a GitHub Actions workflow that:
- Builds your app for macOS (ARM + Intel) and Windows
- Signs and notarizes automatically
- Uploads installers to a draft GitHub Release
- Generates updater artifacts for in-app updates
We'll also build a release automation script that bumps versions, generates changelogs, and triggers the whole pipeline with a single command.
Continue to Part 2: GitHub Actions and Release Automation ->
Top comments (0)