Add virtual-browser templates - #464
Merged
Merged
Conversation
june-hua
force-pushed
the
june/rbi-chromium-policy
branch
2 times, most recently
from
August 24, 2026 17:58
9272bdf to
fd4eca9
Compare
june-hua
force-pushed
the
june/rbi-chromium-policy
branch
4 times, most recently
from
August 25, 2026 03:24
a10bd24 to
e5c5f58
Compare
june-hua
commented
Aug 25, 2026
yonghaoy
approved these changes
Aug 25, 2026
yonghaoy
left a comment
Contributor
There was a problem hiding this comment.
can't wait to see the demo!
june-hua
force-pushed
the
june/rbi-chromium-policy
branch
from
August 25, 2026 19:45
bdf30f7 to
b95c809
Compare
Runs JupyterLab behind remote browser isolation: a server-side Chromium (Selkies, Wayland) streamed to the client as pixels, so app data never reaches the client browser. - browser-common/: shared browser image. Pinned Chromium base; managed enterprise policy generated from the APP_ORIGIN build arg (URLAllowlist = the app, URLBlocklist = *, downloads/devtools/remote-debugging/file-dialogs/clipboard/ extensions off). Exfil-relevant Selkies settings locked with "<value>|locked" so a client on the data websocket cannot re-enable them: clipboard-out, command (remote shell), sharing/collab/shared. File transfers upload-only; sidebar hidden; 20fps/CRF28; audio/mic off (left unlocked for a future accessibility path). - virtual-browser-jupyter/: the template. app (browser, --kiosk) + jupyterlab (internal only, no published ports). shm_size 2gb so Chromium's renderer doesn't crash. CHROME_CLI uses --disable-features=WaylandFractionalScaleV1 to fix the Chromium/Wayland fractional-scaling click-offset (client-agnostic; keeps Wayland auto-fit). - Base image pinned in feature-versions/state.json. - Smoke test asserts the managed policy loaded and the exfil settings are locked; registered in test-pr.yaml.
RStudio Server behind the same remote-browser isolation as virtual-browser-jupyter, reusing the shared browser-common image (managed policy, Selkies exfil locks, --kiosk + WaylandFractionalScaleV1 offset fix + post-load resize). - Backend is the prebuilt rocker/tidyverse image on 8787 with DISABLE_AUTH; internal only (no published ports), reached by the browser over app-network. APP_ORIGIN and CHROME_CLI target http://rstudio:8787. - Parameterized the shared smoke test (BACKEND_CONTAINER / APP_ORIGIN via env) so it covers both templates; added tests/virtual-browser-rstudio.sh and registered the template in test-pr.yaml.
The synthetic resize event didn't fix the offset: the Selkies client re-negotiates only on a real dimension change, so a dispatched resize with unchanged dimensions is a no-op. Instead post the client's own "resetResolutionToWindow" message (it has a handler that recomputes resolution from the container's current size) on staggered timers after load, which is what actually re-syncs the click mapping.
The injected timers that posted resetResolutionToWindow (and the earlier synthetic resize variant) re-negotiated the stream and turned it black a few seconds after load, and did not correct the initial click mapping. Drop the injection; the image keeps only the managed policy and Selkies env.
Removes the --disable-features=WaylandFractionalScaleV1 flag from both templates' CHROME_CLI and the related comments, README troubleshooting note, and Dockerfile header note. Pairs with the earlier removal of the injected client resolution script, returning the image to policy + Selkies env only.
- APP_ORIGIN is scheme-less so the policy URLAllowlist also covers ws:// (kernel/RStudio websockets), not just http://. - chmod 444 the baked managed policy so it can't be modified in-container. - Drop --proxy-server/--proxy-bypass-list from CHROME_CLI; the policy URLBlocklist already blocks Chromium from reaching any non-app origin.
june-hua
force-pushed
the
june/rbi-chromium-policy
branch
from
August 25, 2026 20:41
dcd0061 to
2dc8cc1
Compare
It's the only Dockerfile in browser-common, so use the default name (and drop the now-redundant compose `dockerfile:` keys). Update the state.json filter. Trim redundant comments across the templates.
Set jdkDistro="tem" so the Java feature installs a prebuilt Temurin JDK instead of building via SDKMAN, which was failing during the devcontainer build. Matches the approach used for the other apps.
do not allow file browsing, this exposes the entire Chromium container's filesystem (e.g. /config/ssl/cert.key) for browsing and upload.
pantherman594
approved these changes
Aug 27, 2026
pantherman594
left a comment
Contributor
There was a problem hiding this comment.
Looks great, thanks!
minimize app in app mode causes app to permanently disappear
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Based off changes in #463. Adds two app templates that serve a backend app (JupyterLab / RStudio)
through a server-side Chromium session rendered by Selkies and
streamed to the client as pixels, sharing one hardened browser front end.
Templates
virtual-browser-jupyter— JupyterLab through the streamed browser.virtual-browser-rstudio— RStudio Server through the streamed browser.Each runs two containers on an internal
app-network:application-server(the Chromium/Selkiesfront end, the only published container, port
3000) and the backend (jupyterlab:8888/rstudio:8787, internal only). Chromium opens the backend in--kiosk.Shared browser front end (
src/browser-common/)Both templates share one image. Compose
include:can't override an imported service, so eachtemplate defines its own
appservice pointingbuild.contextat../browser-commonand passing anAPP_ORIGINbuild arg.browser.Dockerfile— pinned Chromium base + Selkies/hardening env; generates the managedpolicy from the template at build.
managed-policy.json.tmpl— managed Chromium enterprise policy;__APP_ORIGIN__issubstituted with the template's origin.
Managed Chromium policy
Baked into
/etc/chromium/policies/managed/workbench-rbi.json(immutable; baked, not mounted, sincethe workspace mount is user-writable).
URLAllowlist= the single app origin,URLBlocklist=["*"](fails closed). Also disables downloads/export, file dialogs/file-system access,clipboard-out, printing, devtools + remote debugging, extensions, new tabs / pop-ups / off-app
navigation, incognito/extra profiles, and password manager / autofill / translation / notifications.
Selkies hardening (image env)
Sidebar hidden; sharing/collab disabled; clipboard-out and command disabled and
|locked; filetransfers upload-only; audio/mic off; framerate/encoding tuned.
Base image pinning
lscr.io/linuxserver/chromium@sha256:4c7b9086…with afeature-versions/state.jsonentry soupdate.shtracks it (dedicatedsrc/.*/browser\.Dockerfilefilter, since the existingDockerfilefilters don't match).Tests & CI
Shared
tests/common/virtual-browser.bats(parameterized byBACKEND_CONTAINER/APP_ORIGIN),per-template runners, and both templates registered in
test-pr.yaml.Embedding requirement (important)
The streamed client only starts its video pipeline on load if the host page embeds it with an
autoplay-capable permissions policy:
Without this (cross-origin iframe), the app shows a black screen until the user switches tabs and
back. This is a host-page requirement, not a container setting — no Selkies/pixelflux env changes
are needed (earlier
H264_STREAMING_MODE/PIXELFLUX_WAYLANDexperiments were removed asunnecessary once the iframe permissions were fixed). The host-side fix lives in verily1 (adds these
to the app iframe's
allow). See the templates' README "Embedding" sections.