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:
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 Ubuntu | Fedora 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:
| Platform | Where | Key |
|---|---|---|
| macOS | a 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 |
| Windows | HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Botseon\Desktop | AllowedTeamIds, 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 botseonlists 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:
| Button | When | What it does |
|---|---|---|
| Update | The newer build is not downloaded yet | Downloads and verifies it |
| Restart to update | The newer build is downloaded and verified | Installs it and restarts the app |
| Try again | The download or the check failed | Checks again |
| Download the latest version | Always | Opens 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.