DEV Community

Thomas Cosialls
Thomas Cosialls

Posted on

Ship Your Tauri v2 App Like a Pro: Code Signing for macOS and Windows (Part 1/2)

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

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": {}
  }
}
Enter fullscreen mode Exit fullscreen mode

The targets array tells Tauri what to produce:

  • "app" -- the .app bundle on macOS
  • "dmg" -- the macOS disk image installer
  • "nsis" -- the Windows .exe installer (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:

  1. On your Mac, open Keychain Access
  2. Go to Keychain Access > Certificate Assistant > Request a Certificate From a Certificate Authority
  3. Enter your email, leave CA Email blank, select Saved to disk, and save the .certSigningRequest file
  4. Go to Apple Developer > Certificates
  5. Click the + button to create a new certificate
  6. Select Developer ID Application and click Continue
  7. Upload your .certSigningRequest file
  8. Download the generated .cer file
  9. Double-click the .cer file 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
Enter fullscreen mode Exit fullscreen mode

You'll see output like:

1) ABC123DEF456... "Developer ID Application: Your Name (TEAMID)"
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. Go to appleid.apple.com > Sign-In and Security > App-Specific Passwords
  2. Generate a new app-specific password (name it something like "Tauri Notarization")
  3. 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)

  1. Go to App Store Connect > Users and Access > Integrations > Keys
  2. Click + to create a new key with Developer access
  3. Download the .p8 private key file (you can only download it once)
  4. 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.

  1. Open Keychain Access
  2. Click My Certificates in the left sidebar (under "login" keychain)
  3. Find your "Developer ID Application" certificate
  4. Expand it (click the arrow) -- you should see a private key underneath
  5. Right-click the private key and select Export
  6. Save as .p12 format and set a strong password
  7. Convert to base64:
base64 -i certificate.p12 -o certificate-base64.txt
Enter fullscreen mode Exit fullscreen mode
  1. Open certificate-base64.txt -- this is the value for the APPLE_CERTIFICATE secret

Security note: Delete the .p12 and certificate-base64.txt files 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

  1. In the Azure Portal, search for Key Vault and create one
  2. Pick a name (e.g., app-signing-tauri), choose your region, select the Standard pricing tier
  3. Create the vault
  4. Navigate to your vault > Objects > Certificates
  5. 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
  6. 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.

  1. In the Azure Portal, go to Microsoft Entra ID (formerly Azure Active Directory)
  2. Navigate to App registrations > New registration
  3. Name it (e.g., tauri-code-signing)
  4. Leave the redirect URI blank
  5. Click Register
  6. Note the Application (client) ID -- this is your AZURE_CLIENT_ID
  7. Note the Directory (tenant) ID -- this is your AZURE_TENANT_ID
  8. Go to Certificates & secrets > Client secrets > New client secret
  9. Set a description and expiration
  10. 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.

  1. Go to your Key Vault in the Azure Portal
  2. Navigate to Access control (IAM)
  3. Click Add role assignment
  4. 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
Enter fullscreen mode Exit fullscreen mode

Make sure $GOPATH/bin is in your PATH. Verify with:

relic --version
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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)