Engineering at MindLink

Hybrid Electron application deployment in the enterprise

March 14, 2019

Enterprises love control, that usually means they’re only going to install your software if there’s an MSI package that they can deploy through group policy.

This means that if you want a lovely auto-updating client so that you don’t have to maintain backwards compatible server code you’re stuck. Except there is a way forward!

TL;DR

By treating your Electron application as a browser shell and taking control of downloading the latest non-native assets from a target server (the “web app”), you can automatically sync the client version with the server version and do that securely by signing your non-native assets.

Background

It is inescapable that some form of forwards or backwards compatibility will be necessary, for MindLink we are forced to have a client that is backwards compatible with older servers since our mobile apps are app-store distributed and hence on a faster deployment cadence than the on-premise servers our customers deploy. If there’s a customer that is slow to deploy a new version, we don’t want all our other customers not getting the latest and greatest client.

This means that we don’t need a backwards compatible server, and that’s brilliant because that keeps code lean and up to date.

However, when we looked to package up our web app as a desktop application, suddenly we found a backwards compatible client is not enough. In an enterprise deployment often its the other way around, servers are updated first and then clients.

That means we either need to also maintain a backwards compatible server, or find a way to keep clients in-sync.

We know first hand the pain of keeping legacy code around for backwards compatibility - the choices available aren’t great when you’re talking about behavioural differences. Thats why we really want to avoid it in the server.

The modern deployment approach

Modern client deployment techniques follow an auto-update process. If you’re trying to use a really old WhatsApp then it’ll force you to install the latest update. Mobile apps and associated app stores have instilled this approach into consumers, and the same trend is happening on desktops with most services now in the cloud. Maintaining a cloud service that has to be compatible with many different client versions is an engineering overhead best avoided.

The classic auto-update approach is to replace the native installation. That’s great if you’re installing into user-space, but in an Enterprise world the app is natively installed and sysadmins don’t want an automatically installed native package that they haven’t got deployment control over.

You could implement a native bootloader instead, so you pull down a native payload in the bootloader and then launch that. There are security concerns around this approach and it’s possible that the native code execution is blocked by some security policy.

However there is a middle ground - do exactly what a browser does. After all, Enterprises don’t stop browsers from loading web sites or apps (provided they’re not blacklisted)!

The idea

In the Electron world you’re basically a browser renderer and engine without the native interop built-in (you implement those bits yourself). So here’s the idea:

  1. Have a versioned native interface
  2. Load a default application payload into Chromium in an isolated context
  3. Fetch the right application payload from the server, stick it in local user app data and load that payload into the isolated context instead
  4. We can remember which payload we last used to skip always loading the default payload

This is basically what a browser does with a cache, only we are busting the cache manually and storing the payload manually.

To ensure that we aren’t getting some nefarious payload from the server we will sign the payload and verify the signature using an embedded public key in the native payload.

In normal circumstances, where the server and client are the same version the default payload is used and everything is awesome.

When the server is upgraded the client sees there’s a newer payload available from the server, downloads it and uses that payload instead.

The same is true of a client that accesses an older server - just download the older package (although a backwards-compatible client would just work).

Wait, what about the native interop?! I hear you say.

Well, we can’t overwrite that native interop with the payload without opening ourselves up to other attack vectors. Instead we version the native interop and ensure that:

  1. Native interop maintains backwards compatibility
  2. Client maintains backwards compatibility with native interop

Perhaps this sounds like more work than maintaining a backwards compatible server? We expect the native interop to change far less often than our server stack, and in addition all our backwards compatibility code remains client side instead of being spread between client and server.

Wrap-up

We didn’t come to this conclusion lightly, we brainstormed the approaches and overhead of maintaining a backwards compatible server and unanimously agreed that we really didn’t want to be doing that if we could avoid it. It would slow the release of new features and mean we’d have to maintain both a backwards compatible server and backwards compatible client - much nicer not to have to worry about that!

This proposal is still being finalised and isn’t live yet. However, this approach essentially amounts to what a browser does, the only difference is we’re exposing some extra APIs for our “web app” to leverage.


Luke Terry

Written by Luke Terry.

Senior Engineer at MindLink. Enjoys technology, playing games and making things work, blogs at www.indescrible.co.uk.