Install Chrome, and keep /opt in the image rather than in /var #5

Merged
sebastian merged 1 commit from chrome-and-image-owned-opt into main 2026-08-24 17:33:27 +02:00
Owner

Adds Google Chrome, and changes /opt from host state to image content so that it can survive an upgrade.

/opt is now image content

bootc-rootfs.sh used to write the ostree convention, /opt/var/opt. That has the same problem as everything else in /var: bootc only materialises the image's /var on the initial install, so anything a package installs under /opt at build time never appears on a host that arrives through bootc upgrade. The generated tmpfiles entries exist precisely to work around this, and a 431 MiB browser is not something to recreate with a tmpfiles entry.

/opt is now the one state directory that is moved rather than emptied: contents go to /usr/lib/opt — the same relocation rpm-ostree does — and /opt becomes a symlink to it. Vendor software installed there ships read-only and composefs-verified like the rest of /usr.

The cost is a writable /opt on a deployed host. /usr/local is still backed by /var for machine-local additions. ostree-state-overlay@opt.service would hand writability back, but it ships in ostree-boot, which this image does not install and which also carries ostree's own boot-time units into an image that boots off bootc's dracut module — a change to test, not a drop-in. Upstream's guidance in ostree-state-overlay@.service(8) is to prefer symlinking just the mutable parts into /var anyway.

Chrome

Second vendor apt repository after Anthropic's, same pattern in build/google-chrome.sh: fetch the key, pin its fingerprint (EB4C1BFD…D38B4796, which also matches the key embedded in Chrome's own postinst), write a deb822 entry, install.

Three corrections beyond claude-desktop.sh:

  • Repo entry named google-chrome-os4.sources. repo_add_once="false" suppresses the postinst's new-install path, but a second branch recreates a google-chrome.sources that already exists, on the theory a dist-upgrade may have commented it out. Under our own name it matches neither.
  • AppArmor profile re-pointed. It attaches to the literal path /opt/google/chrome/chrome, and AppArmor matches the resolved path rather than the symlink, so it stops attaching once the tree moves under /usr/lib/opt. It is inert on Debian, which does not restrict unprivileged user namespaces the way Ubuntu does — but a profile that silently matches nothing is worse than none.
  • /etc/cron.daily/google-chrome removed. It symlinks into /opt/google/chrome/cron/, where the script calls install_key unconditionally and writes to /usr/share/keyrings — read-only on a deployed host. Nothing runs it today because no cron daemon is installed; removing it keeps that true if one is ever added.

Why not just chromium

chromium is in trixie main and most arguments for Google's build don't apply to it: proprietary_codecs=true, ffmpeg_branding="Chrome", use_vaapi=true on amd64, is_official_build=true, Google-assigned API keys in /etc/chromium.d/apikeys, and version parity with Chrome stable. Installed size is comparable (~375 MiB vs ~440 MiB).

What decided it is what Debian cannot ship: no Widevine CDM (widevine-installer is contrib, trixie-backports only), so no Netflix/Prime/Spotify web; and --media-router=0 by default, so no Chromecast. Also no component updater, so no CRLSet revocation updates.

Both can be installed side by side if wanted; update-alternatives sorts out x-www-browser.

Verified

  • The /opt relocation block, against a fake root with content and with an empty /opt.
  • The AppArmor sed, against the real profile from the current deb — produces profile google-chrome-stable /usr/lib/opt/google/chrome/chrome flags=(unconfined).
  • No bootc container lint check touches /opt (all fifteen in bootc 1.16.9).
  • No sysusers warning: the chromemgmt group is only created when /etc/default/google-chrome sets install_device_trust_key_management_command=true, which this does not.
  • var-tmpfiles gets simpler — nothing lands in /var, so d /var/opt is dropped from os4-base-dirs.conf.

Not verified

This has not been built or booted. The /opt/usr/lib/opt move is a boot-path change and wants a real bootc install before it goes near hardware. The specific thing to watch on first boot is that /opt/google/chrome/chrome-sandbox keeps its setuid bit through composefs — the composefs root mounts ro without nosuid and sudo/newuidmap work today, but that is inference rather than a test.

🤖 Generated with Claude Code

