web

Isolated Web Apps

The web is a truly unique application platform. Apps built on it are instantly accessible on any operating system without any code changes or compiling needed. Whenever a user comes to your app, they always have the most up-to-date version. They’re installable and can work offline, are highly capable, and super shareable with just a link. Build a web application, and it just works, everywhere.

Because the web aims to be safe and secure by default, its security model needs to be very conservative. Any new capabilities added need to be safe for a casual user to accidentally come across through a URL. We call this security model the drive by web. While this is great for many applications, and can be made more secure using Content Security Policies and cross-origin isolation, it doesn’t work for every use-case. A number of really important, and very powerful APIs, like Direct Sockets and Controlled Frame, that developers need can’t be made safe enough for the drive by web.

For these applications, they don’t currently have an option for building on the web. For others, the web’s security model may not be conservative enough; they may not share the assumption that the server is trustworthy, and instead prefer discretely versioned and signed stand-alone applications. A new, high-trust security model is needed. Isolated Web Apps (IWAs) provide an isolated, bundled, versioned, signed, and trusted application model built on top of the existing web platform to enable these developers.

A spectrum of trust on the web

You can think of security and capabilities on the web in terms of a spectrum.

An illustration demonstrating the spectrum of trust on the web. On the left, a globe representing the drive-by web. In the middle, Progressive Web Apps. On the right, a fish bowl with a goldfish inside, representing Isolated Web Apps. A solid black line connects all three icons horizontally, and a dashed red line separates Progressive Web Apps from Isolated Web Apps

The drive-by web, on the left, has the lowest trust security model because it needs to be most accessible, and therefore has the least access to a user’s system. Browser-installed web apps, in the middle get a little more trust, and can integrate a little deeper into a user’s system. Users can generally switch between drive-by web versions of apps and browser-installed versions without problem.

Then there’s high-trust, Isolated Web Applications.

They act and feel more like native apps and can get access to deep system integrations and powerful capabilities. Users can’t jump between them and the drive-by-web. If you need this level of security, or these capabilities, there’s no going back.

When trying to decide where on this spectrum you should aim for, default to the lowest-trust security model that you can, like a Progressive Web App. This will give you the greatest reach, require you to manage the least amount of security concerns yourself, and will be the most flexible for your developers and users.

Secure by design

Isolated Web Apps provide a high-trusted security model for web applications. To enable that, though, some of the assumptions that the drive by web makes about trust need to be rethought. Core web building blocks, like servers and DNS, can no longer be explicitly trusted. Attack vectors that may seem more relevant for native apps suddenly become important. So, to gain access to the new high-trust security model provided by IWAs, web apps need to be packaged, isolated, and locked down.

Packaged

Pages and assets for Isolated Web Apps can’t be served from live servers or fetched over the network like normal web applications. Instead, to gain access to the new high-trust security model, web apps need to package all of the resources they need to run into a Signed WebBundle. Signed web bundles take all of the resources required to run a site and package them together into a .swbn file, concatenating them with an integrity block. This allows the web app to be securely downloaded in its entirety, and even shared or installed while offline.

This, however, poses a problem for verifying the authenticity of a site’s code: TLS keys require an internet connection to work. Instead of TLS keys, IWAs are signed with a key that can be kept securely offline. The good news is that, if you can gather all of your production files into a folder, you can turn it into an IWA without much modification.

Generating signing keys

Signing keys are Ed25519 or ECDSA P-256 key pairs, with the private key being used to sign the bundle and the public key used to verify the bundle. You can use OpenSSL to generate and encrypt an Ed25519 or ECDSA P-256 key:

# Generate an unencrypted Ed25519 key
openssl genpkey -algorithm Ed25519 -out private_key.pem

# or generate an unencrypted ECDSA P-256 key
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem 

# Encrypt the generated key.  This will ask for a passphrase, make sure to use a strong one
openssl pkcs8 -in private_key.pem -topk8 -out encrypted_key.pem

# Delete the unencrypted key
rm private_key.pem

