Enterprise policy (managed device)
For managed fleets, the browser extension reads an OS-delivered policy from chrome.storage.managed. Because it comes from the operating system's managed-configuration channel, neither the user nor a web page can forge or override it, only the device administrator sets it. The extension declares its schema in the manifest (storage.managed_schema → managed_schema.json).
Policy authority is the device manager (your IT / MDM), not the open-secret server operator, and the two are deliberately different parties. The server is untrusted to control a user's device or secrets: if it could push policy, a malicious or compromised operator could flip backupPolicy to required and harvest every opted-in vault through the operator backup key, lower the password floor, and so on. So policy is never delivered over the wire from the server, only through the OS/MDM channels below.
A normal website cannot read chrome.storage.managed, that API is extension-only; web pages are sandboxed away from all chrome.* APIs. The web platform's navigator.managed (Managed Configuration API) only delivers admin config to a force-installed managed PWA window, and it does nothing to stop a user opening the same origin in an ordinary tab, where no policy applies. So it configures a PWA instance; it is not a barrier on the origin. A user who controls their browser can always reach the un-managed web UI.
The practical consequence: policy is enforced on the extension, the one client where the OS-managed channel genuinely binds. To "cover the web" you need device/network controls, not an app setting, a managed device that mandates the extension and blocks the web origin (e.g. Chrome URLBlocklist), plus IDP deprovisioning. And ultimately nothing beats a determined key-holder: with E2EE the client can always decrypt-then- serialize, so blockExport is a guardrail for cooperative users on managed clients, never a cryptographic guarantee.
One alternative was considered and deliberately left out: a compile-time build where the operator bakes policy into a custom web bundle that physically omits the export code. It is not implemented, and it is more involved than a quick flag. The export and import paths share modules, so a reliable removal needs build-time module isolation plus a rebuild gate, and a self-hosting operator could still rebuild the bundle without it. That is real maintenance cost for a weaker guarantee than the device and network controls above already provide.
The simpler answer holds: if you require policy enforcement, distribute the managed extension and do not host the web UI. A deployment that does not want an un-managed web surface should not serve one. A business that genuinely needs a locked-down web bundle can build one from source.
What you can set
Eight keys are available:
| Key | Type | Effect |
|---|---|---|
serverUrl | http(s) origin | Pins the backend URL. The extension uses it on first run and locks the Settings field, so a user can't be typo'd or social-engineered onto a hostile server. Bare origin only (no path/query/hash). |
backupPolicy | required | default-on | user-choice | Operator-backup opt-in posture. required forces backup on for every entry and locks the per-entry toggle; default-on pre-selects it for new entries but the user can still opt out; user-choice (the default) leaves it to the user. |
orgName | string | Human-readable org name shown in policy-explanation strings, e.g. "Required by {orgName}". |
blockExport | boolean | When true, the extension hides and refuses both the plaintext vault export and the encrypted device-backup download. A managed-device guardrail, not a cryptographic guarantee. |
lockOnScreenLock | boolean | When true, forces vault lock when the OS screen locks. The user cannot disable this; their preference is OR'd with the policy value. |
maxIdleLockMinutes | integer (1-10080) | Caps the idle-lock timeout in minutes. The effective timeout is min(user preference, policy cap). |
minUnlockPasswordLength | integer (1-1024) | Sets a floor for the unlock password length. Effective floor is max(8, value). Enforced on password set or change. |
clipboardClearSeconds | integer (1-3600) | Pins clipboard auto-clear on with at most this delay. Effective delay is min(user preference, policy value). |
Setting any of them flips the install into "managed" mode, which the UI uses to explain why a control is locked.
backupPolicy=required only seals each entry to the operator backup key, it does nothing unless that key is configured on the instance. If you mandate backup, configure the operator backup key too, or the policy has no recovery effect.
Delivering the policy
Every browser reads 3rd-party extension policy from the same logical place - under the extension's ID, but each platform has its own delivery channel. First, two things you need:
- Your extension ID. Find it at
chrome://extensions(toggle Developer mode). The examples below use<EXTENSION_ID>. - The policy values. The full key list + constraints live in
managed_schema.json. The examples useserverUrl+backupPolicy; add any other keys the same way.
The mechanism is identical across Chromium browsers; only the vendor location changes. Use the row for the browser you actually run (this is the single most common reason a policy file "does not load": a file dropped in Chrome's directory is invisible to Chromium or Brave).
| Browser | Linux directory | Windows registry root | macOS domain | Policy page |
|---|---|---|---|---|
| Google Chrome | /etc/opt/chrome/policies/managed/ | HKLM\SOFTWARE\Policies\Google\Chrome | com.google.Chrome | chrome://policy |
| Chromium | /etc/chromium/policies/managed/ | HKLM\SOFTWARE\Policies\Chromium | org.chromium.Chromium | chrome://policy |
| Brave | /etc/brave/policies/managed/ | HKLM\SOFTWARE\Policies\BraveSoftware\Brave-Browser | com.brave.Browser | brave://policy |
| Edge | /etc/opt/edge/policies/managed/ | HKLM\SOFTWARE\Policies\Microsoft\Edge | com.microsoft.Edge | edge://policy |
The examples below use the Google Chrome locations, so substitute the row for your browser. Some Linux distro packages read /etc/chromium-browser/policies/managed/ instead, so check which path your build uses.
Verify it loaded (and debug when it does not)
Open the policy page from the table (chrome://policy, brave://policy, and so on), click Reload policies, and turn on Show policies with no value if needed. Your keys appear under the extension's ID in the per-extension ("3rdparty") section. If nothing shows, it is almost always one of:
- Wrong directory for your browser variant. A file under
/etc/opt/chrome/...is read only by Google Chrome, not Chromium or Brave. Use the directory from the table above. - Extension ID mismatch. The
<EXTENSION_ID>in the policy must match the ID atchrome://extensions(orbrave://extensions) exactly. For an unpacked or dev install that ID is derived from the install path and differs per machine, so a hardcoded fleet ID will not match. For a stable ID, publish the extension (Web Store or a self-hosted update URL) or pin akeyin the manifest. - File not valid JSON, or not readable. The file must be valid JSON and readable by the browser. The browser only re-reads policy at launch, so fully quit and reopen it (closing the window is not enough).
Linux, managed JSON file (no tooling required)
Drop a JSON file in the browser's managed-policy directory. This is the whole mechanism; "management tools" just automate placing this file.
The directory depends on which browser you run, this trips most people up:
| Browser | Managed-policy directory (Linux) |
|---|---|
| Chromium | /etc/chromium/policies/managed/ |
| Google Chrome | /etc/opt/chrome/policies/managed/ |
| Brave | /etc/brave/policies/managed/ |
| Microsoft Edge | /etc/opt/edge/policies/managed/ |
| Vivaldi | /etc/chromium/policies/managed/ (reuses Chromium's path) |
Notes: older Ubuntu/Debian chromium-browser packages read /etc/chromium-browser/policies/managed/ instead; Opera's Linux managed-policy support is inconsistent, so prefer one of the above for a managed fleet. Snap and Flatpak builds are different again (see the callout below).
The directory does not exist by default, the packages do not create it, so make it first (this example uses Chromium, swap in your browser's path from the table):
sudo mkdir -p /etc/chromium/policies/managed
sudo install -m 0644 open-secret.json /etc/chromium/policies/managed/open-secret.json
The file itself (the name is arbitrary as long as it ends in .json):
{
"3rdparty": {
"extensions": {
"<EXTENSION_ID>": {
"serverUrl": "https://vault.example.com",
"backupPolicy": "required",
"orgName": "Acme Inc"
}
}
}
}
The user cannot edit these (root-owned); the browser re-reads policy at launch. Then confirm at chrome://policy / brave://policy (see "Verify it loaded" above).
A Snap Chromium (Ubuntu's default) does not read /etc/chromium/...; its policy directory is /var/snap/chromium/current/policies/managed/ (or /etc/chromium-browser/policies/managed/ on older builds). A Flatpak Brave or Chromium is confined and will not see /etc/... unless you grant it filesystem access or place the policy inside the app's per-user config under ~/.var/app/<app-id>/. If chrome://policy stays empty with the file in the documented place, check whether the browser is a Snap or Flatpak build, a native package (RPM/DEB) is the simplest target for fleet policy.
Linux, power-manage
Ship the same JSON as a managed file through power-manage: add the file to your fleet's managed-files set targeting the policy directory for your browser (/etc/chromium/policies/managed/open-secret.json for Chromium, /etc/brave/policies/managed/open-secret.json for Brave, the Chrome path for Chrome), owned root:root, mode 0644, and let power-manage create the directory, distribute, and reconcile it across hosts. Nothing open-secret-specific, it's an ordinary managed config file.
Windows, registry (no tooling required)
3rd-party extension policy lives under HKLM\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\<EXTENSION_ID>\policy. Strings are REG_SZ; booleans/integers are REG_DWORD.
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\<EXTENSION_ID>\policy]
"serverUrl"="https://vault.example.com"
"backupPolicy"="required"
"orgName"="Acme Inc"
Boolean/integer keys (added as they ship) use REG_DWORD, e.g. "blockExport"=dword:00000001.
Windows, Group Policy (GPO)
Two options:
- Registry via GPO Preferences, Computer Configuration → Preferences → Windows Settings → Registry: create the same
…\3rdparty\extensions\<id>\policyvalues as above. This is the most direct route for 3rd-party schemas. - Chrome ADMX, import Google's
chrome.admx/adml; the first-party Chrome policies appear under Administrative Templates → Google Chrome, but per-extension 3rd-party keys are still set through the registry path above (the ADMX doesn't model your extension's schema).
Windows, Microsoft Intune
The schema isn't in the Settings Catalog, so push the registry values. A Devices → Scripts → PowerShell platform script (run as system) is the reliable route:
$base = 'HKLM:\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\<EXTENSION_ID>\policy'
New-Item -Path $base -Force | Out-Null
New-ItemProperty -Path $base -Name serverUrl -Value 'https://vault.example.com' -PropertyType String -Force | Out-Null
New-ItemProperty -Path $base -Name backupPolicy -Value 'required' -PropertyType String -Force | Out-Null
# Boolean/integer keys use -PropertyType DWord, e.g.
# New-ItemProperty -Path $base -Name blockExport -Value 1 -PropertyType DWord -Force | Out-Null
(A Win32 app wrapping the same script works too, if you want detection + reporting.)
macOS, JAMF Pro
Configuration Profiles → Application & Custom Settings → Upload with Preference Domain com.google.Chrome and a plist carrying the 3rdparty → extensions → <EXTENSION_ID> dictionary:
<dict>
<key>3rdparty</key>
<dict>
<key>extensions</key>
<dict>
<key>EXTENSION_ID</key>
<dict>
<key>serverUrl</key><string>https://vault.example.com</string>
<key>backupPolicy</key><string>required</string>
</dict>
</dict>
</dict>
</dict>
macOS, Kandji
Add a Custom Profile library item containing a .mobileconfig with a com.google.Chrome Managed Preferences payload holding the same 3rdparty dictionary as the JAMF example. (Any MDM that ships a custom configuration profile for the com.google.Chrome domain works identically, Mosyle, Intune-for-Mac, etc.)
macOS, no MDM
Without a profile you can write the same managed preference locally (run as admin); Chrome reads it on next launch:
sudo defaults write com.google.Chrome '3rdparty' -dict-add extensions \
'{ "<EXTENSION_ID>" = { "serverUrl" = "https://vault.example.com"; "backupPolicy" = "required"; }; }'
Use the domain for your browser instead of com.google.Chrome: com.brave.Browser (Brave), org.chromium.Chromium (Chromium), com.microsoft.Edge (Edge). A profile-delivered value is stronger (a local admin can't clear it); use defaults only for a single machine or testing.
Relationship to auth mode
Managed policy and the deployment auth mode are independent: serverUrl pinning is useful in both standalone and IDP deployments (an IDP deployment still wants the extension pointed at the right backend before SSO). Pin serverUrl for any managed fleet so the client only ever talks to your server.