Why We Did This
Keeping browsers up to date is one of the most important things you can do for endpoint security. Browsers are the single most exposed piece of software on most machines. They render untrusted content all day long, they hold session tokens and saved credentials, and they are the first thing attackers target with zero day exploits. A browser that is two or three versions behind is a genuine risk, not a cosmetic one.
The challenge in a mixed fleet is that no two browsers handle updates the same way, and users do not reliably restart their browser when an update is waiting. It is very common to find a machine where the update has downloaded days ago but the user has simply never quit the browser, so the patched version is sitting on disk doing nothing.
We wanted three things:
- Every managed browser updates itself automatically in the background without relying on the user.
- Once an update is downloaded, the user is given a clear and consistent prompt to relaunch, with a generous but bounded grace period before it happens for them.
- The whole thing is delivered through our existing MDM tooling so it scales across the fleet and is easy to audit.
We run a split environment. Some Macs are managed by Jamf Pro and some by Microsoft Intune, mostly for historical reasons. So whatever we built had to work in both, and produce the same end result regardless of which MDM happened to own the device. This post walks through exactly how we did it, the gotchas we hit, and how you can verify and troubleshoot it yourself.
A Quick Primer on How Browsers Update on macOS
Before touching any configuration, it helps to understand that "browser updates" involves two completely separate concerns, controlled by two completely separate things.
The first is update delivery. This is the mechanism that downloads and installs a new version. Crucially, this is almost never handled by the browser itself. It is handled by a dedicated background updater that the vendor ships alongside the browser.
The second is the relaunch experience. This is what the user sees once an update has been downloaded and is waiting to be applied. This part is handled by the browser, through its own policy engine.
Getting these two mixed up is the single most common mistake, and it is the reason a lot of people think they have enabled something when they have not. You can have updates downloading perfectly while the user never gets told to restart, or you can have a relaunch prompt configured for a browser whose updater you never actually enabled.
Here is how the major browsers break down. Chrome and Brave both use Google's update engine, known as Keystone. Even though Brave is a separate company and a separate browser, it is built on Chromium and it registers with Keystone for updates just like Chrome does, so a single Keystone policy can govern both at once. Edge uses Microsoft AutoUpdate, usually shortened to MAU, the same updater that keeps the rest of Office current, so on most managed Macs it is already present and often already configured. Firefox is the odd one out. It does not use a separate updater daemon in the same way. Its update behaviour is controlled directly through its own enterprise policy engine, which has to be explicitly switched on before any policy is honoured at all.
The relaunch experience is more consistent. Chrome, Brave and Edge are all Chromium based, so they all understand the same two policy keys for controlling relaunch behaviour. Firefox does not support these keys and manages its own restart prompting internally.
Choosing the Values: Required Relaunch with a 3-Day Grace
Two policy values determine the user experience after an update is downloaded: how forceful the notification is, and how long the user gets to act on it. The pairing matters more than either value alone.
The notification key, RelaunchNotification, takes three values. 0 means no notification at all. 1 means recommended: the user sees a gentle prompt but is never forced to relaunch, ever. 2 means required: the user gets the same prompt initially but, after the grace period, the browser relaunches itself to apply the patch. The grace period itself, RelaunchNotificationPeriod, is just a number in milliseconds.
It is tempting to choose 1 for the kinder user experience, but in practice that defeats the point. A user who never quits their browser stays on the vulnerable version indefinitely, which is the exact failure mode this whole exercise exists to prevent. So we used 2, required, on every Chromium browser.
For the grace period we landed on three days, which is 259200000 milliseconds. That gives someone plenty of time to finish what they are doing, span a weekend if one falls in the middle, and choose their own moment to restart. It is gentle without being indefinite. Other common values are 3600000 for one hour, 14400000 for four hours, and 86400000 for a single day, depending on how patient or impatient your fleet's appetite is.
Keep RelaunchNotification at 2 and pick the period that suits your users. The required mode is what guarantees the patched browser runs. Loosening to 1 reintroduces the original problem dressed up as a gift.
The Policy Keys We Used
Everything below is delivered as a property list, or plist, which is just an XML file of key and value pairs. Each block targets a specific preference domain, the reverse domain identifier that tells macOS which application the settings belong to.
Keystone, for Chrome and Brave update delivery
Preference domain: com.google.Keystone
<dict>
<key>updatePolicies</key>
<dict>
<key>global</key>
<dict>
<key>UpdateDefault</key>
<integer>1</integer>
</dict>
<key>com.google.Chrome</key>
<dict>
<key>Update</key>
<integer>1</integer>
</dict>
</dict>
</dict>
UpdateDefault at 1 is the global default for everything Keystone manages, enabling automatic updates. The Chrome specific Update key at 1 makes the intent explicit. Because Brave also registers with Keystone, the global default covers it too.
0 disabled · 1 automatic updates enabled · 2 manual updates only · 3 automatic disabled but manual through the updater still allowed. We used 1 everywhere.
Chrome browser relaunch behaviour
Preference domain: com.google.Chrome
<dict>
<key>RelaunchNotification</key>
<integer>2</integer>
<key>RelaunchNotificationPeriod</key>
<integer>259200000</integer>
</dict>
Brave, update and relaunch combined
Preference domain: com.brave.Browser. Because Brave uses Keystone for updates and understands the Chromium relaunch keys, we put both concerns in one block.
<dict>
<key>Update</key>
<integer>1</integer>
<key>RelaunchNotification</key>
<integer>2</integer>
<key>RelaunchNotificationPeriod</key>
<integer>259200000</integer>
</dict>
Edge browser relaunch behaviour
Preference domain: com.microsoft.Edge
<dict>
<key>RelaunchNotification</key>
<integer>2</integer>
<key>RelaunchNotificationPeriod</key>
<integer>259200000</integer>
</dict>
This block only covers the relaunch experience. The actual update delivery for Edge is handled by Microsoft AutoUpdate, which is very often already configured by an existing Office or Microsoft 365 deployment. Manage it in a separate profile rather than duplicating it here. More on that in the conflicts section.
Firefox, enterprise policies and update delivery
Preference domain: org.mozilla.firefox
<dict>
<key>EnterprisePoliciesEnabled</key>
<true/>
<key>AppAutoUpdate</key>
<true/>
</dict>
Firefox will ignore every other policy you send it on macOS unless EnterprisePoliciesEnabled is set to true first. It is effectively the master switch that tells Firefox to start honouring managed configuration at all.
Note that these are boolean true and false values, written as <true/> and <false/>, not integers. This trips people up because the Chromium browsers all use integers for their equivalent settings. As a belt and braces measure you can also add DisableAppUpdate set to false, which explicitly undoes any earlier policy that might have disabled updates. Firefox does not support the relaunch keys. If you add them they are simply ignored, because Firefox handles its own restart prompting on its own schedule.
A Note on Other Browsers
We focused on the four browsers in use across our fleet, plus the two underlying updaters. If your environment includes others, the picture is mixed. Most other Chromium based browsers such as Vivaldi support the same Chromium policy keys and can be managed in a very similar way using their own preference domain. Arc has only limited plist based settings and no full policy catalogue, so it can be lightly configured but not managed to the same standard. Opera is worth calling out specifically because, despite being Chromium based, it has chosen not to support Google's policy management structure at all on either macOS or Windows, so there is no supported way to manage its updates through a configuration profile. Safari is updated through macOS system updates rather than a browser specific updater, so it sits outside this entirely and is governed by your operating system update policy instead.
The general rule of thumb is that Chromium based browsers usually share policy keys, while anything with its own engine or its own ideas about management needs to be checked individually against the vendor's documentation before you assume anything works.
Putting It All in One Profile
We chose to deliver all of the browser policies in a single profile with multiple payloads, rather than scattering them across several. It keeps everything related to browser behaviour in one auditable place, it is scoped once, and anyone looking at it can see the whole browser story at a glance. The one thing we deliberately kept separate was the Microsoft AutoUpdate configuration, since it is very commonly already in its own profile from an Office or Microsoft 365 deployment.
| # | Preference domain | Purpose |
|---|---|---|
| 1 | com.google.Keystone | Chrome and Brave update delivery |
| 2 | com.google.Chrome | Chrome relaunch behaviour |
| 3 | com.brave.Browser | Brave update and relaunch behaviour |
| 4 | com.microsoft.Edge | Edge relaunch behaviour |
| 5 | org.mozilla.firefox | Firefox enterprise policies and update delivery |
Deploying It in Jamf Pro
In Jamf Pro, browser settings like these are delivered through a configuration profile using the Application and Custom Settings payload, with five payload entries inside one profile.
- In Jamf Pro, go to Computers, then Configuration Profiles, and choose New.
- Under General, give the profile a clear descriptive name such as "Browser Update and Relaunch Policy". Use the description field to note that Microsoft AutoUpdate settings live in a separate profile, and to cross reference related profiles. Future you will be grateful for this.
- Set the level so the profile applies at the computer level, since these are machine wide settings.
- In the left hand payload list, find Application and Custom Settings and select it.
- Click Add, then choose the Upload option, sometimes labelled external applications or custom settings depending on your Jamf version.
- In the Preference Domain field, enter the first domain,
com.google.Keystone. - You can either upload a complete plist file with the XML wrapper, or paste the inner dictionary directly into the property list field. Uploading a file is the cleaner option because the file itself is self documenting and easy to keep in version control. Pasting the inner dictionary works too, just start at
<dict>and end at</dict>with no XML declaration or plist wrapper. - Repeat the previous three steps for each remaining domain, adding a new payload each time, until you have five.
- Go to the Scope tab. For the first test, scope to a single machine, ideally your own, rather than a group.
- Save the profile.
Once saved, the profile is delivered the next time the target Mac checks in, which can take up to around eight hours, or you can force it from the device.
# Force a check in on a Jamf managed Mac
sudo jamf recon
sudo jamf mdm
Be consistent within each payload. If you upload files, every payload uses a complete plist with the XML wrapper. If you paste, every payload uses just the inner dictionary. Mixing the two within a single payload field will not work.
Deploying It in Intune
The Intune path achieves the same result, but the terminology and file handling differ. There are two ways to deliver this in Intune, and one is clearly better.
The recommended way: one bundled .mobileconfig via a Custom profile
Intune accepts a complete .mobileconfig file containing many payloads at once, which collapses what would otherwise be five separate profiles into a single one. This matches the shape of the Jamf profile and is easier to manage and audit.
- In the Intune admin centre, go to Devices, then Configuration, and choose Create, then New policy.
- Set the platform to macOS. For profile type, choose Templates, then Custom. This is the path that accepts a
.mobileconfigfile. - Give the profile a name such as "macOS, Browser Update and Relaunch Policy".
- Set Deployment channel to Device channel, since these are system wide settings.
- Upload your bundled
.mobileconfigfile. Intune will display the XML inline so you can confirm it parsed. - Assign the profile to a group containing only your test machine for the first run.
- Save.
The bundled file contains one outer Configuration payload and five inner payloads, one per preference domain. Each inner payload needs a unique PayloadUUID, a unique PayloadIdentifier, and the appropriate keys for its preference domain. The PayloadType for each inner payload is set to the preference domain itself, for example com.google.Chrome. Generating the UUIDs by hand is error prone, so use a tool or a script rather than typing them out.
The older way: one Preference file profile per domain
Intune also has a Preference file template that handles a single preference domain at a time as a plain .plist. The equivalent of one bundled .mobileconfig is a set of five Preference file profiles, one each for Keystone, Chrome, Brave, Edge and Firefox. This still works, but you end up with five profiles to scope, audit, and keep aligned, and they show up as separate rows everywhere.
You cannot change an existing profile's template type in place. A profile created as Preference file is stuck as Preference file. To migrate from the Preference file approach to a Custom .mobileconfig, create the new Custom profile, verify it lands correctly on a test machine, and only then delete the old Preference file profiles. Keep both live just long enough to confirm continuity, otherwise you risk a window where neither is applying.
To nudge an Intune managed Mac to pick up the policy, use the Company Portal app and trigger a sync, or run:
sudo profiles renew -type enrollment
The end result on the device is identical to the Jamf delivery. The settings land in exactly the same place, /Library/Managed Preferences/, regardless of which MDM sent them.
Finding Conflicts Before They Happen
This is the part that saves you from a real problem, and it is the step most people skip. Before pushing any new policy, check whether something is already managing that preference domain. If two profiles both try to manage the same domain you get unpredictable behaviour, and you may silently overwrite settings another team relies on.
The quickest check is to read the managed preferences directly on a representative machine. If a domain reports that it does not exist, you are starting from a clean slate. If it returns values, something is already managing it and you need to find out what before you add your own.
defaults read /Library/Managed\ Preferences/com.google.Keystone.plist
defaults read /Library/Managed\ Preferences/com.google.Chrome.plist
defaults read /Library/Managed\ Preferences/com.brave.Browser.plist
defaults read /Library/Managed\ Preferences/com.microsoft.Edge.plist
defaults read /Library/Managed\ Preferences/com.microsoft.autoupdate2.plist
defaults read /Library/Managed\ Preferences/org.mozilla.firefox.plist
In a lot of environments you will find the Microsoft AutoUpdate domain, com.microsoft.autoupdate2, already returns values, often delivered as part of an Office or Microsoft 365 rollout that nobody remembers any more. That is exactly the kind of thing this check exists to surface.
This is exactly the situation you want to catch before you act, not after. If you blindly create your own Microsoft AutoUpdate payload here, you end up with two profiles fighting over the same domain. The better move is to leave the existing one untouched, confirm it does not contain an Edge browser payload that would clash with your relaunch settings, and simply add the Edge relaunch behaviour to your own profile. Two profiles, two different domains, no conflict.
To find which profile owns an existing setting, dump all installed profiles and search through them, and check the file's age and ownership for clues about where it came from.
sudo profiles show -all > /tmp/profiles_dump.txt
cat /tmp/profiles_dump.txt | grep -i "autoupdate"
ls -la "/Library/Managed Preferences/com.microsoft.autoupdate2.plist"
The modification date tells you how long the policy has been there, and root ownership confirms it is MDM managed rather than something set locally. The fastest way to identify the exact source profile, though, is to look at the device record in your MDM console and read through the list of profiles scoped to it. It is also worth confirming which browsers are actually installed:
ls /Applications | grep -i -E "chrome|brave|firefox|edge"
It is perfectly reasonable to push policies for browsers that are not currently installed. In a developer heavy organisation people install Brave and Firefox of their own accord all the time, so having the policy already waiting means it applies the moment the browser appears. A policy for an absent application does no harm. It just sits dormant.
Verifying It Worked (and the Cache Trap)
Once the profile has landed, verify at two levels. First confirm the raw managed preferences are present on disk, then confirm the browser itself is actually honouring them.
sudo profiles show -all
defaults read /Library/Managed\ Preferences/com.google.Keystone.plist
defaults read /Library/Managed\ Preferences/com.google.Chrome.plist
defaults read /Library/Managed\ Preferences/com.brave.Browser.plist
defaults read /Library/Managed\ Preferences/com.microsoft.Edge.plist
defaults read /Library/Managed\ Preferences/org.mozilla.firefox.plist
You should see your values reflected back. For Keystone, the update policies with values of 1. For each Chromium browser, the relaunch notification set to 2 and the period set to 259200000. For Firefox, enterprise policies enabled and auto update enabled. Then confirm inside each browser, which is the real proof that the application is reading the policy.
| Browser | Policy page |
|---|---|
| Google Chrome | chrome://policy |
| Microsoft Edge | edge://policy |
| Brave | brave://policy |
| Firefox | about:policies |
On the policy page you should see your keys listed with the correct values and a status of OK.
The values returned by defaults read reflect what is on disk at that moment, not necessarily what the MDM currently thinks the policy should be. If you change a value in your MDM console and immediately re-read on the device, you can see the old cached value and panic. We hit exactly this with Brave during testing: the MDM payload had been updated to the correct values, but defaults read showed the previous values until the device next synced. If verification looks wrong, force a check in (sudo jamf mdm on Jamf, sudo profiles renew -type enrollment on Intune), wait a minute, and re-read. The discrepancy almost always resolves itself.
Troubleshooting and Gotchas
Keystone disables the manual update button in Chrome
After deployment, Chrome's about page reports that automatic updates are enabled but that manual updates are disabled by the administrator. This is expected. Once Keystone is managing updates at the system level, Chrome hands over control and intentionally hides the manual check button, because Keystone is now the authority. The two are designed to be mutually exclusive. Since updates are happening automatically and the relaunch prompt is working, this is generally fine to leave alone. Users can still see their current version on the about page, they just cannot force a check from there.
A pending update can mask the real state
If Chrome is prompting for a relaunch and showing odd messaging, relaunch first to clear the pending update before drawing any conclusions. Always clear a pending relaunch before troubleshooting, otherwise you may be looking at a transient state rather than the steady state.
Firefox ignores everything until enterprise policies are enabled
If your Firefox policies appear to do nothing, the first thing to check is that EnterprisePoliciesEnabled is set to true. Without it, Firefox quietly ignores the rest. Confirm what Firefox has accepted at about:policies.
Boolean versus integer
Firefox uses boolean true and false values where the Chromium browsers use integers. Sending the wrong data type will not work. Match the data type to the browser.
The managed file and the user file are different
If you look for a preference file you may find two with the same name in different locations. The one under /Library/Managed Preferences/ is the MDM delivered policy and is owned by root. The one under the user's own Library Preferences folder is written by the application itself as it runs and reflects local state. The managed one is the policy, the user one is just the app's own bookkeeping.
Stale managed preferences after editing a payload
This is the cache trap mentioned above, written down so it has its own header to find. If you edit a value in your MDM and the device still shows the old value, force a check in and re-read. Both Jamf and Intune deliver the change on the next sync, but until that happens the cached file on disk reflects the previous state.
The Intune profile type trap
Intune does not let you change a profile's template type in place. A profile created as Preference file cannot be converted to Custom. To migrate, create the new profile, verify it on a test machine, then delete the old one. Do not delete first or you risk a gap where no policy is applying.
Profiles can take time to appear
If a freshly scoped profile is not showing up, give it time to check in or force the check in manually. A profile that is scoped correctly but has not been collected yet looks identical to one that failed, so rule out timing before assuming something is broken.
The Command Reference (Reusable Patterns)
Below, each command is shown first as a template with the variable part highlighted, then with specific examples. The templates apply far beyond browsers; the same patterns work for any managed app on macOS, whether you are looking at FileVault, PPPC, an antivirus tool, or anything else delivered by your MDM. Learn the template once and the examples write themselves.
Reading any managed preference
defaults read /Library/Managed\ Preferences/<preference-domain>.plist
Replace <preference-domain> with the reverse-DNS identifier of any application or system service. Same command, any domain.
# Browser policies
defaults read /Library/Managed\ Preferences/com.google.Chrome.plist
defaults read /Library/Managed\ Preferences/org.mozilla.firefox.plist
# Apple/system policies you might also encounter
defaults read /Library/Managed\ Preferences/com.apple.applicationaccess.plist
defaults read /Library/Managed\ Preferences/com.apple.SoftwareUpdate.plist
defaults read /Library/Managed\ Preferences/com.apple.security.firewall.plist
# A specific key from within a domain
defaults read /Library/Managed\ Preferences/<domain>.plist <key-name>
Inspecting profiles delivered by MDM
# Full content of every installed profile
sudo profiles show -all
# Summary list only
sudo profiles list
# Enrolment status (which MDM, if any)
sudo profiles status -type enrollment
# Search the full output for any term
sudo profiles show -all | grep -i "<search-term>"
sudo profiles show -all | grep -i "microsoft"
sudo profiles show -all | grep -i "filevault"
sudo profiles show -all | grep -i "autoupdate"
Forcing a check in
Use these when you have just made a change in the MDM console and do not want to wait for the next automatic sync.
sudo jamf recon
sudo jamf mdm
sudo jamf policy # also triggers policy runs, not just MDM
sudo profiles renew -type enrollment
Investigating the source of an existing setting
When the managed preferences check reveals a domain you did not expect, work outward from the file to find the profile that owns it.
# File metadata: when was it written, by whom
ls -la "/Library/Managed Preferences/<preference-domain>.plist"
stat "/Library/Managed Preferences/<preference-domain>.plist"
# Compare against the user-level version (often written by the app itself)
ls -la ~/Library/Preferences/<preference-domain>.plist
# Dump and grep all profiles for the domain or any related term
sudo profiles show -all > /tmp/profiles_dump.txt
grep -i "<search-term>" /tmp/profiles_dump.txt
Confirming what applications are installed
Useful before deploying app-specific policies, but the pattern is just list /Applications and filter, which generalises to anything you might be looking for.
ls /Applications | grep -i -E "<app1>|<app2>|<app3>"
ls /Applications | grep -i -E "chrome|brave|firefox|edge"
In-app verification pages
Every modern browser exposes its active policies internally. After delivery, the policy page is the authoritative proof the app is honouring the configuration.
| Browser | URL |
|---|---|
| Google Chrome | chrome://policy |
| Microsoft Edge | edge://policy |
| Brave | brave://policy |
| Firefox | about:policies |
Validating a .mobileconfig before uploading to Intune
A malformed mobileconfig fails silently in Intune. The simplest sanity check is whether the file parses as a valid plist at all, which catches the most common issues such as mismatched tags or missing closing elements.
plutil -lint <path-to-file>.mobileconfig
plutil -lint ~/Downloads/browser-update-policy.mobileconfig
A response of "OK" means the file is structurally valid. Any other output is the parser telling you exactly where it broke.
Quick-Action Checklist
For when you just need to do it. Each step links back to the section above with the detail.
- Confirm your values. RelaunchNotification = 2, period = 259200000 (3 days). All Chromium browsers get both keys; Firefox just gets the auto-update key.
- Check for conflicts on a test machine. Run the five
defaults readcommands. Pay particular attention tocom.microsoft.autoupdate2, since that domain is very often already managed by an existing Office or Microsoft 365 profile. - Confirm which browsers are installed:
ls /Applications | grep -i -E "chrome|brave|firefox|edge". Push policies for the others anyway — they sit dormant until the browser appears. - Build the profile.
- Scope to one test machine first. Never to a group on the first run.
- Force a check-in after the change so you do not wait for the cycle:
# Jamf sudo jamf recon sudo jamf mdm # Intune sudo profiles renew -type enrollment - Verify at file level. Re-run the five
defaults readcommands. Every domain should return your values. - Verify at application level. Open each installed browser and check the policy page (
chrome://policy,edge://policy,brave://policy,about:policies). Keys present, status OK. - If values look wrong, remember the cache trap:
defaults readshows what is on disk, which can lag the MDM by a sync. Force another check-in and re-read before assuming the policy is broken. - Roll out to the wider fleet only after the previous step succeeded.
- Send the user comms. Mention auto-update, the 3-day relaunch window, and that tabs are restored. Do not send it before both Jamf and Intune sides match, or some users will get the wrong experience.
Closing Thoughts
The actual configuration here is not complicated once you understand the split between update delivery and relaunch behaviour, and once you know which mechanism each browser uses. The real value is in the discipline around it. Check what already exists before you push anything. Keep related policies cross referenced so the next person can follow the trail. Test on a single machine before you touch the fleet. Verify at both the file level and the browser level, and remember the on-disk cache lags the MDM by up to a sync interval. Do not be surprised when a managed setting changes the user experience in a way you did not expect, such as the manual update button disappearing, because that is usually the policy working as designed rather than a fault.
Done well, the payoff is a fleet where every browser keeps itself current automatically, users get a fair and consistent nudge to restart, and you have a clear and auditable record of exactly what is being enforced and why.
← All guides