Chrome blanks an off-store ExtensionInstallForcelist entry on an unmanaged machine exactly as Edge does - the qualifying states are AD domain join, Entra join, or Chrome Enterprise Core enrolment, and this box is none of them. The guard now covers every browser and refuses up front, with the three ways forward, instead of skipping Edge alone and letting Chrome fail the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
6.5 KiB
Markdown
145 lines
6.5 KiB
Markdown
# The extension update server
|
|
|
|
Chrome has no update channel for an unpacked extension: it reads the folder off
|
|
disk once and never looks again. This is the supported alternative — the
|
|
extension is signed into a CRX, an update manifest is published next to it, and
|
|
Chrome installs and updates it from there like any other extension.
|
|
|
|
```
|
|
droplet 159.223.129.19 Windows box
|
|
key.pem ──signs──> autofirmer.crx ──HTTPS──> Chrome (policy install)
|
|
updates.xml <──polls───
|
|
```
|
|
|
|
Nothing here serves the dashboard. That still runs on the trading machine, next
|
|
to the clicker, exactly as before.
|
|
|
|
## The pieces
|
|
|
|
| | |
|
|
|---|---|
|
|
| `/opt/autofirmer-updates/key.pem` | The signing key. **The extension's identity.** |
|
|
| `/opt/autofirmer-updates/public/` | `autofirmer.crx` and `updates.xml`, served as-is |
|
|
| `deploy/pack-extension.sh` | Signs `extension/` and writes both files |
|
|
| `deploy/docker-compose.yml` | An `nginx:alpine` that serves that folder and nothing else |
|
|
| `deploy/nginx/ext.conf.template` | Rendered into the autotrader stack's `conf.d` |
|
|
| `scripts/install-extension-policy.ps1` | The Windows side: one registry entry |
|
|
|
|
Extension id: `lkjbblagjjbldlkeaaffhiopdlhniaan`, derived from the public half of
|
|
the signing key and repeated in three places that must agree — `key` in
|
|
`extension/manifest.json`, `appid` in `updates.xml`, and the forcelist entry in
|
|
the policy script. `pack-extension.sh` refuses to publish when the first two
|
|
have drifted apart, because the failure otherwise is silent: Chrome simply never
|
|
sees an update that applies to it.
|
|
|
|
**Losing `key.pem` means losing the extension.** A new key is a new id, which
|
|
means a new policy entry on every machine and a fresh install rather than an
|
|
update. It exists in one place, deliberately — it is not in the repo, and a
|
|
compromised copy would let someone else publish an extension your browsers
|
|
would trust and install.
|
|
|
|
## Ingress
|
|
|
|
This stack owns no ingress. The droplet already runs one nginx on 80/443 for
|
|
`master.juicerroom.com` and `git.juicerroom.com`, from `/opt/autotrader/deploy`;
|
|
this joins that network, and `deploy.sh` renders a server block into that
|
|
stack's `nginx/conf.d`. Two consequences:
|
|
|
|
- The rendered `ext.conf` is untracked in the autotrader repo. It survives that
|
|
stack's pulls and deploys, but not its `bootstrap-tls.sh`, which clears
|
|
`conf.d` — re-run `./deploy.sh` here if that ever happens.
|
|
- `deploy.sh` reloads the shared nginx, so a mistake in the template would be a
|
|
mistake in front of the autotrader dashboard too. It runs `nginx -t` first and
|
|
refuses to reload on a bad config, and will not install the TLS server block
|
|
until the certificate actually exists.
|
|
|
|
## First run
|
|
|
|
```bash
|
|
cp .env.example .env # set EXT_HOST
|
|
./bootstrap-tls.sh # DNS check, certificate, then deploy.sh
|
|
```
|
|
|
|
`bootstrap-tls.sh` borrows the autotrader stack's certbot container and Let's
|
|
Encrypt account, reading `ACME_EMAIL` from its `.env` rather than keeping a
|
|
second copy. The A record has to point at the droplet first.
|
|
|
|
## Publishing a release
|
|
|
|
```bash
|
|
git pull && ./deploy/deploy.sh
|
|
```
|
|
|
|
`deploy.sh` packs whatever is in `extension/` at the version its `manifest.json`
|
|
declares, so **bump that version or nothing updates** — Chrome compares versions
|
|
and ignores a CRX that is not newer than what it has.
|
|
|
|
The browsers pick it up on Chrome's own schedule, roughly every five hours. They
|
|
also pick it up within seconds if the dashboard is running, because the
|
|
extension asks Chrome for a check as soon as the dashboard reports a newer
|
|
version than the one it is running.
|
|
|
|
## Installing it on a machine
|
|
|
|
`setup-windows.bat` and `setup-linux.sh` both offer this as their last step, so
|
|
a fresh instance needs nothing extra. To do it by hand, or on a machine that was
|
|
set up before this existed:
|
|
|
|
```powershell
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1
|
|
```
|
|
|
|
```bash
|
|
sudo scripts/install-extension-policy.sh
|
|
```
|
|
|
|
Both configure every Chromium-family browser they find, and narrowing that to
|
|
one is `-Browser Chrome`.
|
|
|
|
### The machine has to be managed
|
|
|
|
This is the constraint the whole approach turns on, and it is worth stating
|
|
plainly. On Windows, Chromium force-installs an extension from outside its own
|
|
web store **only** when the instance is joined to an Active Directory domain,
|
|
joined to Entra ID, or enrolled in Chrome Enterprise Core. Edge says the same of
|
|
the Microsoft Store. On an ordinary machine the policy is read and then blanked:
|
|
|
|
```
|
|
Value ["[BLOCKED]lkjbblagjjbldlkeaaffhiopdlhniaan;https://ext.juicerroom.com/updates.xml"]
|
|
Error Error at ExtensionInstallForcelist[0]: Invalid extension ID.
|
|
Warning This computer is not detected as enterprise managed so policy can only
|
|
automatically install extensions hosted on the Chrome Webstore.
|
|
```
|
|
|
|
The "invalid extension ID" is just `[BLOCKED]` being parsed as the id. Nothing
|
|
about the CRX, the update manifest or the hive is wrong — the machine is what
|
|
Chrome is refusing. Both scripts check for this and write nothing rather than
|
|
leaving a permanent policy error and a "Managed by your organization" badge
|
|
behind; `-Force` overrides.
|
|
|
|
So the update server is only half a solution on an unmanaged box. The other half
|
|
is one of:
|
|
|
|
- **Load `extension/` unpacked.** No update channel, but it reloads itself from
|
|
disk when the dashboard reports a newer version, and the updater has already
|
|
pulled those files. Works today, any browser, nothing to enrol.
|
|
- **Enrol Chrome in Chrome Enterprise Core** (free, and it does not require a
|
|
domain-joined machine): one `CloudManagementEnrollmentToken` registry value
|
|
makes the browser managed, and everything here then works as designed.
|
|
- **Publish to a web store.** Store-hosted extensions update themselves with no
|
|
policy at all — at the price of a listing and a review on every release.
|
|
|
|
The Windows script writes to the current user's policy hive and needs no
|
|
elevation; `-Scope Machine` covers every account on the box and does need an
|
|
elevated shell. Linux has no per-user equivalent — Chrome only reads managed
|
|
policy from `/etc` — so that one is root-only and machine-wide. Both take
|
|
`-Remove` / `--remove`.
|
|
|
|
Afterwards, remove the unpacked copy from `chrome://extensions` — it now has the
|
|
same id, and Chrome will not load both — and restart the browser.
|
|
|
|
Development still works the old way: load `extension/` unpacked on a machine
|
|
with no policy entry. It keeps the same id (that is what `key` in the manifest
|
|
is for), and there it reloads itself from disk rather than asking Chrome for an
|
|
update.
|