Signing keys have a secondary purpose, too. Because a domain may be vulnerable to loss of control like a server, it can’t be used to identify the installed IWA. Instead, an IWA is identified by the bundle’s public key, which is part of its signature and tied to the private key, instead. This is a significant change to how the drive-by web works, so instead of using HTTPS, IWAs use a new scheme, too: isolated-app://.

Bundle your app

With your signing key available, it’s time to bundle your web app. To do so, you can use the official NodeJS packages to bundle then sign your IWAs (Go command-line tools are also available). First, use the, wbn package, pointing to the folder containing all of your IWA’s production files (here dist) to wrap them up into an unsigned bundle:

npx wbn --dir dist

This will generate an unsigned web bundle of that directory to out.wbn. Once generated, use the encrypted Ed25519 or ECDSA P-256 key you previously created to sign it using wbn-sign:

npx wbn-sign -i out.wbn -k encrypted_key.pem -o signed.swbn

This will generate a signed web bundle from the unsigned web bundle called signed.swbn. Once signed, the tool will also output the Web Bundle ID and its Isolated Web App origin. The Isolated Web App origin is how your IWA is identified in the browser.

Web Bundle ID: ggx2sheak3vpmm7vmjqnjwuzx3xwot3vdayrlgnvbkq2mp5lg4daaaic
Isolated Web App Origin: isolated-app://ggx2sheak3vpmm7vmjqnjwuzx3xwot3vdayrlgnvbkq2mp5lg4daaaic/

If you are using Webpack, Rollup, or a tool that supports their plugins (like Vite), you can use one of the bundler plugins (Webpack, Rollup) that wraps these packages instead of calling them directly. Doing so will generate a signed bundle as the output of your build.

Test your app

You can test your IWA in one of two ways: either by running your development server through Chrome’s built-in IWA developer proxy, or by installing your bundled IWA. To do so, you’ll need to be on Chrome or ChromeOS 120 or later, enable the IWA flags, and install your app through Chrome’s Web App Internals:

  1. Enable the chrome://flags/#enable-isolated-web-app-dev-mode flag
  2. Test your IWA by going to Chrome's Web App Internals page at chrome://web-app-internals

Once on the Web App Internals page, you have two choices: Install IWA via Dev Mode Proxy or Install IWA from Signed Web Bundle.

If you install through a Dev Mode Proxy, you can install any URL, including sites running from a local development server, as an IWA, without bundling them, presuming they meet the other IWA installation requirements. Once installed, an IWA for that URL will be added to your system with the correct security policies and restrictions in place and access to IWA-only APIs. It will be assigned a random identifier. Chrome Dev Tools is also available in this mode to help you debug your application. If you install from a Signed Web Bundle, you’ll upload your signed, bundled IWA and it will install as if it had been downloaded by an end-user.

On the Web App Internals page, you can also force update checks for any applications installed through Dev Mode Proxy or from a Signed Web Bundle to test the update process, too.

Isolated

Trust is key to Isolated Web Apps. This starts with how they run. Users have different mental models for what an app can, and should be able to, do depending on whether it’s running in a browser or in a stand-alone window, generally believing that stand-alone apps have more access and are more powerful. Because IWAs can gain access to high-trust APIs, they’re required to run in a stand-alone window to align with this mental model. This visually separates them from the browser. But it goes further than visual separation.

Isolated Web Apps run on a separate protocol than in-browser websites (isolated-app vs http or https). This means each IWA is entirely separated from websites running in-browser, even if they’re built by the same company, thanks to the same-origin policy. IWA storage is also separated from each other. This together ensures that cross-origin content is guaranteed not to leak between different IWAs or between IWAs and a user’s normal browsing context.

But neither isolation nor bundling and signing a site’s code are useful for establishing trust if an IWA can download and run arbitrary code after installation. To ensure this while still allowing IWAs to connect to other sites for content, IWAs enforce a rigorous set of Content Security Policies:

Isolated Web Apps code content security policy
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; 
                         connect-src 'self' https: wss: blob: data:;
                         require-trusted-types-for 'script';
                         frame-src 'self' https: blob: data:;
                         img-src 'self' https: blob: data:;
                         media-src 'self' https: blob: data:;
                         font-src 'self' blob: data:;
                         style-src 'self' 'unsafe-inline';
                         object-src 'none';
                         base-uri 'none';
                         default-src 'self';

