Multi-tenant orchestration and an OCI session runtime for Xanh remote browsing.
xanh-docker authenticates a tenant, creates an isolated browser session, relays WebRTC signaling, and destroys the session on burn. A session owns one container and may contain one to four tabs; browser state is never shared across tenants.
Important
This repository is an engineering candidate, not a production release. Load, latency, memory-pressure, capacity, public-network, and operator-host soak gates still need current release evidence.
| Area | Implementation |
|---|---|
| Control plane | Node.js 24, strict TypeScript, Fastify 5, OIDC, tenant quotas, and lifecycle reconciliation |
| Signaling | Authenticated WebSocket gateway with exact-origin checks and one-use credentials |
| Session runtime | Native C++20/GStreamer runtime using WPE WebKit, H.264/Opus WebRTC, and control DataChannel navigation |
| Isolation | One container, network, tmpfs namespace, quota, and credential set per tenant session |
| Deployment | Hardened Docker profile, Nginx TLS/WebSocket adapter, optional TURN secret mount, and immutable-image promotion tooling |
| Contracts | Versioned Xanh Tab OpenAPI snapshot and manifest |
The session image installs Debian's gstreamer1.0-wpe and libwpewebkit-2.0-1 packages. It does not compile or bundle the xanh-webkit desktop application. XANH_WEBKIT.lock records the shared browser-behavior lineage; session/image-manifest.json marks it as reference-only-not-built-into-image.
flowchart LR
C[Controller] -->|HTTPS / WebSocket| N[Nginx]
N --> O[Fastify orchestrator]
O -->|OIDC + tenant policy| A[Session service]
A --> D[Docker Engine]
D --> S[Isolated session container]
S --> W[WPE WebKit: 1-4 tabs]
W --> G[GStreamer H.264 / Opus]
G -->|WebRTC| C
Each container exposes one selected audio/video stream. Switching the active tab changes the selected GStreamer inputs without replacing the container or its tenant-scoped WebKit context.
- Tenant identity comes only from verified authentication context; request bodies cannot select a tenant.
- Production JWT verification requires an exact issuer, audience, JWKS URL, asymmetric algorithm allowlist, subject, and validated tenant claim. The current verifier validates time claims when present but does not require an
expclaim, so operators must enforce bounded token lifetime at the identity provider until that release gate is added. - Development bearer authentication is restricted to loopback and cannot start in production.
- Pairing tickets and signaling tokens are short-lived, one-use credentials whose stored material is SHA-256 hashed. The separately generated runtime bootstrap secret is scoped to one session container and may be reused for authenticated internal reconnects during that container's lifetime. It becomes unavailable only after successful container destruction; a failed cleanup remains observable and retryable.
- The public API omits tenant identity, container details, internal endpoints, and bootstrap secrets.
- The signaling gateway checks an exact allowed
Origin, authenticates the first frame, bounds payloads and buffering, and closes credentials during burn. - Container creation is transactional. Startup health failure removes both the partial container and its network.
- Restart reconciliation fails closed: only resources carrying the managed label and exact instance label are reclaimed. Legacy
dev.fireball.*labels are recognized only for in-place migration; new resources usedev.xanh.*. - Containers run non-root with a read-only root filesystem, dropped capabilities,
no-new-privileges, private PID/network state, bounded tmpfs, and reviewed seccomp/AppArmor policy. - TURN credentials are supplied through a read-only, root-owned host file. They are not copied into the image or Docker environment.
- Burn revokes pending and active credentials before cleanup. A cleanup failure remains visible as
failedand can be retried.
Container isolation is defense in depth, not a guarantee against every browser zero-day, kernel flaw, or container escape. Docker socket access is effectively host-control authority; use a dedicated host and restrict the socket to the orchestrator service identity.
| Method | Route | Purpose |
|---|---|---|
GET |
/healthz |
Process health; no authentication |
POST |
/orchestrator/v1/sessions |
Create a tenant-bound session and pairing ticket |
GET |
/orchestrator/v1/sessions/:id |
Read an owned session |
DELETE |
/orchestrator/v1/sessions/:id |
Revoke credentials and burn a session |
POST |
/orchestrator/v1/sessions/:id/signaling/tickets |
Rotate pending signaling credentials |
POST |
/orchestrator/v1/signaling/tickets/exchange |
Exchange a pairing ticket for a 30-second signaling token |
GET |
/orchestrator/v1/signaling |
Upgrade to the authenticated signaling WebSocket |
GET |
/orchestrator/v1/sessions/:id/tabs |
List the session's tabs |
POST |
/orchestrator/v1/sessions/:id/tabs |
Create and activate a tab |
PUT |
/orchestrator/v1/sessions/:id/tabs/:tabId/active |
Select the streamed tab |
PUT |
/orchestrator/v1/sessions/:id/tabs/:tabId/navigation |
Navigate to a validated HTTP(S) URL |
DELETE |
/orchestrator/v1/sessions/:id/tabs/:tabId |
Delete a tab while retaining at least one |
Authenticated routes require an OIDC bearer token in production. Development accepts Authorization: Bearer dev:<tenant>:<subject> on loopback only. Tickets and signaling tokens are never accepted in query strings.
After a WebSocket upgrade, the first text frame must be:
{"type":"authenticate","token":"<one-use-token>"}The gateway sends {"type":"ready","sessionId":"<uuid>"} only after the container runtime accepts its separate bootstrap secret.
- Node.js
>=24 <27 - npm with lockfile support
- Docker Engine API
1.47or compatible - Linux for the runtime image; Docker Desktop can build the Linux image on macOS
npm ci
npm ci --prefix session --ignore-scripts
npm run check
docker build -f deploy/Dockerfile -t xanh/orchestrator:dev .npm run check validates the contracts, deployment adapter, candidate evidence, seccomp provenance, session image metadata, TypeScript build, and automated tests.
For an Intel Mac reproducibility smoke:
docker build --platform linux/amd64 \
-f deploy/Dockerfile \
-t xanh/orchestrator:macos-smoke .The production container command fails closed until the required OIDC and immutable-image settings are present. For an internal health smoke, explicitly use development mode, retain the loopback listener, and query /healthz from inside the container.
There is no user-facing dashboard in this repository, so Docker Desktop or API screenshots are not presented as product UI.
The minimal production shape is:
NODE_ENV=production
XANH_HOST=127.0.0.1
XANH_PORT=8787
XANH_INSTANCE_ID=primary
XANH_OIDC_ISSUER=https://identity.example.com/
XANH_OIDC_AUDIENCE=xanh-docker
XANH_OIDC_JWKS_URL=https://identity.example.com/.well-known/jwks.json
XANH_OIDC_TENANT_CLAIM=tenant_id
XANH_PUBLIC_ORIGINS=https://browser.example.com
XANH_SESSION_IMAGE=ghcr.io/lamppkk/xanh-session@sha256:<64-hex-digest>
XANH_SESSION_SECCOMP_PROFILE=/etc/xanh/xanh-session-seccomp.json
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_API_VERSION=1.47Production requires an immutable session-image digest and the exact reviewed seccomp profile. On AppArmor hosts, load deploy/apparmor/xanh-session and set XANH_SESSION_APPARMOR_PROFILE=xanh-session. Optional TURN configuration uses XANH_ICE_SERVERS_FILE with an absolute, root-owned, read-only host path.
Do not disable WebKit sandboxing, use seccomp=unconfined, add CAP_SYS_ADMIN, remove Docker system-path masks, or run privileged containers. Two live orchestrators must not share an XANH_INSTANCE_ID.
Read the operational details before deployment:
- Session image, provenance, and promotion gates
- Nginx and TURN deployment adapters
- Reviewed seccomp profile and provenance
session/Dockerfile targets linux/amd64 and linux/arm64 from a digest-pinned base image and exact source revisions. Debian security packages are resolved at build time, so release identity comes from the tested output digest, SBOM, attestations, and signature rather than a claim that every package input is immutable. The runtime is UID/GID 10001, uses a read-only root filesystem plus /run/xanh-session tmpfs, and records component versions in the artifact.
The manual session-candidate workflow is the release gate. It must rebuild and test each native platform, publish a two-platform OCI index by exact digest, emit an SPDX SBOM and attestations, and sign the index with GitHub OIDC/Cosign. Historical binaries or a green source-only CI run are not promotion evidence.
Inspect bounded runtime metadata with:
docker run --rm --entrypoint /usr/bin/node <xanh-session-image> \
/opt/xanh-session/healthcheck.mjs --metadata| Path | Purpose |
|---|---|
src/ |
Fastify API, authentication, lifecycle, Docker adapter, and signaling gateway |
session/ |
WPE/GStreamer runtime, supervisor, tab control, healthcheck, and image definition |
deploy/ |
Orchestrator image, Nginx, TURN example, seccomp, and AppArmor assets |
contracts/xanhtab/ |
Frozen Xanh Tab API snapshot consumed by this repository |
docs/session-image.md |
Image architecture, evidence, and promotion runbook |
docs/deployment-adapters.md |
TLS, WebSocket, and TURN deployment guidance |
test/ |
Unit, contract, authentication, isolation, and lifecycle coverage |
- Re-run the manual session candidate lane after material image changes and promote only its exact signed digest.
- Complete load, memory-pressure, bitrate, latency, and capacity benchmarks before publishing sizing claims.
- Treat public DNS, certificate issuance, firewall policy, and operator-host soak testing as deployment gates.
- Regenerate the frozen Xanh Tab contract only from an explicitly promoted upstream artifact.
| Repository | Role |
|---|---|
xanh-webkit |
Multi-platform reference hosts, shared engine policies, and release gates |
xanh-android |
Android browser application |
xanh-ios |
Native iPhone and iPad browser |
xanh-webview |
Cross-platform embedding contract |
xanh-tab |
Raspberry Pi remote-browser appliance and controller contract |
xanh-docker |
This orchestrator and OCI session runtime |
This repository does not currently include a project-wide license file. Until one is added, no general permission to copy, modify, or redistribute the Xanh Docker source is granted. Third-party inputs retain their own terms; the Moby-derived seccomp material includes its Apache-2.0 notice under deploy/seccomp/.