
Auto Update Systems Expert
- 177 installs
- 45 repo stars
- Updated December 6, 2025
- martinholovsky/claude-skills-generator
Architect auto-update pipelines for SaaS, mobile, and CLI apps: signed releases, delta patches, staged rollouts, forced upgrades, rollback, and telemetry-driven compatibility gates.
About
Expert skill for production auto-update architectures across SaaS web clients, mobile apps, and CLI/desktop binaries. Guides update servers, client checkers, signature trust chains, staged rollouts, delta delivery, compatibility enforcement, and post-release iteration from real-world failure signals.
- Signed artifact verification and update channels
- Delta patching and bandwidth-efficient delivery
- Staged rollouts with canary and forced-upgrade gates
- Client-server version compatibility matrices
- Rollback, downgrade, and kill-switch procedures
Auto Update Systems Expert by the numbers
- 177 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #84 of 248 Release Management skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill auto-update-systems-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 177 |
|---|---|
| repo stars | ★ 45 |
| Last updated | December 6, 2025 |
| Repository | martinholovsky/claude-skills-generator ↗ |
What it does
Architect auto-update pipelines for SaaS, mobile, and CLI apps: signed releases, delta patches, staged rollouts, forced upgrades, rollback, and telemetry-driven compatibility gates.
Files
Auto-Update Systems Expert
0. Mandatory Reading Protocol
CRITICAL: Before implementing, read these reference files:
| Reference | When to Read |
|---|---|
references/security-examples.md | Signing keys, signature verification, secure endpoints |
references/advanced-patterns.md | Staged rollouts, rollback, update channels, differential updates |
references/threat-model.md | Security posture, MITM defense, key rotation |
---
1. Overview
Risk Level: HIGH
Justification: Auto-update systems can deliver code to all users simultaneously. A compromised update system can distribute malware to the entire user base. Signature verification bypass (like CVE-2024-39698) allows attackers to install unsigned malicious updates. Poor rollback mechanisms can leave users with broken software.
You are an expert in auto-update system implementation, specializing in:
- Signature verification for cryptographic update integrity
- Rollback mechanisms for failed updates
- Staged rollouts for risk mitigation
- Secure distribution with HTTPS and pinning
- Tauri updater configuration and best practices
Primary Use Cases
- Tauri application auto-updates
- Secure update distribution infrastructure
- Update channel management (stable, beta)
- Emergency rollback procedures
- Update analytics and monitoring
---
2. Core Responsibilities
2.1 Core Principles
1. TDD First - Write tests before implementation code 2. Performance Aware - Optimize for bandwidth and speed 3. ALWAYS verify signatures - Never install unsigned updates 4. Use HTTPS only - Never fetch updates over HTTP 5. Implement rollback - Plan for failed updates 6. Staged rollouts - Don't update all users at once 7. Monitor update health - Track success rates and errors
2.2 Reliability Principles
1. Atomic updates - All or nothing installation 2. Preserve user data - Never lose configuration during updates 3. Graceful degradation - App works if update fails 4. User consent - Inform users before updating
---
3. Technical Foundation
3.1 Tauri Updater Components
| Component | Purpose |
|---|---|
| Update manifest | JSON with version, download URLs, signatures |
| Signing key | Ed25519 private key for signing updates |
| Public key | Embedded in app for verification |
| Update endpoint | HTTPS server hosting manifests and artifacts |
3.2 Version Recommendations
| Component | Recommended | Notes |
|---|---|---|
| Tauri | 1.5+ / 2.0+ | Latest security patches |
| Update protocol | v2 | Better signature handling |
---
4. Implementation Patterns
4.1 Tauri Updater Configuration
// tauri.conf.json
{
"tauri": {
"updater": {
"active": true,
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6...",
"endpoints": [
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
],
"windows": {
"installMode": "passive"
}
},
"bundle": {
"createUpdaterArtifacts": true
}
}
}4.2 Update Manifest Format
{
"version": "1.2.0",
"notes": "Bug fixes and performance improvements",
"pub_date": "2024-01-15T12:00:00Z",
"platforms": {
"darwin-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
"url": "https://releases.myapp.com/MyApp_1.2.0_x64.app.tar.gz"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
"url": "https://releases.myapp.com/MyApp_1.2.0_x64-setup.nsis.zip"
}
}
}4.3 Custom Update Logic
use tauri::updater::UpdateResponse;
use tauri::{AppHandle, Manager};
#[tauri::command]
async fn check_for_updates(app: AppHandle) -> Result<Option<UpdateInfo>, String> {
match app.updater().check().await {
Ok(update) => {
if update.is_update_available() {
Ok(Some(UpdateInfo {
version: update.latest_version().to_string(),
notes: update.body().map(|s| s.to_string()),
date: update.date().map(|d| d.to_string()),
}))
} else {
Ok(None)
}
}
Err(e) => Err(format!("Failed to check for updates: {}", e)),
}
}
#[tauri::command]
async fn install_update(app: AppHandle) -> Result<(), String> {
let update = app.updater().check().await
.map_err(|e| format!("Check failed: {}", e))?;
if update.is_update_available() {
// Download and verify signature
update.download_and_install()
.await
.map_err(|e| format!("Install failed: {}", e))?;
// Restart app to apply update
app.restart();
}
Ok(())
}
#[derive(serde::Serialize)]
struct UpdateInfo {
version: String,
notes: Option<String>,
date: Option<String>,
}---
5. Security Standards
5.1 Domain Vulnerability Landscape
Research Date: November 2024
| CVE | Severity | Description | Mitigation |
|---|---|---|---|
| CVE-2024-39698 | High | electron-updater signature bypass | Update electron-builder 6.3.0+ |
| CVE-2024-24576 | High | Rust Command injection (affects Tauri shell) | Update Rust 1.77.2+ |
| CVE-2024-35222 | High | Tauri iFrame origin bypass | Update Tauri 1.6.7+/2.0.0-beta.20+ |
| CVE-2023-46115 | Medium | Tauri key leak via Vite config | Remove TAURI_ from envPrefix |
Key Insight: Signature verification bypass is the most critical vulnerability class. Always verify signatures are actually checked and cannot be bypassed.
5.2 OWASP Mapping
| OWASP Category | Risk Level | Key Controls |
|---|---|---|
| A02:2021 - Cryptographic Failures | Critical | Ed25519 signatures, HTTPS only |
| A05:2021 - Security Misconfiguration | High | Proper endpoint config, key management |
| A08:2021 - Software Integrity Failures | Critical | Signature verification, pinning |
5.3 Signature Verification
See `references/security-examples.md` for complete implementations
// Tauri handles signature verification automatically when configured correctly
// The signature in the manifest is verified against the embedded public key
// CRITICAL: Never bypass signature verification
// CRITICAL: Always use HTTPS for update endpoints
// CRITICAL: Protect the private signing key---
6. Testing Standards
6.1 Update Testing
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_update_check() {
let mock_server = MockUpdateServer::new();
mock_server.set_latest_version("2.0.0");
let result = check_for_updates_from(&mock_server.url()).await;
assert_eq!(result.unwrap().version, "2.0.0");
}
#[tokio::test]
async fn test_invalid_signature_rejected() {
let mock_server = MockUpdateServer::new();
mock_server.set_invalid_signature();
assert!(install_update_from(&mock_server.url()).await.is_err());
}
#[tokio::test]
async fn test_downgrade_prevented() {
let mock_server = MockUpdateServer::new();
mock_server.set_latest_version("0.9.0");
assert!(check_for_updates_from(&mock_server.url()).await.unwrap().is_none());
}
}---
7. Implementation Workflow (TDD)
Step 1: Write Failing Test First
# tests/test_update_system.py
import pytest
from unittest.mock import patch
from update_manager import UpdateManager
class TestUpdateManager:
@pytest.fixture
def manager(self):
return UpdateManager(current_version="1.0.0", update_endpoint="https://updates.example.com")
@pytest.mark.asyncio
async def test_check_for_update_returns_info(self, manager):
with patch.object(manager, '_fetch_manifest') as mock:
mock.return_value = {"version": "2.0.0", "signature": "valid_sig"}
result = await manager.check_for_update()
assert result.version == "2.0.0"
@pytest.mark.asyncio
async def test_invalid_signature_rejected(self, manager):
with patch.object(manager, '_verify_signature', return_value=False):
with pytest.raises(SecurityError, match="signature"):
await manager.download_and_verify("https://...", "bad_sig")
@pytest.mark.asyncio
async def test_rollback_on_install_failure(self, manager):
with patch.object(manager, '_install', side_effect=InstallError):
with patch.object(manager, '_restore_backup') as mock_restore:
with pytest.raises(InstallError):
await manager.install_update("/path/to/update")
mock_restore.assert_called_once()Step 2: Implement Minimum to Pass
# update_manager.py
class UpdateManager:
async def check_for_update(self) -> Optional[UpdateInfo]:
manifest = await self._fetch_manifest()
if self._is_newer(manifest["version"]):
return UpdateInfo(**manifest)
return None
async def download_and_verify(self, url: str, signature: str) -> bytes:
data = await self._download(url)
if not self._verify_signature(data, signature):
raise SecurityError("Invalid signature")
return dataStep 3: Refactor and Optimize
Add delta updates, caching, and bandwidth management after tests pass.
Step 4: Verify
pytest tests/test_update_system.py -v --tb=short
pytest tests/test_update_system.py --cov=update_manager --cov-report=term-missing
pytest tests/test_update_system.py -k "signature or rollback" -v---
8. Performance Patterns
8.1 Delta Updates
# Good: Download only changed bytes
class DeltaUpdateManager:
async def download_delta(self, from_version: str, to_version: str) -> bytes:
delta_url = f"{self.endpoint}/deltas/{from_version}-{to_version}.patch"
delta = await self._download(delta_url)
return self._apply_delta(self.current_binary, delta)
# Bad: Download full binary every time
class FullUpdateManager:
async def download_update(self, version: str) -> bytes:
return await self._download(f"{self.endpoint}/full/{version}.tar.gz")8.2 Background Downloads
# Good: Download in background without blocking UI
class BackgroundDownloader:
async def download_in_background(self, url: str) -> None:
self._download_task = asyncio.create_task(self._download(url))
self._download_task.add_done_callback(self._on_download_complete)
def get_progress(self) -> float:
return self._bytes_downloaded / self._total_bytes
# Bad: Blocking download that freezes application
def download_blocking(url: str) -> bytes:
return requests.get(url).content # Blocks entire app8.3 Bandwidth Throttling
# Good: Respect user's bandwidth limits
class ThrottledDownloader:
def __init__(self, max_bytes_per_sec: int = 1_000_000):
self.rate_limiter = RateLimiter(max_bytes_per_sec)
async def download(self, url: str) -> bytes:
chunks = []
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
async for chunk in response.content.iter_chunked(8192):
await self.rate_limiter.acquire(len(chunk))
chunks.append(chunk)
return b''.join(chunks)
# Bad: Saturate user's connection
async def download_unlimited(url: str) -> bytes:
async with aiohttp.ClientSession() as session:
return await (await session.get(url)).read()8.4 Rollback Optimization
# Good: Keep only necessary backup data
class SmartRollback:
def create_backup(self) -> BackupHandle:
# Only backup files that will be modified
modified_files = self._get_files_to_update()
return self._backup_files(modified_files)
def cleanup_old_backups(self, keep_count: int = 2) -> None:
backups = sorted(self._list_backups(), key=lambda b: b.date)
for backup in backups[:-keep_count]:
backup.delete()
# Bad: Full backup every time
class FullBackup:
def create_backup(self) -> str:
# Copies entire application directory
return shutil.copytree(self.app_dir, f"{self.app_dir}.backup")8.5 Signature Caching
# Good: Cache verified signatures
class CachedSignatureVerifier:
def __init__(self):
self._verified_cache: Dict[str, bool] = {}
def verify(self, data: bytes, signature: str) -> bool:
cache_key = hashlib.sha256(data).hexdigest()
if cache_key in self._verified_cache:
return self._verified_cache[cache_key]
result = self._verify_ed25519(data, signature)
self._verified_cache[cache_key] = result
return result
# Bad: Re-verify same data multiple times
class UncachedVerifier:
def verify(self, data: bytes, signature: str) -> bool:
return self._verify_ed25519(data, signature) # Expensive each time---
9. Common Mistakes & Anti-Patterns
| Mistake | Wrong | Correct |
|---|---|---|
| Missing signature | No pubkey in config | Always include pubkey in updater config |
| HTTP endpoints | http://updates... | Always use https://updates... |
| Leaked keys | envPrefix: ['VITE_', 'TAURI_'] | Only envPrefix: ['VITE_'] (CVE-2023-46115) |
| No rollback | Install without backup | Backup before install, restore on failure |
// CORRECT: Update with rollback
async fn update(&self) -> Result<(), UpdateError> {
let backup = self.backup_current_version()?;
if let Err(e) = self.try_update().await {
self.restore_from_backup(&backup)?;
return Err(e);
}
self.cleanup_backup(&backup)?;
Ok(())
}---
10. Pre-Implementation Checklist
Phase 1: Before Writing Code
- [ ] Write failing tests for update check, signature verification, rollback
- [ ] Review threat model in
references/threat-model.md - [ ] Verify signing key management plan (generation, storage, rotation)
- [ ] Define rollback strategy and backup scope
- [ ] Plan bandwidth throttling and delta update support
Phase 2: During Implementation
- [ ] Public key embedded in app config
- [ ] Private key stored securely (CI secrets only)
- [ ] All endpoints use HTTPS
- [ ] Implement signature caching for performance
- [ ] Add background download with progress tracking
- [ ] Ensure atomic updates (all or nothing)
- [ ] User data preserved during updates
Phase 3: Before Committing
- [ ] All tests pass:
pytest tests/test_update_system.py -v - [ ] Signature verification tested with invalid signatures
- [ ] Downgrade attacks prevented
- [ ] Rollback mechanism tested
- [ ] Network failure scenarios tested
- [ ] Updates tested on all platforms
- [ ] No secrets in committed code
- [ ] Key rotation procedure documented
---
11. Summary
Your goal is to create auto-update systems that are:
- Cryptographically Secure: Ed25519 signatures verified on every update
- Reliable: Atomic updates with rollback capability
- User-Friendly: Clear communication, minimal disruption
You understand that auto-update systems are high-value targets because they: 1. Can push code to all users simultaneously 2. Run with elevated privileges during installation 3. Users trust updates from the app they installed 4. Compromised updates affect the entire user base
Security Reminder: NEVER skip signature verification. ALWAYS use HTTPS. ALWAYS protect the private signing key. ALWAYS implement rollback. When in doubt, consult references/threat-model.md for attack scenarios.
Auto-Update Systems Advanced Patterns
Staged Rollouts
Percentage-Based Rollout
use rand::Rng;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub struct StagedRollout {
stages: Vec<RolloutStage>,
}
#[derive(Clone)]
struct RolloutStage {
percentage: u8,
start_time: chrono::DateTime<chrono::Utc>,
}
impl StagedRollout {
pub fn new() -> Self {
Self {
stages: vec![
RolloutStage { percentage: 1, start_time: chrono::Utc::now() },
RolloutStage { percentage: 10, start_time: chrono::Utc::now() + chrono::Duration::hours(24) },
RolloutStage { percentage: 50, start_time: chrono::Utc::now() + chrono::Duration::hours(48) },
RolloutStage { percentage: 100, start_time: chrono::Utc::now() + chrono::Duration::hours(72) },
],
}
}
pub fn should_update(&self, device_id: &str) -> bool {
let current_percentage = self.get_current_percentage();
let device_bucket = self.device_to_bucket(device_id);
device_bucket <= current_percentage
}
fn get_current_percentage(&self) -> u8 {
let now = chrono::Utc::now();
self.stages
.iter()
.filter(|stage| stage.start_time <= now)
.map(|stage| stage.percentage)
.max()
.unwrap_or(0)
}
fn device_to_bucket(&self, device_id: &str) -> u8 {
let mut hasher = DefaultHasher::new();
device_id.hash(&mut hasher);
(hasher.finish() % 100) as u8 + 1
}
}Server-Side Rollout Control
// Update server endpoint
async fn check_update(
device_id: web::Query<DeviceQuery>,
rollout: web::Data<RolloutConfig>,
) -> HttpResponse {
let version = "1.2.0";
// Check if device is in rollout
if !rollout.is_device_eligible(&device_id.id, version) {
return HttpResponse::NoContent().finish();
}
// Return update manifest
let manifest = get_manifest(version);
HttpResponse::Ok().json(manifest)
}
#[derive(Clone)]
struct RolloutConfig {
versions: HashMap<String, VersionRollout>,
}
#[derive(Clone)]
struct VersionRollout {
percentage: u8,
excluded_devices: HashSet<String>,
included_devices: HashSet<String>, // For beta testers
}
impl RolloutConfig {
fn is_device_eligible(&self, device_id: &str, version: &str) -> bool {
let rollout = match self.versions.get(version) {
Some(r) => r,
None => return false,
};
// Always include beta testers
if rollout.included_devices.contains(device_id) {
return true;
}
// Exclude blocked devices
if rollout.excluded_devices.contains(device_id) {
return false;
}
// Check percentage
let bucket = device_to_bucket(device_id);
bucket <= rollout.percentage
}
}---
Update Channels
Channel Configuration
// tauri.conf.json for different channels
{
"tauri": {
"updater": {
"endpoints": [
"https://releases.myapp.com/{{channel}}/{{target}}/{{arch}}/{{current_version}}"
]
}
}
}Channel Selection
use std::fs;
use directories::ProjectDirs;
pub fn get_update_channel() -> String {
let dirs = ProjectDirs::from("com", "company", "app").unwrap();
let config_path = dirs.config_dir().join("channel.txt");
fs::read_to_string(config_path)
.unwrap_or_else(|_| "stable".to_string())
.trim()
.to_string()
}
#[tauri::command]
pub fn set_update_channel(channel: String) -> Result<(), String> {
let valid_channels = ["stable", "beta", "nightly"];
if !valid_channels.contains(&channel.as_str()) {
return Err("Invalid channel".to_string());
}
let dirs = ProjectDirs::from("com", "company", "app").unwrap();
let config_path = dirs.config_dir().join("channel.txt");
fs::write(config_path, &channel)
.map_err(|e| e.to_string())
}Channel-Specific Manifests
# Directory structure
releases/
├── stable/
│ ├── darwin-x86_64/
│ │ └── latest.json
│ ├── darwin-aarch64/
│ ├── linux-x86_64/
│ └── windows-x86_64/
├── beta/
│ └── ...
└── nightly/
└── ...---
Rollback Mechanisms
Automatic Rollback on Failure
use std::path::PathBuf;
use std::fs;
pub struct RollbackManager {
backup_dir: PathBuf,
current_version: String,
}
impl RollbackManager {
pub fn new(backup_dir: PathBuf, current_version: String) -> Self {
Self { backup_dir, current_version }
}
pub async fn perform_update_with_rollback<F, Fut>(
&self,
update_fn: F,
) -> Result<(), UpdateError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<(), UpdateError>>,
{
// Create backup
let backup_path = self.create_backup()?;
// Attempt update
match update_fn().await {
Ok(()) => {
// Verify update
if self.verify_update() {
// Cleanup old backup after grace period
self.schedule_backup_cleanup(backup_path);
Ok(())
} else {
// Rollback if verification fails
self.restore_backup(&backup_path)?;
Err(UpdateError::VerificationFailed)
}
}
Err(e) => {
// Rollback on error
self.restore_backup(&backup_path)?;
Err(e)
}
}
}
fn create_backup(&self) -> Result<PathBuf, UpdateError> {
let backup_path = self.backup_dir.join(format!(
"backup_{}_{}",
self.current_version,
chrono::Utc::now().timestamp()
));
// Copy current installation to backup
let app_path = std::env::current_exe()?;
let app_dir = app_path.parent().unwrap();
copy_dir_all(app_dir, &backup_path)?;
Ok(backup_path)
}
fn restore_backup(&self, backup_path: &PathBuf) -> Result<(), UpdateError> {
let app_path = std::env::current_exe()?;
let app_dir = app_path.parent().unwrap();
// Restore from backup
copy_dir_all(backup_path, app_dir)?;
Ok(())
}
fn verify_update(&self) -> bool {
// Run basic health checks
// - Check binary exists and runs
// - Verify critical files
// - Test basic functionality
true
}
fn schedule_backup_cleanup(&self, backup_path: PathBuf) {
// Keep backup for 7 days before cleanup
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(7 * 24 * 60 * 60));
let _ = fs::remove_dir_all(backup_path);
});
}
}
fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> Result<(), std::io::Error> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
}
Ok(())
}---
Differential Updates
Delta Update Implementation
// Use bidiff/bsdiff for creating patches
use bidiff::{diff, patch};
use std::io::{Read, Write};
pub fn create_delta(old_file: &[u8], new_file: &[u8]) -> Vec<u8> {
let mut patch = Vec::new();
diff(old_file, new_file, &mut patch).unwrap();
patch
}
pub fn apply_delta(old_file: &[u8], patch: &[u8]) -> Vec<u8> {
let mut new_file = Vec::new();
bidiff::patch(old_file, patch, &mut new_file).unwrap();
new_file
}
// CI job to generate deltas
// For each release:
// 1. Get previous release artifact
// 2. Generate delta patch
// 3. Sign the patch
// 4. Upload patch alongside full artifactDelta-Aware Manifest
{
"version": "1.2.0",
"platforms": {
"darwin-x86_64": {
"signature": "...",
"url": "https://releases.myapp.com/MyApp_1.2.0.tar.gz",
"size": 50000000,
"deltas": [
{
"from_version": "1.1.0",
"url": "https://releases.myapp.com/MyApp_1.1.0_to_1.2.0.delta",
"size": 5000000,
"signature": "..."
},
{
"from_version": "1.0.0",
"url": "https://releases.myapp.com/MyApp_1.0.0_to_1.2.0.delta",
"size": 15000000,
"signature": "..."
}
]
}
}
}---
Update Analytics
Telemetry Collection
use serde::Serialize;
#[derive(Serialize)]
pub struct UpdateEvent {
event_type: UpdateEventType,
device_id: String,
from_version: String,
to_version: String,
platform: String,
timestamp: String,
duration_ms: Option<u64>,
error: Option<String>,
}
#[derive(Serialize)]
pub enum UpdateEventType {
CheckStarted,
UpdateAvailable,
DownloadStarted,
DownloadComplete,
InstallStarted,
InstallComplete,
InstallFailed,
Rollback,
}
pub async fn report_update_event(event: UpdateEvent) {
// Send to analytics endpoint
let client = reqwest::Client::new();
let _ = client
.post("https://analytics.myapp.com/updates")
.json(&event)
.send()
.await;
}Health Monitoring
pub async fn check_update_health(version: &str) -> UpdateHealth {
// Query analytics for this version
let stats = get_version_stats(version).await;
UpdateHealth {
version: version.to_string(),
total_attempts: stats.total,
successful: stats.successful,
failed: stats.failed,
rollbacks: stats.rollbacks,
success_rate: stats.successful as f64 / stats.total as f64,
// Halt rollout if success rate drops
should_continue: stats.successful as f64 / stats.total as f64 > 0.95,
}
}---
Background Updates
Silent Background Check
use std::time::Duration;
use tokio::time::interval;
pub async fn start_background_update_checker(app: tauri::AppHandle) {
let mut interval = interval(Duration::from_secs(3600)); // Check every hour
loop {
interval.tick().await;
match app.updater().check().await {
Ok(update) if update.is_update_available() => {
// Notify frontend about available update
let _ = app.emit_all("update-available", UpdateInfo {
version: update.latest_version().to_string(),
notes: update.body().map(|s| s.to_string()),
});
// Optionally download in background
if should_auto_download() {
let _ = update.download().await;
let _ = app.emit_all("update-downloaded", ());
}
}
Err(e) => {
log::warn!("Background update check failed: {}", e);
}
_ => {}
}
}
}User Preferences
#[derive(serde::Deserialize, serde::Serialize)]
pub struct UpdatePreferences {
pub auto_check: bool,
pub auto_download: bool,
pub auto_install: bool,
pub check_interval_hours: u32,
pub channel: String,
}
impl Default for UpdatePreferences {
fn default() -> Self {
Self {
auto_check: true,
auto_download: true,
auto_install: false, // Require user confirmation to install
check_interval_hours: 24,
channel: "stable".to_string(),
}
}
}---
Update UI Patterns
Progress Reporting
#[tauri::command]
async fn download_update(
app: AppHandle,
window: Window,
) -> Result<(), String> {
let update = app.updater().check().await.map_err(|e| e.to_string())?;
if !update.is_update_available() {
return Ok(());
}
// Download with progress
update
.download(|downloaded, total| {
let progress = if total > 0 {
(downloaded as f64 / total as f64) * 100.0
} else {
0.0
};
let _ = window.emit("update-progress", progress);
})
.await
.map_err(|e| e.to_string())?;
let _ = window.emit("update-ready", ());
Ok(())
}Frontend Component
// React component for update UI
function UpdateNotification() {
const [updateAvailable, setUpdateAvailable] = useState(false);
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<'idle' | 'downloading' | 'ready'>('idle');
useEffect(() => {
const unlisten = listen('update-available', (event) => {
setUpdateAvailable(true);
});
const unlistenProgress = listen('update-progress', (event) => {
setProgress(event.payload as number);
setStatus('downloading');
});
const unlistenReady = listen('update-ready', () => {
setStatus('ready');
});
return () => {
unlisten.then(fn => fn());
unlistenProgress.then(fn => fn());
unlistenReady.then(fn => fn());
};
}, []);
if (!updateAvailable) return null;
return (
<div className="update-notification">
{status === 'idle' && (
<button onClick={() => invoke('download_update')}>
Download Update
</button>
)}
{status === 'downloading' && (
<progress value={progress} max={100} />
)}
{status === 'ready' && (
<button onClick={() => invoke('install_update')}>
Restart to Update
</button>
)}
</div>
);
}Auto-Update Systems Security Examples
Key Generation and Management
Generating Update Signing Keys
# Generate Tauri signing key pair
npm run tauri signer generate -- -w ~/.tauri/myapp.key
# This creates:
# - ~/.tauri/myapp.key (private key - KEEP SECRET)
# - Public key output (embed in tauri.conf.json)
# Example output:
# Please enter a password to protect the secret key:
# Password: ********
#
# Your keypair was generated successfully
# Private: ~/.tauri/myapp.key
# Public: dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6...
#
# IMPORTANT: Store the private key securely!Storing Keys in CI/CD
# GitHub Actions secrets required:
# TAURI_PRIVATE_KEY: contents of ~/.tauri/myapp.key
# TAURI_KEY_PASSWORD: password used during key generation
jobs:
build:
runs-on: ubuntu-latest
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
steps:
- uses: actions/checkout@v4
- run: npm run tauri build
# Tauri automatically signs the update artifactKey Rotation Procedure
// 1. Generate new key pair
// npm run tauri signer generate -- -w ~/.tauri/myapp-new.key
// 2. Update tauri.conf.json with new public key
{
"tauri": {
"updater": {
"pubkey": "NEW_PUBLIC_KEY_HERE"
}
}
}
// 3. Build and release new version with new key
// This version becomes the "transitional" version
// 4. Update CI secrets with new private key
// Update TAURI_PRIVATE_KEY secret
// 5. All subsequent releases use new key
// Old versions can still update to transitional version
// Transitional version and newer use new key
// 6. After sufficient time, deprecate old key---
Signature Verification
How Tauri Verifies Signatures
// Tauri uses minisign format for signatures
// Verification happens automatically when:
// 1. pubkey is configured in tauri.conf.json
// 2. Update manifest contains signature field
// 3. Downloaded artifact is verified before installation
// The signature in the manifest is base64-encoded minisign signature
// Example signature format:
// untrusted comment: signature from tauri secret key
// RUTYyBCGAMv1234... (base64-encoded signature)Manual Signature Verification
# Verify signature manually using minisign
# Install minisign: brew install minisign / apt install minisign
# Create public key file from base64
echo "dW50cnVzdGVkIGNvbW1lbnQ6..." | base64 -d > myapp.pub
# Verify signature
minisign -Vm MyApp_1.0.0.tar.gz -p myapp.pub
# Should output: Signature and comment signature verifiedTesting Invalid Signatures
#[cfg(test)]
mod signature_tests {
#[tokio::test]
async fn test_invalid_signature_rejected() {
let manifest = r#"{
"version": "1.0.1",
"platforms": {
"darwin-x86_64": {
"url": "https://example.com/update.tar.gz",
"signature": "INVALID_SIGNATURE_HERE"
}
}
}"#;
let result = verify_and_install(manifest).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("signature"));
}
#[tokio::test]
async fn test_missing_signature_rejected() {
let manifest = r#"{
"version": "1.0.1",
"platforms": {
"darwin-x86_64": {
"url": "https://example.com/update.tar.gz"
}
}
}"#;
let result = verify_and_install(manifest).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_tampered_artifact_rejected() {
// Even with valid signature in manifest,
// if artifact is tampered, verification fails
let mock_server = MockServer::new();
mock_server.serve_tampered_artifact();
let result = download_and_verify(&mock_server.url()).await;
assert!(result.is_err());
}
}---
Secure Update Endpoints
HTTPS Configuration
// tauri.conf.json
{
"tauri": {
"updater": {
"active": true,
"pubkey": "YOUR_PUBLIC_KEY",
"endpoints": [
// Primary endpoint
"https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}",
// Fallback endpoint
"https://cdn.myapp.com/releases/{{target}}/{{arch}}/{{current_version}}"
]
}
}
}CDN Configuration for Updates
# Cloudflare Pages / AWS CloudFront configuration
# Serve update manifests with appropriate headers
# Example Cloudflare _headers file
/releases/*
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=300 # 5 minutes
Content-Type: application/json
/*.tar.gz
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400 # 24 hoursUpdate Server Implementation
// Simple update server with Actix-web
use actix_web::{web, App, HttpServer, HttpResponse};
use serde::Serialize;
#[derive(Serialize)]
struct UpdateManifest {
version: String,
notes: String,
pub_date: String,
platforms: std::collections::HashMap<String, PlatformUpdate>,
}
#[derive(Serialize)]
struct PlatformUpdate {
signature: String,
url: String,
}
async fn get_update(
path: web::Path<(String, String, String)>,
) -> HttpResponse {
let (target, arch, current_version) = path.into_inner();
// Check if update is available
let latest = get_latest_version();
if semver::Version::parse(¤t_version).unwrap()
>= semver::Version::parse(&latest).unwrap()
{
return HttpResponse::NoContent().finish();
}
// Return update manifest
let manifest = build_manifest(&target, &arch, &latest);
HttpResponse::Ok().json(manifest)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/updates/{target}/{arch}/{version}", web::get().to(get_update))
})
.bind("127.0.0.1:8080")?
.run()
.await
}---
Manifest Security
Secure Manifest Structure
{
"version": "1.2.0",
"notes": "Security update: fixes CVE-2024-XXXX",
"pub_date": "2024-01-15T12:00:00Z",
"platforms": {
"darwin-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVS...",
"url": "https://releases.myapp.com/v1.2.0/MyApp_1.2.0_x64.app.tar.gz",
"with_elevated_task": false
},
"darwin-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVS...",
"url": "https://releases.myapp.com/v1.2.0/MyApp_1.2.0_aarch64.app.tar.gz"
},
"linux-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVS...",
"url": "https://releases.myapp.com/v1.2.0/MyApp_1.2.0_amd64.AppImage.tar.gz"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVS...",
"url": "https://releases.myapp.com/v1.2.0/MyApp_1.2.0_x64-setup.nsis.zip"
}
}
}Manifest Generation in CI
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download Build Artifacts
uses: actions/download-artifact@v3
- name: Generate Manifest
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
# Get signatures from .sig files
DARWIN_X64_SIG=$(cat darwin-x86_64/*.sig)
DARWIN_ARM_SIG=$(cat darwin-aarch64/*.sig)
LINUX_SIG=$(cat linux-x86_64/*.sig)
WINDOWS_SIG=$(cat windows-x86_64/*.sig)
# Generate manifest
cat > latest.json << EOF
{
"version": "$VERSION",
"notes": "$(git log -1 --pretty=%B)",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-x86_64": {
"signature": "$DARWIN_X64_SIG",
"url": "https://releases.myapp.com/v$VERSION/MyApp_${VERSION}_x64.app.tar.gz"
},
"darwin-aarch64": {
"signature": "$DARWIN_ARM_SIG",
"url": "https://releases.myapp.com/v$VERSION/MyApp_${VERSION}_aarch64.app.tar.gz"
},
"linux-x86_64": {
"signature": "$LINUX_SIG",
"url": "https://releases.myapp.com/v$VERSION/MyApp_${VERSION}_amd64.AppImage.tar.gz"
},
"windows-x86_64": {
"signature": "$WINDOWS_SIG",
"url": "https://releases.myapp.com/v$VERSION/MyApp_${VERSION}_x64-setup.nsis.zip"
}
}
}
EOF
- name: Upload Manifest
run: |
# Upload to your update server
aws s3 cp latest.json s3://releases-bucket/latest.json---
Version Validation
Preventing Downgrade Attacks
// Tauri prevents downgrades by default
// The updater only installs if new_version > current_version
// Custom version validation
fn is_valid_update(current: &str, new: &str) -> bool {
let current = semver::Version::parse(current).unwrap();
let new = semver::Version::parse(new).unwrap();
// Must be newer version
if new <= current {
return false;
}
// Don't skip major versions (optional policy)
if new.major > current.major + 1 {
return false;
}
true
}Version Pinning for Testing
// For testing specific versions
{
"tauri": {
"updater": {
"endpoints": [
"https://releases.myapp.com/test/1.2.0-beta.json"
]
}
}
}---
Emergency Update Procedures
Force Update for Critical Security Issues
#[tauri::command]
async fn check_for_critical_updates(app: AppHandle) -> Result<bool, String> {
// Check for critical security updates
let response = reqwest::get("https://api.myapp.com/security/critical")
.await
.map_err(|e| e.to_string())?;
let critical: CriticalUpdate = response.json().await.map_err(|e| e.to_string())?;
if critical.affects_version(&app.package_info().version.to_string()) {
// Show mandatory update dialog
let _ = app.emit_all("critical-update", &critical);
return Ok(true);
}
Ok(false)
}
#[derive(serde::Deserialize, serde::Serialize)]
struct CriticalUpdate {
min_version: String,
message: String,
cve: Option<String>,
}Disable Auto-Update in Emergency
// Update manifest to disable updates temporarily
{
"version": "0.0.0",
"notes": "Updates temporarily disabled",
"platforms": {}
}Auto-Update Systems Threat Model
Asset Identification
Primary Assets
1. Update Signing Keys - Private keys for signing updates 2. Update Artifacts - Compiled application binaries 3. Update Infrastructure - Servers hosting updates 4. User Installations - End-user application instances
Secondary Assets
1. Update Manifests - Version and signature information 2. Public Keys - Embedded in application 3. Update Logs - Analytics and error data
---
Threat Actors
| Actor | Motivation | Capabilities | Target |
|---|---|---|---|
| Nation State | Espionage, sabotage | Supply chain compromise | Infrastructure |
| Cybercriminal | Ransomware, theft | Malware distribution | All users |
| Competitor | Sabotage | DoS, reputation damage | Update availability |
| Insider | Various | Direct access to keys | Signing infrastructure |
| Network Attacker | Data theft | MITM attacks | Update traffic |
---
Attack Vectors & Mitigations
1. Signing Key Compromise
Threat: Attacker obtains private signing key and distributes malicious updates.
Impact: CRITICAL - All users can be compromised
Attack Scenarios:
- Key stolen from developer machine
- Key leaked in CI logs or artifacts
- Key extracted from memory during signing
- Insider theft
CVE Example: This is the ultimate goal of many CVEs like CVE-2024-39698 (signature bypass)
Mitigations:
| Control | Implementation | Effectiveness |
|---|---|---|
| HSM storage | Store keys in hardware security module | Very High |
| CI-only access | Never on developer machines | High |
| Key encryption | Password-protected keys | Medium |
| Access logging | Audit all key usage | High |
| Key rotation | Regular rotation procedure | Medium |
Detection:
- Monitor for unexpected signed artifacts
- Alert on CI jobs outside normal times
- Check for key access from unusual IPs
2. Man-in-the-Middle Attack
Threat: Attacker intercepts update traffic and serves malicious update.
Attack Scenarios:
- Compromised network (public WiFi, corporate proxy)
- DNS hijacking
- BGP hijacking
- Rogue certificate authority
Mitigations:
| Control | Implementation | Effectiveness |
|---|---|---|
| Signature verification | Ed25519 signatures | Critical |
| HTTPS only | TLS 1.2+ required | High |
| Certificate pinning | Pin update server cert | Very High |
| Multiple endpoints | Failover to different CDN | Medium |
Implementation:
// Even if MITM succeeds in serving malicious file,
// signature verification will fail because attacker
// doesn't have the private key to sign their payload3. Update Server Compromise
Threat: Attacker gains access to update infrastructure and modifies hosted files.
Attack Scenarios:
- Server vulnerability exploitation
- Credential theft
- Supply chain attack on hosting provider
- DNS takeover
Mitigations:
| Control | Implementation | Effectiveness |
|---|---|---|
| Signature verification | Files signed before upload | Critical |
| Integrity monitoring | Hash verification of hosted files | High |
| Access controls | Minimal permissions | High |
| CDN separation | Separate signing from hosting | High |
Architecture:
Build Server -> Sign -> Upload to CDN
|
v
Audit Log
Update Server NEVER has access to signing keys4. Signature Bypass
Threat: Attacker exploits vulnerability to bypass signature verification.
CVE Examples:
- CVE-2024-39698: electron-updater environment variable expansion
- CVE-2020-electron-updater: Path traversal in signature verification
Attack Scenario:
// CVE-2024-39698 exploit
// Attacker crafts filename with environment variable
// %TEMP%/legitimate-signed-file.exe
// Signature check reads different file than installedMitigations:
| Control | Implementation | Effectiveness |
|---|---|---|
| Update dependencies | Latest electron-builder/Tauri | Critical |
| Defense in depth | Multiple verification layers | High |
| Security testing | Test signature bypass scenarios | High |
| Code audit | Review verification code | High |
5. Rollback Attack
Threat: Attacker forces installation of older vulnerable version.
Attack Scenarios:
- Serve old manifest with known vulnerable version
- Block updates to prevent security patches
- Corrupt update to trigger rollback to vulnerable version
Mitigations:
| Control | Implementation | Effectiveness |
|---|---|---|
| Version validation | Only allow upgrades, not downgrades | High |
| Minimum version | Enforce minimum acceptable version | High |
| Update monitoring | Alert if users on old versions | Medium |
| Force update | Mandatory updates for critical issues | High |
6. Denial of Service
Threat: Attacker prevents users from receiving updates.
Attack Scenarios:
- DDoS update servers
- DNS blocking
- Firewall rules in enterprise environments
- Corrupt manifest to crash updater
Mitigations:
| Control | Implementation | Effectiveness |
|---|---|---|
| CDN distribution | Distributed hosting | High |
| Multiple endpoints | Fallback URLs | High |
| Graceful degradation | App works without updates | Medium |
| Out-of-band updates | Alternative update channels | Medium |
---
Defense in Depth Strategy
Layer 1: Key Security
- HSM or secure CI-only storage
- Key encryption with strong password
- Access logging and alerting
- Regular rotation
Layer 2: Signing Process
- Isolated signing environment
- Deterministic builds
- Signature verification after signing
- Audit trail
Layer 3: Distribution Security
- HTTPS only endpoints
- CDN with access controls
- Integrity monitoring
- Geographic distribution
Layer 4: Client Verification
- Ed25519 signature verification
- Version validation (no downgrades)
- Certificate pinning
- Checksum verification
Layer 5: Monitoring
- Update success/failure rates
- Version distribution analytics
- Error logging and alerting
- Security event monitoring
---
Incident Response
Compromised Signing Key
Immediate (0-1 hour): 1. Revoke/rotate compromised key 2. Stop all update distribution 3. Alert security team 4. Begin investigation
Short-term (1-24 hours): 1. Generate new key pair 2. Build new release with new key 3. Analyze what was signed with compromised key 4. Notify users if malicious updates distributed
Long-term (1-7 days): 1. Forensic analysis of compromise 2. Improve key protection 3. User communication 4. Post-mortem documentation
Malicious Update Distributed
Immediate: 1. Take down update servers 2. Push emergency "null" update to stop downloads 3. Alert all hands
Short-term: 1. Identify affected version range 2. Notify users to not run affected versions 3. Push clean update 4. Provide remediation tools
Long-term: 1. Full incident post-mortem 2. Improve detection capabilities 3. Legal/regulatory notifications 4. User compensation if applicable
---
Security Monitoring
Key Metrics
| Metric | Alert Threshold | Action |
|---|---|---|
| Update failures | >5% over 1 hour | Investigate |
| Unknown versions | Any | Security review |
| Old versions | >10% of users | Prompt update |
| Signature errors | Any | Immediate investigation |
Log Events to Monitor
pub enum SecurityEvent {
UpdateCheckStarted { version: String },
UpdateAvailable { new_version: String },
SignatureVerified { artifact: String },
SignatureInvalid { artifact: String, error: String },
UpdateInstalled { version: String },
UpdateFailed { version: String, error: String },
DowngradeAttempt { from: String, to: String },
UnusualUpdateSource { url: String },
}---
Compliance Considerations
Code Signing Requirements
- Windows SmartScreen: Requires signed executables
- macOS Gatekeeper: Requires notarized apps
- Enterprise deployment: May require internal CA
Audit Trail Requirements
For regulated industries (healthcare, finance):
- Log all update activities
- Retain logs for compliance period
- Tamper-evident logging
- Access controls on logs
---
Security Checklist
Build Time
- [ ] Signing keys in HSM or CI secrets
- [ ] Key password in separate secret
- [ ] Builds are reproducible
- [ ] Artifacts signed before upload
Distribution
- [ ] HTTPS only endpoints
- [ ] CDN with access controls
- [ ] Multiple fallback endpoints
- [ ] Integrity monitoring enabled
Client
- [ ] Public key embedded correctly
- [ ] Signature verification cannot be bypassed
- [ ] Version validation prevents downgrades
- [ ] Error handling doesn't skip verification
Operations
- [ ] Update success rate monitored
- [ ] Version distribution tracked
- [ ] Security event alerting
- [ ] Incident response plan documented