Deploying the desktop app across an organisation

Administrator·14 minutes to read

How to put one pinned version of the desktop app on every managed Mac, Windows and Linux machine with the device-management tools you already run, and what happens when a pinned build falls below the oldest version the service still supports. Any member can install the app from the download page without asking anyone; everything here is optional and is for an organisation that wants to control which version its machines run, or which organisations its machines may sign in to.

Open the network path

Do this first, before you push the app to any machine. If your organisation runs a secure web gateway, a proxy or a firewall, allow the hosts the app reaches, exempt them from TLS inspection and from response buffering on every gateway location and profile, and run the checks from one member's device, as Opening the network path lists. An app installed behind a gateway that inspects or buffers these hosts signs in and then never shows its computer.

Pinned addresses and feeds

Every release has its own installer address on this site, one per platform and format, in the form /download/<version>/<platform>/<arch>/<format>. The address keeps serving that release's installer after newer releases ship, so a deployment you point at it installs exactly the version you tested. The download page lists every address for the current release under For administrators: pinned addresses and feeds.

Each platform also has a JSON feed answering the current version and its installer addresses:

  • macOS: /download/feed/macos
  • Windows: /download/feed/windows
  • Linux: /download/feed/linux

The download page links each feed once a release is published. A feed answers 503 while no signed release is available, so treat any answer other than 200 as "no release to install".

A feed answers version, publishedAt and an installers list; each installer carries arch, format, url (the pinned address), directUrl (the file itself) and sha256, so a script can check the bytes it downloaded. To read the current version from a shell, set SITE to the address of the site this page is served from:

curl -fsSL "$SITE/download/feed/linux" | jq -r .version

macOS: Jamf Pro and Kandji

Use the universal zip for a fleet: it holds the signed, notarised app and runs on both Apple silicon and Intel. The two disk images are for members installing by hand.

  • Jamf Pro. Upload the installer to a distribution point as a package and install it from a policy scoped to the machines you choose, as Jamf's package management guide describes. If you prefer not to upload files, a policy can run the script below instead.
  • Kandji. Add a Custom App to the Library, choose the ZIP type, attach the zip, and assign it to a Blueprint, following Kandji's custom apps guide.

The same script works from any tool that runs a shell script as root. It installs the pinned version and nothing newer:

VERSION=1.4.0
curl -fsSL -o /tmp/botseon.zip "$SITE/download/$VERSION/macos/universal/zip"
ditto -x -k /tmp/botseon.zip /Applications
rm /tmp/botseon.zip

Windows: Microsoft Intune and the silent install

The Windows installer installs per user, without administrator rights, and accepts the standard silent switch. Run it with /S for no interface at all and /currentuser to keep it a per-user install whatever an earlier install chose. Saved under the name the release gives it, the line is:

"Botseon Setup 1.4.0.exe" /S /currentuser

For Microsoft Intune, wrap the installer with the Microsoft Win32 Content Prep Tool into an .intunewin file and add it as a Windows app (Win32), as Microsoft's Win32 app guide describes. Set the install command to the line above and the install behaviour to User. For detection, use the uninstall entry the installer writes under HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall, whose DisplayVersion is the release's version. Download the installer for the package from its pinned address, for example /download/1.4.0/windows/x64/nsis, so the package you test is the package you ship.

Linux: the signed apt and dnf repositories

Every Linux release is published to a signed apt repository and a signed dnf (yum) repository. Add the repository to a machine once, then install the app with the system's package manager. Each new release then arrives as an ordinary package update, with apt upgrade or dnf upgrade and the rest of the machine's packages. The package is called botseon in both.

