Security Configuration
Learn how to lock down your Tauri v2 app using the principle of least privilege, capabilities, permissions, and CSP to protect your users and system
When you ship a Tauri app, you give the frontend — your React components — a bridge into the native operating system. That bridge is powerful, and without configuration it stays completely closed. Every action the frontend wants to take on the system must be explicitly granted. That is the security model Tauri v2 uses, and configuring it correctly is what keeps a compromised script inside the WebView from turning into a compromised machine.
This section covers the mechanisms that enforce that model: capabilities, permissions, scopes, the Content Security Policy, and the rules you apply to shape them. Everything here builds on the capability files introduced earlier in Capability Files (which map permissions to windows). Now we look at the security decisions behind those files and how to make them safely.
Principle of Least Privilege
The principle of least privilege means giving code exactly the permissions it needs — and nothing more. In Tauri, that translates to three concrete habits: grant the narrowest permission that still does the job, restrict it to only the windows that genuinely need it, and scope it so that even a compromised window can only touch a tiny surface of the system.
A permission like fs:default sounds convenient. It hands the frontend safe file system operations — reading, writing, checking existence — but it does so with no path restrictions. If any dependency you pull into your React app ever runs malicious JavaScript, that script can immediately read and write any file the app's own process can reach. Pair this with a strict Content Security Policy.
A better capability file starts with the opposite mindset: pick individual allow- permissions and pair them with scopes.
Here is a capability that applies the principle of least privilege to a file system operation. It lets the main window read only files inside the user’s documents directory and explicitly denies a sensitive subfolder.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "secure-fs",
"description": "Restricted file read access for the main window",
"windows": ["main"],
"permissions": [
"fs:allow-read",
"fs:allow-exists"
],
"scope": {
"fs:allow-read": {
"allow": [
"$HOME/Documents/**"
],
"deny": [
"$HOME/Documents/secret/**"
]
}
}
}
This capability cannot write, cannot list directories, and cannot touch anything outside $HOME/Documents. The deny clause carves out the secret subfolder even inside the allowed area — so even if an attacker gains control of the frontend, that folder remains off‑limits. The mental model is a set of keys: each window gets a key ring, and you decide exactly which keys go on it and which doors they open.
Avoid Wildcard Windows:
Using "windows": ["*"] hands the permissions to every window and webview, including any you add later. Prefer explicit window labels unless you have a window that is genuinely universal.
Restricting Permissions
A permission identifier looks like plugin:permission — for example window:allow-set-title. Tauri’s core plugins ship with a long list of them, and every plugin you add brings its own. The job of a security‑conscious developer is to review that list and pick only the ones the app actually uses, not what seems safe.
The *:default permissions (like window:default) are convenience bundles. They enable the most common operations for a plugin, but they often include commands you might never need. For instance, window:default grants allow-set-title and allow-close and allow-show and many others — even if your app never programmatically closes a window from the frontend. Dropping window:default in favor of individual window:allow-* permissions keeps the attack surface narrower.
Scoping a Permission
Some permissions accept a scope that further limits what the command can operate on. The file system plugin’s fs:allow-read is the clearest example: without a scope it reads nothing; with a scope you give it a list of allowed and denied paths, as shown above.
Scoping also applies to other plugins — for example, the shell plugin can be scoped to only allow specific commands. Always check the plugin’s documentation to see which permissions support scoping and what format they expect.
Denying a Permission
You can explicitly deny a permission that might otherwise be granted by a broader rule or by a plugin’s default set. Denial always overrides allowance. In the capability file above, deny ensures the secret folder stays unreachable even though it lives inside an allowed directory. This is critical for defense‑in‑depth: if a future update accidentally widens a scope, the deny rule blocks it.
Missing Permission Equals Runtime Error:
If a command is called from the frontend but no capability grants its permission, Tauri throws a runtime error in the WebView console. The app will not silently fail — the call will be blocked, and the user (or developer) will see something like doggy:bark not allowed. Plugin did not define its manifest. That is Tauri working correctly; fix the capability, not the security model.
Secure Defaults
Tauri v2 ships with a set of defaults that already close the most obvious doors:
- No capabilities are active by default. Unless you define a capability file or inline a capability object and reference it in
tauri.conf.json, the frontend has access to no Tauri APIs at all — not even the window title. This forces you to explicitly choose every permission. - Commands you register are restricted. By default, every command registered with
tauri::Builder::invoke_handleris available to all windows. But you can lock this down by listing only the allowed commands in the build script usingAppManifest::commands. Once you do, unlisted commands become unreachable regardless of capability files. This is the strongest guardrail available. - Remote sources are blocked. A capability applies only to local, bundled code by default. A script loaded from a remote URL (like a CDN or a server‑side rendered page) cannot call Tauri APIs unless you explicitly add a
remoteobject to the capability. This prevents a compromised remote resource from reaching into the native layer. - The WebView itself is sandboxed. The system WebView (WebKit on macOS, Edge WebView2 on Windows, etc.) runs with its own OS‑level restrictions. Tauri does not relax those; it layers additional application‑level controls on top.
These defaults mean a freshly created Tauri v2 project is, by default, a locked box. The rest of the security configuration work is about opening only the tiny windows you need.
You Start Secure:
If you create a new Tauri v2 app and try to call getCurrentWindow().setTitle("Hello") from React without any capability, you will see an error. That’s the default working. Once you add window:allow-set-title to a capability, the call succeeds. You are always starting from a safe position.
Configuring Content Security Policy (CSP)
The Content Security Policy is a browser‑level mechanism that tells the WebView which sources of scripts, styles, and connections it is allowed to use. A strong CSP prevents cross‑site‑scripting (XSS) attacks from injecting malicious code into your app — even if an attacker manages to slip a <script> tag into the DOM.
Tauri sets a default CSP that is more permissive than you might want in production. You should override it with a strict policy tailored to your frontend stack. The configuration lives under app.security.csp in tauri.conf.json.
{
"app": {
"security": {
"csp": {
"default-src": "'self'",
"script-src": "'self'",
"style-src": "'self' 'unsafe-inline'",
"connect-src": "'self' ipc: http://ipc.localhost",
"img-src": "'self' asset: http://asset.localhost blob: data:",
"font-src": "'self' https://fonts.gstatic.com"
}
}
}
}
This policy tells the WebView:
- Only load scripts from the app’s own origin (
'self'). That means no external CDN scripts can execute, even if they are referenced in your HTML. - Styles may come from
'self'plus inline<style>blocks ('unsafe-inline'). Many CSS‑in‑JS libraries require inline styles; remove'unsafe-inline'if you can use hashes instead. - Network connections are allowed only to the app’s origin, the
ipc:protocol (required for Tauri IPC), andhttp://ipc.localhost(used internally). - Images may load from the app’s assets and from
blob:anddata:URIs — typical for dynamically generated images. - Fonts may come from the app itself or from Google Fonts.
CSP Errors Are Silent in Production:
The WebView reports CSP violations to the developer console during tauri dev. In a built app, those violations just block the resource silently. A missing connect-src entry might break IPC without an obvious error. Always test with the console open and watch for CSP violation messages.
A common mistake is forgetting that Tauri’s IPC uses custom URL schemes (ipc: and http://ipc.localhost). If your CSP does not include them, every API call will be blocked with a network error that looks like a connectivity problem. The example above includes them explicitly.
Adding a Custom Command with Permission
Tauri commands are Rust functions exposed to the frontend. Creating one and securing it properly involves four steps: write the command, register it in the Rust backend, define a permission for it, and add that permission to a capability. This walkthrough shows a minimal command that reads a greeting from a Rust string and returns it to the React frontend.
Step 1: Write the Rust command and register it
Create a command function and add it to the Tauri builder. To limit which commands are available by default, use the build script to declare the allowed commands list.
// Prevents an additional console window on Windows in release builds
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust.", name)
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn main() {
tauri_build::try_build(
tauri_build::Attributes::new()
.app_manifest(tauri_build::AppManifest::new().commands(&["greet"])),
)
.unwrap();
}
The AppManifest::commands call restricts the app so that only the greet command is considered registered. Any other command you add later will be ignored by the frontend unless you add it to this list. This is the ultimate backstop: even a misconfigured capability cannot grant access to an unregistered command.
Step 2: Create a permission for the command
Permissions live in the src-tauri/permissions directory. Create a file that defines a unique identifier for the greet command.
[[permission]]
identifier = "greet:allow-greet"
description = "Allows the greet command to be called from the frontend."
commands.allow = ["greet"]
The identifier follows the pattern plugin-or-app:action. Here greet:allow-greet is a single permission that enables exactly one command. You could also create a greet:default permission that bundles multiple commands for convenience, but for least privilege a single‑command permission is preferable.
Step 3: Add the permission to a capability
Now that the permission exists, grant it to a specific window inside a capability file. This file can be a new one or an existing capability — we’ll create one dedicated to the greet command.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "greet-capability",
"description": "Allows the greet command in the main window",
"windows": ["main"],
"permissions": [
"greet:allow-greet"
]
}
Finally, reference this capability in tauri.conf.json if you are not auto‑activating all capabilities (auto‑activation is the default when capabilities are present in the capabilities directory). Explicit referencing is safer.
{
"app": {
"security": {
"capabilities": ["greet-capability"]
}
}
}
Capability Not Referenced:
If your tauri.conf.json explicitly lists capabilities, any capability file not listed will be ignored. The command will fail at runtime even though the permission file exists. Either list every capability explicitly, or remove the security.capabilities array entirely to let Tauri auto‑activate all files in the capabilities directory.
Step 4: Call the command from React
With the permission granted, the frontend can now invoke the command using the @tauri-apps/api package.
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
function App() {
const [greeting, setGreeting] = useState("");
const handleGreet = async () => {
try {
const response: string = await invoke("greet", { name: "Tauri User" });
setGreeting(response);
} catch (error) {
console.error("Failed to greet:", error);
}
};
return (
<div>
<button onClick={handleGreet}>Greet from Rust</button>
{greeting && <p>{greeting}</p>}
</div>
);
}
export default App;
If everything is wired correctly, clicking the button displays Hello, Tauri User! You've been greeted from Rust.. If the permission is missing, the console will show an error stating that greet is not allowed — exactly the protective behavior you want during development.
Verify Your Work:
After setting up the capability, run tauri dev and open the browser console (or attach a debugger). The absence of permission errors confirms that the command is reachable. Intentionally miss a permission to see the error, then add it — that builds confidence in the security model.
Remote API Access
Some applications load their frontend from a remote server — a brownfield pattern where Tauri wraps an existing web app. By default, remote scripts cannot access Tauri APIs. That is by design: if a remote server is compromised, the attacker cannot jump from the browser context into the native system.
To enable remote API access, add a remote object to the capability. This is the v2 equivalent of Tauri v1’s dangerousRemoteDomainIpcAccess setting, but more granular.
{
"$schema": "../gen/schemas/remote-schema.json",
"identifier": "remote-capability",
"description": "Allows Tauri API from a trusted subdomain",
"windows": ["main"],
"remote": {
"urls": ["https://app.trusted-domain.com"]
},
"platforms": ["macOS", "windows", "linux"],
"permissions": [
"core:default"
]
}
The urls array accepts patterns with wildcards, like https://*.tauri.app. Each entry defines a source that may call the listed permissions. The platform array can further restrict this to desktop only — mobile might use a different capability with no remote access.
Remote Access Expands Your Trust Boundary:
Once you open remote API access, the security of your app depends on the security of that remote server and its TLS configuration. If the server is compromised, every permission you grant becomes available to the attacker. Use this only when absolutely necessary, scope the permissions tightly, and combine it with a strong CSP that restricts script sources to the same trusted origin.
Security Recommendations
Beyond the configuration mechanics, a few habits significantly reduce risk over the lifetime of a Tauri application:
- Audit capabilities regularly. Every time you add a dependency or a new feature, review the capabilities directory. Permissions tend to accumulate; remove any that are no longer needed.
- Prefer per‑window capabilities. Separate sensitive operations into a dedicated window with its own capability, rather than granting broad permissions to the main window. A settings window that reads a config file needs
fs:allow-read; the main content window probably does not. - Use the isolation pattern for untrusted code. If your app loads third‑party plugins or user‑generated scripts, enable the isolation pattern (configured via
app.security.pattern.use: "isolation"in application configuration). It injects a sandboxed JavaScript layer between the frontend and Tauri Core, letting you inspect and reject IPC messages before they reach the native side. - Keep the Rust backend simple. The Rust code is your last line of defense. Avoid complex control flow in commands; validate every input as if it came from an untrusted source — because it effectively does.
- Sign your binaries. Distribute builds through Tauri’s updater with signed artifacts. Unsigned binaries can be replaced by attackers, bypassing all capability restrictions.
- Test against the CSP early. Run
tauri devwith the browser console open. CSP violations appear as warnings. Resolve them before shipping so that no resource silently breaks in production. - Scope file system access to the smallest possible paths. Use
$HOME/Documents/**instead of$HOME/**. Use explicit deny entries for anything sensitive, even if you think the allow list already excludes it.
Do Not Ignore Supply‑chain Risks:
Your frontend likely includes dozens of npm packages, any of which could be compromised. The capability system limits what a malicious package can do — but only if you have restricted permissions. Rely on the principle of least privilege, not on trust in your dependencies.
The security configuration in Tauri v2 is not a single setting you toggle on. It is the sum of every capability file, every permission identifier, every scope, and the CSP header you define. The defaults start you from a locked‑down state; the rest is about opening only the doors you truly need and leaving everything else sealed.