La saga de la firma en el sandbox de macOS (o por qué existen las versiones de la 1.4.0 a la 1.4.5)
Si hoy has mirado nuestro changelog y has visto seis versiones de parche en un mismo día, quizá te preguntes qué ha pasado. La respuesta corta: la firma de código y el App Sandbox de macOS casi acaban con nosotros. Aquí va la historia completa.
Cómo funciona MailVault por dentro
MailVault es una app de escritorio hecha con Tauri v2. El frontend es React ejecutándose en una webview nativa, y el backend es un servidor Express de Node.js compilado a binario nativo con Bun. Ese binario (lo llamamos "sidecar") se ejecuta junto a la app principal y se encarga de toda la comunicación IMAP/SMTP.
Para distribuir en macOS, Apple exige que las apps estén firmadas con un certificado Developer ID y notarizadas. Además activamos el App Sandbox — la capa de seguridad de Apple que restringe a qué puede acceder la app en tu sistema. Es obligatorio para la Mac App Store y una buena práctica en cualquier app de escritorio.
El problema
Tras publicar la v1.4.0, descubrimos que el servidor sidecar no arrancaba en las compilaciones de publicación. La app abría bien, pero el backend se caía en silencio con el código de salida 133 (SIGTRAP). Todo funcionaba perfecto en desarrollo: solo se veía afectada la compilación firmada y con sandbox.
Hicieron falta varias iteraciones para ir pelando las capas:
- v1.4.0 — El servidor escuchaba en
0.0.0.0pero el frontend se conectaba alocalhost, con el consiguiente desajuste entre IPv4 e IPv6. Se resolvió escuchando explícitamente en127.0.0.1. - v1.4.1 — La ventana de "Añadir cuenta" usaba un
fetch('/api/test-connection')directo en lugar del helper de la API. En las compilaciones de publicación de Tauri, el origen de la webview estauri://localhost, nohttp://localhost— así que las URL relativas apuntan al sitio equivocado. - v1.4.2 — Faltaba el permiso
com.apple.security.network.server. El sandbox impedía que el sidecar escuchara en un puerto. - v1.4.3 — Teníamos una limpieza de procesos basada en la shell (
lsof,kill) que el App Sandbox bloquea. La sustituimos por el handleCommandChildde Tauri. - v1.4.4 — El culpable de verdad: los binarios compilados con Bun necesitan JIT (compilación al vuelo). El compilador JIT reserva memoria ejecutable en tiempo de ejecución. macOS tiene permisos para autorizarlo (
com.apple.security.cs.allow-jit), pero Tauri firma todos los binarios del paquete de la app con los mismos permisos usando--deep. El sidecar acababa con los permisos del App Sandbox, que bloqueaban el JIT aunque estuviera presente el flag allow-jit. - v1.4.5 — Arreglamos la cadena de firma. Ahora el sidecar se firma por separado con sus propios permisos (sin App Sandbox y con JIT permitido). La app principal conserva el App Sandbox. El orden de firma importa: primero el sidecar y después el paquete de la app principal sin
--deep, para preservar los permisos del sidecar. También descubrimos que nuestra cadena de CI subía el DMG sin firmar que genera Tauri automáticamente, en lugar del nuestro correctamente firmado.
El arreglo
La solución tiene dos partes:
- Permisos separados — El sidecar recibe
entitlements-sidecar.plist(JIT + red, sin sandbox). La app principal recibeentitlements.plist(sandbox + JIT + red). - Orden de firma correcto — Firma primero el sidecar con sus propios permisos y después el paquete
.appprincipal sin--deep. Con--deepse volvería a firmar recursivamente todo lo que hay dentro del paquete, sobrescribiendo los permisos del sidecar.
Cómo verificar la firma tú mismo
Si quieres comprobar que MailVault está bien firmado y notarizado en tu máquina, abre el Terminal y ejecuta estos comandos:
1. Verifica la firma de la app:
codesign --verify --verbose /Applications/MailVault.app
Deberías ver:
/Applications/MailVault.app: valid on disk /Applications/MailVault.app: satisfies its Designated Requirement
2. Comprueba los permisos de la app principal:
codesign -d --entitlements - /Applications/MailVault.app
Busca com.apple.security.app-sandbox con valor true — eso confirma que la app se ejecuta en un sandbox.
3. Comprueba los permisos del sidecar:
codesign -d --entitlements - /Applications/MailVault.app/Contents/MacOS/mailvault-server
El sidecar debe tener com.apple.security.cs.allow-jit, pero no debe tener com.apple.security.app-sandbox.
4. Verifica la notarización:
spctl --assess --type execute -v /Applications/MailVault.app
Deberías ver:
/Applications/MailVault.app: accepted source=Notarized Developer ID
5. Comprueba la notarización del DMG (antes de instalar):
spctl --assess --type open --context context:primary-signature -v ~/Downloads/MailVault-v1.4.5.dmg
Lecciones aprendidas
- Prueba pronto las compilaciones de publicación. Las compilaciones de desarrollo se saltan por completo la firma de código y el sandbox. La distancia entre "funciona en desarrollo" y "funciona en publicación" puede ser enorme.
- Los sidecars requieren un trato especial. Si empaquetas dentro de una app de Tauri un binario compilado con Bun, Node o Deno, necesita sus propios permisos. Tauri no admite permisos por binario de forma nativa — hay que volver a firmar después de compilar.
--deepes peligroso. Es cómodo, pero sobrescribe todas las firmas anidadas con los mismos permisos. Si distintos binarios necesitan permisos distintos, fírmalos uno a uno.- Un solo script de compilación para local y para CI. Tener una lógica de firma distinta para las compilaciones locales y para la CI hizo que la CI subiera el DMG equivocado, el que no estaba firmado. Ahora las dos usan el mismo script.
Si te afectaron las versiones rotas, sentimos las molestias. La v1.4.5 es la versión estable y firmada correctamente. Tus correos nunca corrieron peligro: el problema era solo que el binario de la app no arrancaba su servidor backend.