Adds Google Chrome, and changes `/opt` from host state to image content so that it can survive an upgrade. ## `/opt` is now image content `bootc-rootfs.sh` used to write the ostree convention, `/opt` → `/var/opt`. That has the same problem as everything else in `/var`: bootc only materialises the image's `/var` on the initial install, so anything a package installs under `/opt` at build time never appears on a host that arrives through `bootc upgrade`. The generated tmpfiles entries exist precisely to work around this, and a 431 MiB browser is not something to recreate with a tmpfiles entry. `/opt` is now the one state directory that is moved rather than emptied: contents go to `/usr/lib/opt` — the same relocation rpm-ostree does — and `/opt` becomes a symlink to it. Vendor software installed there ships read-only and composefs-verified like the rest of `/usr`. The cost is a writable `/opt` on a deployed host. `/usr/local` is still backed by `/var` for machine-local additions. `ostree-state-overlay@opt.service` would hand writability back, but it ships in `ostree-boot`, which this image does not install and which also carries ostree's own boot-time units into an image that boots off bootc's dracut module — a change to test, not a drop-in. Upstream's guidance in `ostree-state-overlay@.service(8)` is to prefer symlinking just the mutable parts into `/var` anyway. ## Chrome Second vendor apt repository after Anthropic's, same pattern in `build/google-chrome.sh`: fetch the key, pin its fingerprint (`EB4C1BFD…D38B4796`, which also matches the key embedded in Chrome's own postinst), write a deb822 entry, install. Three corrections beyond `claude-desktop.sh`: - **Repo entry named `google-chrome-os4.sources`.** `repo_add_once="false"` suppresses the postinst's new-install path, but a second branch recreates a `google-chrome.sources` that already exists, on the theory a dist-upgrade may have commented it out. Under our own name it matches neither. - **AppArmor profile re-pointed.** It attaches to the literal path `/opt/google/chrome/chrome`, and AppArmor matches the resolved path rather than the symlink, so it stops attaching once the tree moves under `/usr/lib/opt`. It is inert on Debian, which does not restrict unprivileged user namespaces the way Ubuntu does — but a profile that silently matches nothing is worse than none. - **`/etc/cron.daily/google-chrome` removed.** It symlinks into `/opt/google/chrome/cron/`, where the script calls `install_key` unconditionally and writes to `/usr/share/keyrings` — read-only on a deployed host. Nothing runs it today because no cron daemon is installed; removing it keeps that true if one is ever added. ### Why not just `chromium` `chromium` is in trixie `main` and most arguments for Google's build don't apply to it: `proprietary_codecs=true`, `ffmpeg_branding="Chrome"`, `use_vaapi=true` on amd64, `is_official_build=true`, Google-assigned API keys in `/etc/chromium.d/apikeys`, and version parity with Chrome stable. Installed size is comparable (~375 MiB vs ~440 MiB). What decided it is what Debian cannot ship: no Widevine CDM (`widevine-installer` is `contrib`, trixie-backports only), so no Netflix/Prime/Spotify web; and `--media-router=0` by default, so no Chromecast. Also no component updater, so no CRLSet revocation updates. Both can be installed side by side if wanted; `update-alternatives` sorts out `x-www-browser`. ## Verified - The `/opt` relocation block, against a fake root with content and with an empty `/opt`. - The AppArmor `sed`, against the real profile from the current deb — produces `profile google-chrome-stable /usr/lib/opt/google/chrome/chrome flags=(unconfined)`. - No `bootc container lint` check touches `/opt` (all fifteen in bootc 1.16.9). - No `sysusers` warning: the `chromemgmt` group is only created when `/etc/default/google-chrome` sets `install_device_trust_key_management_command=true`, which this does not. - `var-tmpfiles` gets simpler — nothing lands in `/var`, so `d /var/opt` is dropped from `os4-base-dirs.conf`. ## Not verified **This has not been built or booted.** The `/opt` → `/usr/lib/opt` move is a boot-path change and wants a real `bootc install` before it goes near hardware. The specific thing to watch on first boot is that `/opt/google/chrome/chrome-sandbox` keeps its setuid bit through composefs — the composefs root mounts `ro` without `nosuid` and `sudo`/`newuidmap` work today, but that is inference rather than a test. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Install Chrome, and keep /opt in the image rather than in /var
All checks were successful
Publish / build (pull_request) Successful in 29m55s
Publish / build (push) Successful in 33m53s
6b01f75e5a
/opt was a symlink to /var/opt, which bootc only materialises on the initial
install -- so anything a package puts there is missing on every host that
arrives through `bootc upgrade`. bootc-rootfs.sh now moves /opt to
/usr/lib/opt and leaves the conventional path as a symlink, the same
relocation rpm-ostree does. Vendor software under /opt then ships read-only
and composefs-verified like the rest of /usr, at the cost of a writable /opt
on a deployed host; /usr/local is still backed by /var for that.

Chrome is the first thing that needs it, at 431 MiB into /opt. Its packaging
takes two corrections beyond the claude-desktop.sh pattern. The AppArmor
profile attaches to the literal path /opt/google/chrome/chrome and AppArmor
matches the resolved path, not the symlink, so it is re-pointed at
/usr/lib/opt -- the profile is inert on Debian, which does not restrict
unprivileged user namespaces the way Ubuntu does, but one that silently
matches nothing is worse than none. And /etc/cron.daily/google-chrome runs a
script that calls install_key unconditionally, writing to /usr/share/keyrings,
which is read-only on a deployed host; nothing runs it today because no cron
daemon is installed, and removing the symlink keeps that true.

The repo entry is named google-chrome-os4.sources rather than the name Chrome
uses itself: repo_add_once="false" suppresses the postinst's new-install path,
but it recreates a google-chrome.sources that already exists regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sebastian deleted branch chrome-and-image-owned-opt 2026-08-24 17:33:27 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sebastian/os4!5
No description provided.