These CSPs aren’t enough to fully protect against potentially malicious third-party code. IWAs are also cross-origin isolated, setting headers to reduce the ability of third-party resources to affect them:

Isolated Web App cross-origin isolation headers and CSP
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
Content-Security-Policy: frame-ancestors 'self'

Even with these restrictions in place, there’s one more potential attack that IWAs guard against: sequence breaking attacks. A sequence breaking attack happens when malicious third-party content attempts to create a confusing and potentially exploitable user experience by navigating to a page in an unexpected way, like navigating directly to an internal settings page. IWAs prevent this by disallowing arbitrary deep-linking from external sites, only allowing applications to be opened by navigating to well-defined entry points, like a start_url, a protocol handler, a share target, or through a launch handler.

Locked down

Packaging and isolation provide a set of guarantees around what is allowed to run and where it came from, but the dynamic nature of permissions on the web means they alone can’t ensure that a web application is only using the capabilities it needs. Because different capabilities have different security considerations, a user or administrator will want to audit what permissions an IWA may use, just like they can do with other native apps like Android and iOS before they install or update an app.

To facilitate this, Isolated Web Apps block all permission requests by default. Developers can then opt-in to the permission they need by adding a permissions_policy field to their Web App Manifest. This field contains key/value pairs of permission policy directives and permission policy allowlists for each permission the IWA, or any child frame like a Controlled Frame or an iframe, may request. Adding a permission here does not automatically grant it; instead it makes it available to be requested when a request for that capability is made.

Consider you’re building a fleet tracking IWA as an example. You may need your IWA to be able to request the user’s location, and for an embedded map to request location, too. You may also want any embedded site to be able to go fullscreen to give an immersive view for the user. To do so, you’d set up the following permission policy in your Web App Manifest:

Permission policy example for Isolated Web Apps
"permissions_policy": {
   "geolocation": [ "self", "https://map.example.com" ],
   "fullscreen": [ "*" ]
}

Because WebBundles can specify Permissions Policy headers, too, only permissions declared in both will be allowed, and then only origins in the allowlists that are in both will be allowed.

Named and versioned

Normal web apps rely on their domain name to identify themselves to users and can be updated by changing code that’s served at that domain. But because of the security constraints around Isolated Web Apps, identity and updates need to be handled differently. Much like Progressive Web Apps, Isolated Web Apps need a Web App Manifest file to identify them to your users.

Web app manifest

Isolated Web Apps share the same key manifest properties for their Web App Manifest as PWAs, with some slight variations. display, for instance, works a little differently; both browser and minimal-ui are forced into a minimal-ui display, and fullscreen and standalone are both forced into a standalone display (additional display_override options work as expected). In addition, there are two more fields that should be included, version and update_manifest_url:

  • version - Required for Isolated Web Apps. A string consisting of one or more integers separated by a dot (.). Your version can be something simple like 1, 2, 3, etc…, or something complex like SemVer (1.2.3). The version number should match the Regex ^(\d+.?)*\d$.
  • update_manifest_url - Optional, but recommended field that points to an HTTPS URL (or localhost for testing) where a Web Application Update Manifest can be retrieved.

A minimal Web App Manifest for an Isolated Web App may look something like this:

A minimal sample Web App Manifest for an Isolated Web App
{
  "name": "IWA Kitchen Sink",
  "version": "0.1.0",
  "update_manifest_url": "https://example.com/updates.json",
  "start_url": "/",
  "icons": [
    {
      "src": "/images/icon.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "any"
    },
    {
      "src": "/images/icon-mask.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "maskable"
    }
  ]
}

Web application update manifest

A Web Application Update Manifest is a JSON file that describes each version of a given web application. The JSON object contains one required field, version, which is a list of objects containing version, src, and channels:

  • version - The version number of the application, same as the Web App Manifest’s version field
  • src - The HTTPS URL (or localhost for testing) pointing to the hosted bundle for that version (the .swbn file). Relative URLs are relative to the Web Application Update Manifest file.
  • channels - A list of strings to identify the update channel this version is part of. A special default channel is used to describe the primary channel that will be used if no other channel is selected.