The repository is signed with Botseon's own key, published beside it at https://updates.botseon.com/linux/botseon-archive-keyring.asc (the key file). apt checks the repository against that key through the source's Signed-By line, and dnf through gpgcheck=1 (every package is signed) and repo_gpgcheck=1 (the repository's metadata is signed). No key is added to the system's general trust.

The key is Botseon Package Signing <packages@botseon.com>, RSA 4096, fingerprint D9F0 EEC2 0EBC AA68 CEC1 693D EE63 11C3 3291 6186, expiring 2029-10-19. Check a downloaded copy against that fingerprint before you roll it out:

gpg --with-fingerprint --show-keys /etc/apt/keyrings/botseon.asc

Debian and Ubuntu

Add the repository:

<!-- linux-repo-drill: apt-add -->
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSLo /etc/apt/keyrings/botseon.asc https://updates.botseon.com/linux/botseon-archive-keyring.asc
sudo tee /etc/apt/sources.list.d/botseon.sources > /dev/null <<'SOURCE'
Types: deb
URIs: https://updates.botseon.com/linux/apt
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/botseon.asc
SOURCE

Install the app:

<!-- linux-repo-drill: apt-install -->
sudo apt update
sudo apt install botseon

New releases come with the machine's ordinary upgrades:

<!-- linux-repo-drill: apt-upgrade -->
sudo apt update
sudo apt upgrade

Fedora and RHEL

Add the repository:

<!-- linux-repo-drill: dnf-add -->
sudo curl -fsSLo /etc/pki/rpm-gpg/RPM-GPG-KEY-botseon https://updates.botseon.com/linux/botseon-archive-keyring.asc
sudo tee /etc/yum.repos.d/botseon.repo > /dev/null <<'REPO'
[botseon]
name=Botseon
baseurl=https://updates.botseon.com/linux/rpm/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-botseon
REPO

Install the app:

<!-- linux-repo-drill: dnf-install -->
sudo dnf install botseon

New releases come with the machine's ordinary upgrades. dnf rereads a repository's metadata on its own schedule (metadata_expire, 48 hours by default), so a plain dnf upgrade picks a release up within two days; --refresh takes it now:

<!-- linux-repo-drill: dnf-upgrade -->
sudo dnf upgrade --refresh

Any configuration-management tool that can write these two files, or that has its own setting for an apt source or a yum repository, adds the repository the same way. They are the same files the package writes for itself when it is installed on a machine that does not have them yet, and the package never rewrites a file that is already there, so a machine set up either way ends up with the same configuration:

Debian and UbuntuFedora and RHEL
Source/etc/apt/sources.list.d/botseon.sources/etc/yum.repos.d/botseon.repo
Key/etc/apt/keyrings/botseon.asc/etc/pki/rpm-gpg/RPM-GPG-KEY-botseon

Holding a version

To keep a fleet on the version you tested, install that version from the repository and hold it, so the system's own upgrades leave it alone until you release it.

On Debian and Ubuntu, apt-mark hold keeps a package from being upgraded until it is released with apt-mark unhold (see the apt-mark manual):

sudo apt install botseon=1.4.0
sudo apt-mark hold botseon

On Fedora and RHEL, the versionlock plugin does the same (see the DNF versionlock documentation):

sudo dnf install botseon-1.4.0
sudo dnf versionlock add botseon

Turning the repository off

To control upgrades you can hold the package as shown above, or turn the repository off. A disabled repository stays disabled through every upgrade of the package.

On Debian and Ubuntu, add Enabled: no to the source:

echo 'Enabled: no' | sudo tee -a /etc/apt/sources.list.d/botseon.sources

On Fedora and RHEL, set enabled=0:

sudo sed -i 's/^enabled=1/enabled=0/' /etc/yum.repos.d/botseon.repo

Removing the package keeps the repository until it is purged: apt purge botseon takes the source and its key away, and so does dnf remove botseon. apt remove leaves both, as it leaves any configuration, so reinstalling carries on from the same repository.

A machine that cannot reach the repository can install a release's package file from its pinned address instead: /download/<version>/linux/<arch>/deb or /download/<version>/linux/<arch>/rpm. Installing that file adds the repository too, unless /etc/botseon/no-package-repo exists first:

sudo install -d -m 0755 /etc/botseon
sudo touch /etc/botseon/no-package-repo

The same packages and the same repository serve both editions. A home-edition install updates from it in the same way, and is held or turned off in the same way.

Allow only your organisation's teams on company computers

On a company computer you can limit the desktop app to your own organisation's teams. Set the Allowed Team IDs policy to the list of team IDs that may sign in. On a machine with the policy:

  • only a member of a listed team can sign in. Any other organisation's account is refused, and so is a personal account;
  • an account that was signed in before the policy arrived, and is on no listed team, is signed out the next time the app starts. Its session is ended and the machine is removed from the account, so it no longer appears under the member's machines and can no longer run commands for them;
  • while the app runs, the window is held to the list too. Each page it opens on the service is checked, and so is the list itself once a minute. If the window ends up in an organisation that is not listed, or in a personal account, the app signs it out, removes the machine's credential and stored data, and shows a screen that names the device policy;
  • the organisation switcher in the app shows only the listed teams, and refuses a switch to any other organisation;
  • Create a team is refused in the app, because a new team is on no list yet. Members can still create one in their browser, and you add its ID to the policy if it should be used on the device;
  • a policy that lists no valid team ID allows nobody to sign in.

The policy only restricts. It has no effect on members' own computers, which carry no policy, or on the browser. It is a hosted-edition control for organisations: the home edition has no teams to list, and a home install with no policy works exactly as before.

A team ID is the organisation's id. An owner or admin finds it under Settings → Security as Organisation ID, and it can also be read with an organisation API key from GET /api/v1/organisation; see the API reference. List several teams separated by commas, or as separate entries where the platform has a list type.

The policy lives in the operating system's own machine-wide policy store, which only an administrator can write:

PlatformWhereKey
macOSa configuration profile for the preference domain com.botseon.app (it is written to /Library/Managed Preferences/com.botseon.app.plist)AllowedTeamIds, an array of strings
WindowsHKEY_LOCAL_MACHINE\SOFTWARE\Policies\Botseon\DesktopAllowedTeamIds, a REG_MULTI_SZ (or a comma-separated REG_SZ)
Linux/etc/botseon/policy.json, owned by root"AllowedTeamIds": [ ... ]

For Jamf Pro or Kandji, add a custom settings payload for the preference domain com.botseon.app:

<key>AllowedTeamIds</key>
<array>
  <string>11111111-1111-4111-8111-111111111111</string>
</array>

For Microsoft Intune or Group Policy, set the registry value from an elevated prompt or a remediation script:

reg add "HKLM\SOFTWARE\Policies\Botseon\Desktop" /v AllowedTeamIds /t REG_MULTI_SZ /d "11111111-1111-4111-8111-111111111111" /f

On Linux, write the file with your configuration-management tool:

sudo install -d -m 0755 /etc/botseon
echo '{ "AllowedTeamIds": ["11111111-1111-4111-8111-111111111111"] }' | sudo tee /etc/botseon/policy.json

The app reads the policy at every start, at every sign-in and once a minute while it runs, so a change takes effect within a minute. A member who is refused sees the sign-in page in their browser with the sentence This computer is managed by your organisation and only allows sign-in to its own teams. Each refusal and sign-out is in the organisation's audit log as enrolment.refused.team_not_allowed or installation.signed_out.team_not_allowed. If the app starts while it cannot reach the service, the start-up check waits for the next launch, and a new sign-in is still checked.

AppArmor on Ubuntu 24.04 and later

Ubuntu 24.04 refuses unprivileged user namespaces to any program AppArmor has no profile for, and the desktop app's sandbox needs one, so the app starts there only with a profile. The .deb and the .rpm carry it and install it themselves: after install, the package copies it to /etc/apparmor.d/botseon and loads it, and removing the package unloads and deletes it. The profile grants the user namespace to /opt/Botseon/botseon and confines nothing else.

  • On a host whose AppArmor is older than 4.0 (Ubuntu 22.04 and earlier) the profile is skipped, because those releases do not restrict user namespaces and the app starts without it.
  • On a host without AppArmor (Fedora, RHEL) nothing is installed.
  • When an image is built in a chroot, the profile file is written but not loaded; it loads at the next boot.
  • Add site rules in /etc/apparmor.d/local/botseon, which the profile includes; never edit /etc/apparmor.d/botseon, because every upgrade replaces it.
  • To check a machine: sudo aa-status | grep botseon lists the profile once it is loaded.

Every release's x64 .deb is installed on a clean Ubuntu 24.04 machine before it is published: the release build checks that the kernel loads the profile, that the app opens its first window under it with its sandbox on, and that it does not start once the profile is unloaded. A .deb that fails is not published. The publisher then reads every .deb and .rpm of the release once more and refuses to publish one that does not install the profile and load and unload it. After publishing, node scripts/release-apparmor-status.mjs downloads the .deb and .rpm files the public feed actually serves and confirms each carries the profile; it runs again every night.

The AppImage installs nothing, so it cannot bring the profile; deploy the .deb to Ubuntu machines.

Holding or turning off the app's own updates

Organisation → Policies → Desktop updates sets what your team's desktop apps do about the app's own signed updates. Both settings only restrict; neither can make an update mandatory.

  • Check for desktop app updates is on by default. Turned off, your team's desktop apps stop checking for updates and stay on the version they have until a member updates them.
  • Hold desktop apps at version is empty by default. With a version such as 1.4.0, the apps update up to that version and no further. Clear it to allow every update again.

Each desktop app reads the policy from the service before every update check, so a change reaches the app at its next check. An app signed in to a personal account has no organisation policy and keeps the default. On a single machine, BOTSEON_UPDATE_CHECK=0 or BOTSEON_LOCAL_ONLY=1 in the app's environment turns checking off whatever the policy says; the policy can never turn it back on. Every change is in the audit log as org.desktop_update_policy_changed.

When a pinned version stops being supported

Each release names the oldest version the service still supports, inside its signed update manifest, so the limit is set remotely, by the release, and not on the machine. A desktop app that checks for updates shows an Update required screen covering the whole app until it is updated when it is below that version, or when it is more than 14 days older than a newer published release (on the hosted service; BOTSEON_UPDATE_MAX_AGE_DAYS sets another age, 0 none).

The screen offers what the member needs to get past it, and nothing else:

ButtonWhenWhat it does
UpdateThe newer build is not downloaded yetDownloads and verifies it
Restart to updateThe newer build is downloaded and verifiedInstalls it and restarts the app
Try againThe download or the check failedChecks again
Download the latest versionAlwaysOpens the download page in the browser

On Linux installed from a .deb or .rpm, Update opens the system's package installer, and the screen also shows the install command with Copy command (sudo apt install '<file>.deb' or sudo dnf install '<file>.rpm', naming the downloaded file) for a terminal. Settings → Updates shows the same command beside Open Installer.

The app checks for updates by default, on a home-edition desktop as well as a hosted one. A version held with the organisation policy (above) does not keep a withdrawn build running: below the supported version the screen shows all the same. A fleet that must never be interrupted turns checks off, with the policy or with BOTSEON_UPDATE_CHECK=0 (or BOTSEON_LOCAL_ONLY=1) on the machine, and is then responsible for moving to a supported release itself. To stay ahead of the screen, read the platform feed above and move your pinned version forward before the old one is retired.

Last verified against build 5b75936.