You can also include a channels field, an object of your channel IDs with an optional name property for each ID to provide a human-readable name (including for the default channel. A channel that doesn’t include the name property, or isn’t included in the channels object, uses its ID as its name.

A minimal update manifest may look something like this:

A sample Web Application Update Manifest for an Isolated Web App
{
  "versions": [
    {
      "version": "5.2.17",
      "src": "https://cdn.example.com/app-package-5.2.17.swbn",
      "channels": ["next", "5-lts", "default"]
    },
    {
      "version": "5.3.0",
      "src": "v5.3.0/package.swbn",
      "channels": ["next", "default"]
    },
    {
      "version": "5.3.1",
      "src": "v5.3.1/package.swbn",
      "channels": ["next"]
    },
  ],
  "channels": {
    "default": {
      "name": "Stable"
    },
    "5-lts": {
      "name": "5.x Long-term Stable"
    }
  }
}

In this example, there are three channels: default that will be labeled Stable, 5-lts that will be labeled 5.x Long-term Stable, and next that will be labeled next. If a user is on channel 5-lts, they’ll get version 5.2.17, if they’re on channel default they’ll get version 5.3.0, and if they’re on channel next they’ll get version 5.3.1.

Web Application Update Manifests can be hosted on any server. Updates are checked for every 4-6 hours.

Admin managed

For their initial launch, Isolated Web Apps will only be able to be installed on Chrome Enterprise managed Chromebooks by an administrator through the Admin panel.

To get started, from your Admin panel, go to Devices > Chrome > Apps & extensions > Users & browsers. This tab allows you to add apps and extensions from the Chrome Web Store, Google Play, and the web for users across your organization. Adding items is done by opening the yellow floating add (+) button on the bottom right corner of the screen and selecting the type of item to add.

When opened, there will be an icon of a square inside a square, labeled Add an Isolated Web App. Clicking on it will open a modal to add an IWA to you OU. To do so, you’ll need two pieces of information: the IWA’s Web Bundle ID (generated from your app’s public key and displayed after the app is bundled and signed) and the URL to the Web App Update Manifest for the IWA. Once installed, you’ll have the Admin panel’s standard set of options for managing it:

  • Installation policy - How you want the IWA installed, either force installed, force installed and pinned to the ChromeOS shelf, or prevent installation.
  • Launch on login - How you want the IWA launched, either allow a user to launch manually, force the IWA to launch when the user logs in, but let them close it, or force launch when the user logs in and prevent it from closing.

Once saved, the app will be installed the next time a policy update is applied to Chromebooks in that OU. Once installed, a user's device will check for updates from the Web App Update Manifest every 4-6 hours.

In addition to force-installing IWAs, you can also auto-grant some permissions for them in a similar way you can for other web applications. To do so, go Devices > Chrome > Web capabilities and click the Add origin button. In the Origin / site pattern field, paste in the IWA’s Web Bundle ID (isolated-app:// will automatically be added onto it as its protocol). From there, you can set access levels to different APIs (allowed/blocked/unset), including window management, local font management, and the screen monitoring API. For APIs that may require additionyal opt-in from an administrator to enable, like the mandatory screen monitoring API, an additional dialog will pop up to confirm your selection. Once you’re done, save your changes and your users will be ready to start using your IWA!

Working with extensions

While Isolated Web Apps don't work with extensions out of the box, you can connect extensions you own to them. To do so, you'll need to edit the extension's manifest file. The externally_connectable section of the manifest defines which external web pages or other Chrome Extensions your extension can interact with. Add your IWA's origin under the matches field within externally_connectable (be sure. to include the isolated-app:// scheme):

manifest.json
{
  "externally_connectable": {
    "matches": ["isolated-app://79990854-bc9f-4319-a6f3-47686e54ed29/*"]
  }
}

While this will allow your extension to run in the Isolated Web App, it won't allow it to inject content into it; you're limited to passing messages between the extension and your IWA.