Add Host API and fix kiosk performance
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
[submodule "serving/kiosk-app"]
|
||||
path = serving/kiosk-app
|
||||
url = git@git.bitasylum.org:audiobuzz/fleet-kiosk-app.git
|
||||
branch = restyle
|
||||
|
||||
@@ -31,6 +31,11 @@ files/etc/dracut.conf.d/90-diskless.conf no-hostonly + drivers + modules
|
||||
files/plymouth/themes/fleet/ Custom boot splash (scripted security
|
||||
checklist; driven by hook status tokens)
|
||||
files/kiosk/ weston+Chromium kiosk + chromium lockdown policy
|
||||
(policy allows PNA to the host-info agent)
|
||||
files/hostapi/ Go host-info agent (fleet-hostapi): read-only,
|
||||
loopback :9099 /v1/host for the kiosk SPA
|
||||
(HOST-API.md contract). Built in 2b-hostapi,
|
||||
fapolicyd-trusted, golang removed in 2d.
|
||||
scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline
|
||||
scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image
|
||||
scripts/build-ipxe-oci.sh sbsign snponly.efi -> FROM-scratch fleet-ipxe image
|
||||
|
||||
+56
-3
@@ -233,6 +233,13 @@ build {
|
||||
destination = "/tmp/"
|
||||
}
|
||||
|
||||
# Workstation host-info API (Go). Source staged to /tmp/hostapi; compiled +
|
||||
# installed in the 2b-hostapi step. The kiosk SPA reads it at localhost:9099.
|
||||
provisioner "file" {
|
||||
source = "files/hostapi"
|
||||
destination = "/tmp/"
|
||||
}
|
||||
|
||||
# Custom 'fleet' Plymouth theme (scripted security-handshake splash). Staged to
|
||||
# /tmp/plymouth, installed + set default in the package step before dracut runs.
|
||||
provisioner "file" {
|
||||
@@ -418,8 +425,13 @@ build {
|
||||
# --use-pixman + chromium --disable-gpu — candidate to drop once the kiosk
|
||||
# is display-verified; see TODO #4.)
|
||||
"dnf install -y --setopt=tsflags=nodocs weston chromium mesa-dri-drivers mesa-libEGL mesa-libgbm dejavu-sans-fonts",
|
||||
# Unprivileged, passwordless kiosk user; video/input as a seat fallback.
|
||||
# Unprivileged, passwordless kiosk user. Groups: 'video' (KMS /dev/dri/card0
|
||||
# for weston) + 'input' (libinput) as a seat fallback; 'render'
|
||||
# (/dev/dri/renderD128) so Chromium's GPU process can do hardware raster on
|
||||
# real hardware (kiosk-session enables GPU when a real KMS driver is present;
|
||||
# render nodes aren't logind-seat-ACL'd on EL9, so group membership is needed).
|
||||
"id kiosk >/dev/null 2>&1 || useradd -m -G video,input kiosk",
|
||||
"getent group render >/dev/null 2>&1 && usermod -aG render kiosk || true",
|
||||
# Install the staged unit, weston config, and session wrapper.
|
||||
"install -D -m0755 /tmp/kiosk/kiosk-session /usr/local/bin/kiosk-session",
|
||||
"install -D -m0644 /tmp/kiosk/kiosk.service /etc/systemd/system/kiosk.service",
|
||||
@@ -452,6 +464,44 @@ build {
|
||||
]
|
||||
}
|
||||
|
||||
# --- 2b-hostapi. Workstation host-info API (Go, loopback, read-only) ------
|
||||
# Compiles files/hostapi into /usr/local/bin/fleet-hostapi and enables it as a
|
||||
# systemd service the kiosk SPA reads at http://localhost:9099/v1/host (the
|
||||
# HOST-API.md contract). Build-time, image-GENERIC facts (image version +
|
||||
# weston/chromium versions) are written to /etc/fleet-hostinfo.json; everything
|
||||
# host-specific (kernel, hostname, IP, MAC, board, uptime) is read at RUNTIME,
|
||||
# so NO per-host state lands in the image (invariant #1). The Go toolchain is
|
||||
# installed here and removed in 2d hygiene; it never ships.
|
||||
provisioner "shell" {
|
||||
environment_vars = [
|
||||
"IMAGE_VERSION=${var.image_version}",
|
||||
]
|
||||
inline = [
|
||||
"set -euxo pipefail",
|
||||
"dnf -y install golang",
|
||||
# Reproducible, dependency-free static build (the verity hash must be a pure
|
||||
# function of the inputs — invariant #8). GOPROXY=off + GOTOOLCHAIN=local
|
||||
# guarantee no network/toolchain fetch; -trimpath + -buildid= strip paths
|
||||
# and the build id. Stdlib-only, so this needs no modules. NOTE: a golang
|
||||
# package update changes the binary (hence the hash) — a reproducibility-
|
||||
# sensitive input, like the STIG semanage store; build twice to confirm.
|
||||
"install -d -m0755 /tmp/gocache",
|
||||
"env -C /tmp/hostapi CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=off GOTOOLCHAIN=local GOFLAGS=-mod=mod GOCACHE=/tmp/gocache GOPATH=/tmp/gopath go build -trimpath -buildvcs=false -ldflags '-s -w -buildid=' -o /usr/local/bin/fleet-hostapi .",
|
||||
"chmod 0755 /usr/local/bin/fleet-hostapi",
|
||||
"install -D -m0644 /tmp/hostapi/fleet-hostapi.service /etc/systemd/system/fleet-hostapi.service",
|
||||
# Build-time facts file (image-generic; same for every host of a release).
|
||||
# rpm '%%{...}' is doubled so Packer's HCL2 doesn't parse %{ as a directive.
|
||||
"WESTON_VER=$(rpm -q --qf '%%{VERSION}' weston 2>/dev/null || echo '')",
|
||||
"CHROMIUM_VER=$(rpm -q --qf '%%{VERSION}' chromium 2>/dev/null || rpm -q --qf '%%{VERSION}' chromium-browser 2>/dev/null || echo '')",
|
||||
"printf '{\"image\":\"%s\",\"weston\":\"%s\",\"chromium\":\"%s\"}\\n' \"$IMAGE_VERSION\" \"$WESTON_VER\" \"$CHROMIUM_VER\" > /etc/fleet-hostinfo.json",
|
||||
"chmod 0644 /etc/fleet-hostinfo.json",
|
||||
"restorecon -R /usr/local/bin/fleet-hostapi /etc/systemd/system/fleet-hostapi.service /etc/fleet-hostinfo.json 2>/dev/null || true",
|
||||
"systemctl enable fleet-hostapi.service",
|
||||
"rm -rf /tmp/hostapi /tmp/gocache /tmp/gopath",
|
||||
"echo '==> fleet-hostapi enabled (localhost:9099/v1/host)'",
|
||||
]
|
||||
}
|
||||
|
||||
# --- 2c. Admin access: sudo-enabled key user + locked root --------------
|
||||
# Fleet-generic (same user + key on every host => NOT per-host state). The
|
||||
# key comes from var.ssh_authorized_key (CI secret). If no key is supplied we
|
||||
@@ -701,9 +751,9 @@ build {
|
||||
# custom kiosk files: add them to fapolicyd's trust DB and rebuild it. weston
|
||||
# /chromium are RPM-installed so already trusted. (See docs/stig-deviations.md.)
|
||||
"if command -v fapolicyd-cli >/dev/null 2>&1; then",
|
||||
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
|
||||
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll /usr/local/bin/fleet-hostapi; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
|
||||
" fapolicyd-cli --update 2>/dev/null || true",
|
||||
" echo '==> fapolicyd: trusted custom binaries (kiosk-session, fleet-sb-enroll) + rebuilt trust db'",
|
||||
" echo '==> fapolicyd: trusted custom binaries (kiosk-session, fleet-sb-enroll, fleet-hostapi) + rebuilt trust db'",
|
||||
"fi",
|
||||
]
|
||||
}
|
||||
@@ -776,6 +826,9 @@ build {
|
||||
# already downloaded to the host) — slims the erofs + cuts attack surface.
|
||||
# Keep auditd (it's part of the hardened runtime). Best-effort.
|
||||
"dnf remove -y openscap-scanner scap-security-guide openscap-utils 2>/dev/null || true",
|
||||
# The Go toolchain was only needed to compile fleet-hostapi (2b-hostapi);
|
||||
# the static binary ships, the compiler does not.
|
||||
"dnf remove -y golang 2>/dev/null || true",
|
||||
# The build-time STIG reports must not ship inside the read-only rootfs.
|
||||
"rm -f /root/stig-report.html /root/stig-arf.xml",
|
||||
"dnf clean all",
|
||||
|
||||
+105
-6
@@ -685,12 +685,26 @@ must be built around **never restarting the process that holds live connections.
|
||||
the same `/dev/disk/by-id/nvme-uuid.<uuid>` host symlink.
|
||||
- **Bootstrap unchanged:** `docker compose up` with one version still works —
|
||||
the `/artifacts` image volume is seeded into `./releases/${IMAGE_VERSION}`.
|
||||
- **BENCH-VERIFY:** (a) `load` a 2nd version while a host is connected to the
|
||||
1st → 1st's connection survives (no AER/reset on the running host); (b)
|
||||
`unload` drain-gate actually blocks while CONNS>0; (c) serving-host reboot
|
||||
reconciles all staged versions; (d) per-version UUID build is reproducible
|
||||
(two builds, same version → identical `data_ns_uuid`/`hash_ns_uuid` +
|
||||
`root_hash`). See `serving/spdk/README.md`.
|
||||
- **PARTIALLY BENCH-VERIFIED 2026-06-28:** long-lived target boots a host end
|
||||
to end — UKI served over HTTP → host boots → NVMe/TCP root connects to the
|
||||
persistent `nvmf_tgt` (single version `spdk-permanence-02ffd7d`). Surfaced +
|
||||
fixed several real bugs along the way: bootstrap seed must COPY not symlink
|
||||
(a symlink into the version-pinned `/artifacts` re-pointed a stale release at
|
||||
new data); `spdk-release` parses RPC JSON via **python3** (SPDK image has no
|
||||
jq); legacy `verity.json` (no per-version UUIDs) falls back to the fixed pair;
|
||||
mislabeled-dir guard (dir name vs verity.json `image_version`, case-insensitive);
|
||||
dropped the redundant discovery-listener re-add (daemon logged "Listener
|
||||
already exists"). **CI lowercases `image_version`** (gitea-main-ci vars step) —
|
||||
a mixed-case branch (`SPDK-permanence`) had baked a capital
|
||||
`fleet-SPDK-….efi` the lowercase boot URL couldn't fetch (iPXE "no such
|
||||
file"); booted via a local retag workaround pending a lowercase rebuild.
|
||||
- **STILL BENCH-VERIFY:** (a) `load` a 2nd version while a host is connected to
|
||||
the 1st → 1st's connection survives (no AER/reset on the running host); (b)
|
||||
`unload`/auto-reap drain-gate blocks while CONNS>0 and reaps after grace on a
|
||||
LIVE host; (c) serving-host reboot reconciles all staged versions; (d)
|
||||
per-version UUID build reproducible (two builds, same version → identical
|
||||
`data_ns_uuid`/`hash_ns_uuid` + `root_hash`); (e) the full fix on a clean
|
||||
LOWERCASE CI rebuild (no retag). See `serving/spdk/README.md`.
|
||||
- **Capacity:** each concurrently-served version costs target memory/hugepages +
|
||||
file-backed bdev overhead — plan for N live versions (current + previous +
|
||||
canary) during long rollouts (raise `HUGEPAGES`).
|
||||
@@ -864,6 +878,10 @@ smoke test.
|
||||
`docs/stig-deviations.md` #6 to reflect encrypted offload.
|
||||
(RELP+TLS via `omrelp`/`imrelp` is the alternative if guaranteed delivery is
|
||||
wanted alongside encryption.)
|
||||
- **fleet-hostapi: scope CORS to the kiosk origin.** The host-info agent ships
|
||||
with `FLEET_HOSTAPI_ALLOW_ORIGIN=*` (lab). For prod set it to the exact kiosk
|
||||
origin (default `http://boot.fleet.lab`) via the unit's commented
|
||||
`Environment=` line (or a drop-in). See #12.
|
||||
|
||||
## [ ] 9. (OPTIONAL / nice-to-have) Convert provisioning to Ansible where it pays off
|
||||
|
||||
@@ -1024,3 +1042,84 @@ non-destructive TPM capability check (device, key persist, NV roundtrip, openssl
|
||||
TPM2 provider, CSR signed by the TPM key) to de-risk the TPM half before any of
|
||||
this. Expected gap: the image installs `tpm2-tss` but no openssl TPM2 **provider**
|
||||
(`tpm2-openssl`) — add it to the build once the self-test confirms.
|
||||
|
||||
## [~] 12. Workstation host-info API (`fleet-hostapi`) for the kiosk
|
||||
|
||||
**IMPLEMENTED 2026-06-28.** A small **Go, read-only, loopback-only** HTTP agent on
|
||||
the workstation that reports host/OS/hardware facts to the kiosk SPA, per the
|
||||
[HOST-API.md](../HOST-API.md) contract (`GET http://localhost:9099/v1/host`).
|
||||
Source: `files/hostapi/` (stdlib-only, reproducible static build). Built + enabled
|
||||
by provisioner **2b-hostapi**; toolchain removed in 2d; binary fapolicyd-trusted
|
||||
in 2c-STIG-postfix (register #10). Chromium policy grants Private Network Access
|
||||
scoped to the kiosk origin (`files/kiosk/chromium-kiosk-policy.json`).
|
||||
|
||||
- **Per-host safety:** host-specific fields (kernel, hostname, IP, MAC, board,
|
||||
uptime) are read at RUNTIME; only image-generic facts (image version +
|
||||
weston/chromium versions) are baked, to `/etc/fleet-hostinfo.json` (invariant
|
||||
#1 preserved). Deliberately does NOT expose DMI serial (root-only/per-host) or
|
||||
any TPM/cert/identity material.
|
||||
- **Hardening shipped:** loopback bind enforced in code; `DynamicUser`, no caps,
|
||||
`ProtectSystem=strict`, `MemoryDenyWriteExecute`, `SystemCallFilter=@system-service`,
|
||||
`IPAddressDeny=any` (loopback only). Runs unprivileged, reads world-readable
|
||||
files only.
|
||||
|
||||
**Lab posture LEFT AS-IS (decided 2026-06-28) — revisit for prod:**
|
||||
- **CORS `Allow-Origin=*`** → scope to the kiosk origin (`http://boot.fleet.lab`).
|
||||
Consolidated into #8.
|
||||
- **SELinux:** runs in the default service domain (no custom policy module).
|
||||
**BENCH-VERIFY** no AVC denials for `fleet-hostapi` on an enforcing booted host
|
||||
(Wazuh/`fleet_audit` rule 100120 will surface any). If denials appear, decide:
|
||||
write a confined SELinux policy module vs adjust the sandbox.
|
||||
- **No auth** (justified: loopback + read-only + single-user appliance). Option if
|
||||
the threat model tightens: shared token in `kiosk.env` + header check.
|
||||
- **Info sensitivity:** exposes hostname/IP/MAC/board/versions to the kiosk. Drop
|
||||
MAC/IP if that's later deemed too much.
|
||||
- **Reproducibility:** the `golang` toolchain version is a reproducibility-sensitive
|
||||
input (a package bump changes the binary → verity hash); covered by the
|
||||
build-twice gate. Pin the go version only if drift becomes a problem.
|
||||
|
||||
**BENCH-VERIFY (functional):** (a) service starts under enforcing SELinux +
|
||||
fapolicyd (not blocked); (b) the kiosk SPA's System page fetches `/v1/host`
|
||||
successfully through the PNA allowance (preflight + GET); (c) facts render
|
||||
correctly on real hardware (board/CPU/GPU from DMI/DRM, not the virtio build VM).
|
||||
|
||||
## [~] 13. Kiosk GPU compositing + Chromium flag ownership
|
||||
|
||||
**GPU FIX APPLIED 2026-06-28.** The 820 (Intel HD620/i915) ran Chromium at ~150%
|
||||
CPU with periodic crashes: `--disable-gpu` in our `kiosk-session` forced software
|
||||
raster/compositing. Root cause confirmed from the live cmdline. Fixes in
|
||||
`files/kiosk/kiosk-session` + `diskless.pkr.hcl`:
|
||||
- **Runtime GPU probe** (fleet-generic image boots real HW *and* virtio VMs): a
|
||||
real KMS driver (`i915`/`amdgpu`/…) + `/dev/dri/renderD128` → GPU path
|
||||
(`--ignore-gpu-blocklist --enable-gpu-rasterization --enable-zero-copy`, weston
|
||||
GL renderer); else software (`--disable-gpu` + `weston --use-pixman`) so the VMs
|
||||
still render. No per-host state baked.
|
||||
- **kiosk user added to `render`** (renderD128 isn't logind-seat-ACL'd on EL9;
|
||||
`video` only covers card0/KMS for weston).
|
||||
- **Merged the distro wrapper's `--disable-features` into ours** — Chromium takes
|
||||
the LAST `--disable-features` (not the union), so ours was silently dropping the
|
||||
wrapper's Lens/Gemini/Compose disables.
|
||||
|
||||
**BENCH-VERIFY (on the 820):** GPU process alive (`chrome://gpu`), Compositor/
|
||||
ThreadPool threads near-idle + CPU drop, `gt_act_freq_mhz` rises under load, no
|
||||
SELinux AVC for `dri_device_t` from the kiosk domain (Wazuh `fleet_audit` 100120
|
||||
will surface any — if present, add a scoped SELinux allow or confirm the kiosk
|
||||
user's SELinux mapping is unconfined). Confirm VMs still render (software path).
|
||||
|
||||
**DECISION OPEN — Chromium flag ownership (security).** The distro chromium
|
||||
wrapper PREPENDS vendor defaults we don't control, including capability-broadening
|
||||
ones a locked kiosk arguably shouldn't carry: `--enable-extensions`,
|
||||
`--enable-user-scripts`, `--enable-plugins`, `--enable-printing`,
|
||||
`--auto-ssl-client-auth`, `--enable-sync`. Today the managed policy neutralises
|
||||
the security-relevant ones (sync off via `SyncDisabled`; sign-in/metrics/etc.),
|
||||
but extensions/user-scripts/auto-ssl-client-auth are NOT policy-locked. Option to
|
||||
**own the full flag set**: bypass/neutralise the wrapper (invoke the real binary,
|
||||
or empty its default-flags source) and specify everything ourselves incl. the
|
||||
VA-API `--enable-features` (now known:
|
||||
`AllowQt,WaylandLinuxDrmSyncobj,WaylandPerSurfaceScale,WaylandUiScale,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoDecodeLinuxZeroCopyGL,AcceleratedVideoEncoder`).
|
||||
Tradeoff: full determinism + drop unwanted capabilities, vs we then track Chromium
|
||||
feature renames ourselves instead of inheriting the distro's. Also fully dedupes
|
||||
`--disable-features`/`--enable-sync`. Needs locating the wrapper's flag source on
|
||||
the box first (so we don't lose VA-API or break its env setup). Pairs with policy
|
||||
adds: `ExtensionInstallBlocklist=["*"]`, disable user scripts, restrict
|
||||
AutoSelectCertificateForUrls.
|
||||
|
||||
@@ -143,10 +143,13 @@ hotkey disabled (e.g. a physically exposed terminal), re-select both rule IDs.
|
||||
- **SELinux enforcing** (`selinux --enforcing`; labels preserved into the erofs).
|
||||
- **fapolicyd** kept **enabled** (NOT waived) — it's the real exec-allowlist guard
|
||||
for the writable RAM overlay, which dm-verity does **not** cover. The only
|
||||
adjustment: the one non-RPM executable, `/usr/local/bin/kiosk-session`, is added
|
||||
to fapolicyd's trust DB at build (provisioner 2c-STIG-postfix) so the kiosk runs;
|
||||
everything else stays RPM-trust-only. (fapolicyd blocking that binary was the
|
||||
original kiosk-won't-start cause.)
|
||||
adjustment: the few non-RPM executables we ship are added to fapolicyd's trust DB
|
||||
at build (provisioner 2c-STIG-postfix) so they run; everything else stays
|
||||
RPM-trust-only. Trusted custom binaries: `/usr/local/bin/kiosk-session`,
|
||||
`/usr/local/bin/fleet-sb-enroll`, `/usr/local/bin/fleet-hostapi` (the loopback
|
||||
host-info agent, built from `files/hostapi`). Each is built into the immutable,
|
||||
dm-verity-protected root, so trusting it doesn't weaken the overlay exec guard.
|
||||
(fapolicyd blocking `kiosk-session` was the original kiosk-won't-start cause.)
|
||||
- **auditd** enabled; STIG audit rules applied; logs locally.
|
||||
- **USBGuard** enabled + enforcing (`block` target) with a HID/hub allowlist and
|
||||
`AuditBackend=LinuxAudit` (see #12) — blocks USB mass storage / unknown device
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# fleet-hostapi — workstation host-info agent
|
||||
|
||||
A tiny **read-only, loopback-only** Go HTTP service that runs on the diskless
|
||||
workstation and reports facts about the machine the kiosk renders on. It is the
|
||||
OS-side implementation of the contract in [`../../HOST-API.md`](../../HOST-API.md)
|
||||
(authored alongside the kiosk-app SPA that consumes it).
|
||||
|
||||
```
|
||||
GET http://localhost:9099/v1/host -> application/json (os, hardware, net, kiosk, agent)
|
||||
GET http://localhost:9099/healthz -> 200 ok
|
||||
```
|
||||
|
||||
## Properties (and why)
|
||||
|
||||
- **Loopback only.** Binds `127.0.0.1`; refuses to start on any routable address.
|
||||
Host facts must never reach the boot LAN.
|
||||
- **Read only.** No endpoint mutates anything; it reads `/proc`, `/sys`,
|
||||
`/etc/os-release`, and the build-time facts file.
|
||||
- **No per-host state baked in** (invariant #1). Host-specific fields (kernel,
|
||||
hostname, IP, MAC, board, uptime) are read at **runtime**. The only baked facts
|
||||
are image-generic — image version + weston/chromium versions — written to
|
||||
`/etc/fleet-hostinfo.json` by the build provisioner.
|
||||
- **Stdlib only → reproducible build** (the verity hash is a security property).
|
||||
Built `CGO_ENABLED=0 -trimpath -buildvcs=false -ldflags=-buildid=`; identical
|
||||
bytes across builds with the same Go toolchain.
|
||||
- **CORS + Private Network Access.** The SPA is cross-origin and the fetch is a
|
||||
Chromium private-network request, so responses carry
|
||||
`Access-Control-Allow-Private-Network: true`. The matching browser-side
|
||||
allowance is in `files/kiosk/chromium-kiosk-policy.json`
|
||||
(`InsecurePrivateNetworkRequestsAllowedForUrls`, scoped to the kiosk origin).
|
||||
|
||||
## Build / install
|
||||
|
||||
Done by the `2b-hostapi` provisioner in `diskless.pkr.hcl`: installs the Go
|
||||
toolchain, builds the static binary to `/usr/local/bin/fleet-hostapi`, writes the
|
||||
facts file, installs + enables `fleet-hostapi.service`, and adds the binary to
|
||||
fapolicyd trust (2c-STIG-postfix). The toolchain is removed in 2d hygiene — only
|
||||
the static binary ships.
|
||||
|
||||
Local build:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -buildvcs=false \
|
||||
-ldflags '-s -w -buildid=' -o fleet-hostapi .
|
||||
```
|
||||
|
||||
## Config (env, set in the unit)
|
||||
|
||||
| Var | Default | Notes |
|
||||
|---|---|---|
|
||||
| `FLEET_HOSTAPI_ADDR` | `127.0.0.1:9099` | must be loopback |
|
||||
| `FLEET_HOSTAPI_ALLOW_ORIGIN` | `*` | tighten to the kiosk origin in prod |
|
||||
| `FLEET_HOSTAPI_FACTS` | `/etc/fleet-hostinfo.json` | build-time facts |
|
||||
@@ -0,0 +1,52 @@
|
||||
[Unit]
|
||||
Description=Fleet workstation host-info API (loopback, read-only)
|
||||
Documentation=https://git.bitasylum.org/audiobuzz/diskless-workstation/HOST-API.md
|
||||
# Needs networking up only to read the primary IP/MAC; the listener is loopback.
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/local/bin/fleet-hostapi
|
||||
Environment=FLEET_HOSTAPI_ADDR=127.0.0.1:9099
|
||||
# Lab default: any origin may read (it's a loopback, read-only facts service on a
|
||||
# single-purpose kiosk). Tighten to the exact kiosk origin in production, e.g.
|
||||
# Environment=FLEET_HOSTAPI_ALLOW_ORIGIN=http://boot.fleet.lab
|
||||
Environment=FLEET_HOSTAPI_ALLOW_ORIGIN=*
|
||||
Environment=FLEET_HOSTAPI_FACTS=/etc/fleet-hostinfo.json
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
|
||||
# --- sandboxing (STIG-friendly; the agent only READS /proc, /sys, /etc) -------
|
||||
# Runs as a transient unprivileged user with no capabilities; everything it
|
||||
# reads is world-readable, so it needs no privilege at all.
|
||||
DynamicUser=yes
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectClock=yes
|
||||
ProtectHostname=yes
|
||||
ProtectProc=default
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
# Kernel-enforced loopback-only egress/ingress (belt + suspenders to the bind).
|
||||
IPAddressAllow=localhost
|
||||
IPAddressDeny=any
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,7 @@
|
||||
// fleet-hostapi: the workstation host-info agent (HOST-API.md contract).
|
||||
// Stdlib only — no external modules — so the build needs no network and is
|
||||
// trivially reproducible (a hard requirement: the rootfs/verity hash must be a
|
||||
// pure function of the inputs). Keep it dependency-free.
|
||||
module fleet-hostapi
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,400 @@
|
||||
// fleet-hostapi — read-only workstation host-info agent for the kiosk.
|
||||
//
|
||||
// Implements the contract in HOST-API.md: a tiny HTTP service on the diskless
|
||||
// workstation that reports facts about the machine the kiosk renders on. The
|
||||
// kiosk SPA (served from the boot server, a different origin) fetches
|
||||
//
|
||||
// GET http://localhost:9099/v1/host -> application/json
|
||||
//
|
||||
// Design constraints (see CLAUDE.md invariants):
|
||||
// - LOOPBACK ONLY. Never bind a routable address — this leaks host facts and
|
||||
// must never reach the boot LAN.
|
||||
// - READ ONLY. No endpoint mutates anything; the agent only reads /proc, /sys,
|
||||
// /etc/os-release, and a build-time facts file.
|
||||
// - NO PER-HOST STATE baked into the image. Everything host-specific (kernel,
|
||||
// hostname, IP, MAC, board, uptime) is read at RUNTIME. The only baked facts
|
||||
// are image-generic (image version + weston/chromium versions), written to a
|
||||
// facts file by the build provisioner — same for every host of a release.
|
||||
// - Stdlib only -> reproducible build (the verity hash is a security property).
|
||||
//
|
||||
// CORS + Private Network Access: the SPA is cross-origin AND the fetch is a
|
||||
// Chromium "private network" request, so every response (incl. the OPTIONS
|
||||
// preflight) carries Access-Control-Allow-Private-Network: true.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// agentVersion is overridable at build time via -ldflags "-X main.agentVersion=…".
|
||||
var agentVersion = "1.0.0"
|
||||
|
||||
// ---- response shape (HOST-API.md) — all fields optional, omit what's unknown --
|
||||
|
||||
type osInfo struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Kernel string `json:"kernel,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
type hwInfo struct {
|
||||
CPU string `json:"cpu,omitempty"`
|
||||
Cores int `json:"cores,omitempty"`
|
||||
MemGB int `json:"memGB,omitempty"`
|
||||
GPU string `json:"gpu,omitempty"`
|
||||
Board string `json:"board,omitempty"`
|
||||
}
|
||||
|
||||
type netInfo struct {
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
MAC string `json:"mac,omitempty"`
|
||||
}
|
||||
|
||||
type kioskInfo struct {
|
||||
Uptime string `json:"uptime,omitempty"`
|
||||
Weston string `json:"weston,omitempty"`
|
||||
Chromium string `json:"chromium,omitempty"`
|
||||
}
|
||||
|
||||
type agentInfo struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type hostResponse struct {
|
||||
OS osInfo `json:"os"`
|
||||
Hardware hwInfo `json:"hardware"`
|
||||
Net netInfo `json:"net"`
|
||||
Kiosk kioskInfo `json:"kiosk"`
|
||||
Agent agentInfo `json:"agent"`
|
||||
}
|
||||
|
||||
// buildFacts is the image-generic, build-time facts file (written by the Packer
|
||||
// provisioner). Distinct from anything per-host.
|
||||
type buildFacts struct {
|
||||
Image string `json:"image"`
|
||||
Weston string `json:"weston"`
|
||||
Chromium string `json:"chromium"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", env("FLEET_HOSTAPI_ADDR", "127.0.0.1:9099"), "listen address (MUST be loopback)")
|
||||
origin := flag.String("allow-origin", env("FLEET_HOSTAPI_ALLOW_ORIGIN", "*"), "Access-Control-Allow-Origin value")
|
||||
factsPath := flag.String("facts", env("FLEET_HOSTAPI_FACTS", "/etc/fleet-hostinfo.json"), "build-time facts file")
|
||||
flag.Parse()
|
||||
|
||||
// Refuse to bind anything that isn't loopback — a hard guardrail against a
|
||||
// misconfiguration exposing host facts on the boot LAN.
|
||||
if host, _, err := net.SplitHostPort(*addr); err != nil || !isLoopback(host) {
|
||||
log.Fatalf("refusing to listen on %q: bind address must be loopback (127.0.0.1/::1)", *addr)
|
||||
}
|
||||
|
||||
facts := loadFacts(*factsPath)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/v1/host", func(w http.ResponseWriter, r *http.Request) {
|
||||
cors(w, *origin)
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
_ = enc.Encode(collect(facts))
|
||||
})
|
||||
// Liveness for the unit / debugging (no host facts).
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
log.Printf("fleet-hostapi %s listening on %s (loopback, read-only)", agentVersion, *addr)
|
||||
log.Fatal(srv.ListenAndServe())
|
||||
}
|
||||
|
||||
func cors(w http.ResponseWriter, origin string) {
|
||||
// Private Network Access: the kiosk page (boot-server origin, an insecure/
|
||||
// public context) fetching localhost is a "private network" request — the
|
||||
// preflight needs this header or Chromium blocks it. See HOST-API.md.
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Private-Network", "true")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Vary", "Origin")
|
||||
}
|
||||
|
||||
// collect gathers all host facts. Each source is best-effort: a failed read
|
||||
// leaves its field empty (the SPA renders "—" for missing fields).
|
||||
func collect(f buildFacts) hostResponse {
|
||||
var resp hostResponse
|
||||
|
||||
// os
|
||||
rel := parseKV("/etc/os-release")
|
||||
resp.OS.Name = firstNonEmpty(rel["NAME"], rel["ID"])
|
||||
resp.OS.Version = firstNonEmpty(rel["VERSION_ID"], rel["VERSION"])
|
||||
resp.OS.Kernel = kernelRelease()
|
||||
resp.OS.Image = f.Image
|
||||
|
||||
// hardware
|
||||
resp.Hardware.CPU = cpuModel()
|
||||
resp.Hardware.Cores = runtime.NumCPU()
|
||||
resp.Hardware.MemGB = memGB()
|
||||
resp.Hardware.GPU = gpuModel()
|
||||
resp.Hardware.Board = boardModel()
|
||||
|
||||
// net
|
||||
resp.Net.Hostname, _ = os.Hostname()
|
||||
resp.Net.IP, resp.Net.MAC = primaryIfaceAddr()
|
||||
|
||||
// kiosk
|
||||
resp.Kiosk.Uptime = uptimeHuman()
|
||||
resp.Kiosk.Weston = f.Weston
|
||||
resp.Kiosk.Chromium = f.Chromium
|
||||
|
||||
resp.Agent.Version = agentVersion
|
||||
return resp
|
||||
}
|
||||
|
||||
// ---- collectors -----------------------------------------------------------
|
||||
|
||||
func kernelRelease() string {
|
||||
var u syscall.Utsname
|
||||
if err := syscall.Uname(&u); err != nil {
|
||||
return ""
|
||||
}
|
||||
return charsToString(u.Release[:])
|
||||
}
|
||||
|
||||
func cpuModel() string {
|
||||
f, err := os.Open("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
// x86: "model name". arm64: "Hardware"/"Processor" if present.
|
||||
for _, key := range []string{"model name", "Hardware", "Processor"} {
|
||||
if strings.HasPrefix(line, key) {
|
||||
if _, v, ok := strings.Cut(line, ":"); ok {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = sc.Err() // best-effort read; empty result on any error
|
||||
return ""
|
||||
}
|
||||
|
||||
func memGB() int {
|
||||
for _, line := range readLines("/proc/meminfo") {
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
fields := strings.Fields(line) // MemTotal: <kB> kB
|
||||
if len(fields) >= 2 {
|
||||
if kb, err := strconv.ParseFloat(fields[1], 64); err == nil {
|
||||
return int(math.Round(kb / 1024.0 / 1024.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// boardModel: vendor + product from DMI (world-readable sysfs — NOT product_serial,
|
||||
// which is root-only and per-host; we never read it).
|
||||
func boardModel() string {
|
||||
vendor := strings.TrimSpace(readFile("/sys/class/dmi/id/sys_vendor"))
|
||||
product := strings.TrimSpace(readFile("/sys/class/dmi/id/product_name"))
|
||||
if vendor == "" && product == "" {
|
||||
vendor = strings.TrimSpace(readFile("/sys/class/dmi/id/board_vendor"))
|
||||
product = strings.TrimSpace(readFile("/sys/class/dmi/id/board_name"))
|
||||
}
|
||||
return strings.TrimSpace(strings.TrimSpace(vendor + " " + product))
|
||||
}
|
||||
|
||||
// gpuModel: best-effort from the first DRM card's PCI ids. No PCI-ID database —
|
||||
// we map the few known vendors and otherwise emit the raw ids. Often empty on a
|
||||
// headless/virtio build VM; that's fine (optional field).
|
||||
func gpuModel() string {
|
||||
for _, card := range []string{"card0", "card1"} {
|
||||
base := "/sys/class/drm/" + card + "/device/"
|
||||
vid := strings.TrimSpace(readFile(base + "vendor"))
|
||||
did := strings.TrimSpace(readFile(base + "device"))
|
||||
if vid == "" {
|
||||
continue
|
||||
}
|
||||
name := map[string]string{
|
||||
"0x8086": "Intel",
|
||||
"0x1002": "AMD",
|
||||
"0x10de": "NVIDIA",
|
||||
"0x1af4": "Red Hat (virtio-gpu)",
|
||||
"0x1234": "QEMU",
|
||||
"0x1a03": "ASPEED",
|
||||
}[strings.ToLower(vid)]
|
||||
ids := strings.TrimPrefix(strings.ToLower(vid), "0x")
|
||||
if did != "" {
|
||||
ids += ":" + strings.TrimPrefix(strings.ToLower(did), "0x")
|
||||
}
|
||||
if name != "" {
|
||||
return name + " [" + ids + "]"
|
||||
}
|
||||
return "[" + ids + "]"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// primaryIfaceAddr returns the preferred outbound IPv4 + the MAC of the
|
||||
// interface that owns it. Uses a UDP "connect" (no packet is sent) to let the
|
||||
// kernel pick the route's source address.
|
||||
func primaryIfaceAddr() (ip string, mac string) {
|
||||
c, err := net.Dial("udp", "192.0.2.1:9") // TEST-NET-1, never reachable; no traffic on UDP connect
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
defer c.Close()
|
||||
la, ok := c.LocalAddr().(*net.UDPAddr)
|
||||
if !ok || la.IP == nil {
|
||||
return "", ""
|
||||
}
|
||||
ip = la.IP.String()
|
||||
// find the interface carrying this IP -> its MAC
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ip, ""
|
||||
}
|
||||
for _, ifc := range ifaces {
|
||||
addrs, _ := ifc.Addrs()
|
||||
for _, a := range addrs {
|
||||
if n, ok := a.(*net.IPNet); ok && n.IP.Equal(la.IP) {
|
||||
return ip, ifc.HardwareAddr.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ip, ""
|
||||
}
|
||||
|
||||
// uptimeHuman formats system uptime as "3h 12m". On a kiosk appliance the system
|
||||
// boots straight into the kiosk, so system uptime ≈ kiosk-session uptime —
|
||||
// reported here to avoid coupling to (and exec'ing) systemd for the unit time.
|
||||
func uptimeHuman() string {
|
||||
var si syscall.Sysinfo_t
|
||||
if err := syscall.Sysinfo(&si); err != nil {
|
||||
return ""
|
||||
}
|
||||
d := time.Duration(si.Uptime) * time.Second
|
||||
days := int(d.Hours()) / 24
|
||||
hours := int(d.Hours()) % 24
|
||||
mins := int(d.Minutes()) % 60
|
||||
switch {
|
||||
case days > 0:
|
||||
return strconv.Itoa(days) + "d " + strconv.Itoa(hours) + "h " + strconv.Itoa(mins) + "m"
|
||||
case hours > 0:
|
||||
return strconv.Itoa(hours) + "h " + strconv.Itoa(mins) + "m"
|
||||
default:
|
||||
return strconv.Itoa(mins) + "m"
|
||||
}
|
||||
}
|
||||
|
||||
// ---- helpers --------------------------------------------------------------
|
||||
|
||||
func loadFacts(path string) buildFacts {
|
||||
var f buildFacts
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return f // file absent at runtime -> image/weston/chromium just omitted
|
||||
}
|
||||
_ = json.Unmarshal(b, &f)
|
||||
return f
|
||||
}
|
||||
|
||||
func env(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func isLoopback(host string) bool {
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func parseKV(path string) map[string]string {
|
||||
m := map[string]string{}
|
||||
for _, line := range readLines(path) {
|
||||
k, v, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
m[strings.TrimSpace(k)] = strings.Trim(strings.TrimSpace(v), `"`)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func readLines(path string) []string {
|
||||
b := readFile(path)
|
||||
if b == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(strings.TrimRight(b, "\n"), "\n")
|
||||
}
|
||||
|
||||
func readFile(path string) string {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func firstNonEmpty(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// charsToString converts a NUL-terminated Utsname field (int8 or uint8 across
|
||||
// arches) to a Go string.
|
||||
func charsToString(ca []int8) string {
|
||||
b := make([]byte, 0, len(ca))
|
||||
for _, c := range ca {
|
||||
if c == 0 {
|
||||
break
|
||||
}
|
||||
b = append(b, byte(c))
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -31,5 +31,11 @@
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"ShoppingListEnabled": false,
|
||||
"EnableMediaRouter": false
|
||||
"EnableMediaRouter": false,
|
||||
|
||||
"_comment_pna": "Private Network Access: the kiosk page (insecure http boot-server origin) fetches the workstation host-info agent at http://localhost:9099 — a cross-origin PRIVATE-network request Chromium blocks by default. The agent sends Access-Control-Allow-Private-Network: true; this is the matching browser-side allowance, scoped to the kiosk ORIGIN ONLY (not '*'). MUST list the origin of kiosk_url (default http://boot.fleet.lab). The machine talking to itself; no internet egress (see HOST-API.md).",
|
||||
"InsecurePrivateNetworkRequestsAllowedForUrls": [
|
||||
"http://boot.fleet.lab",
|
||||
"http://localhost"
|
||||
]
|
||||
}
|
||||
|
||||
+53
-22
@@ -26,6 +26,50 @@ SOCK=wayland-kiosk
|
||||
BROWSER=$(command -v chromium-browser || command -v chromium || true)
|
||||
: "${BROWSER:?chromium not found (expected chromium-browser or chromium in PATH)}"
|
||||
|
||||
# GPU vs software path. This image is FLEET-GENERIC: it boots both real hardware
|
||||
# (e.g. the 820 / Intel HD620 -> i915) AND the virtio build/test VMs (no real
|
||||
# GPU). Probe for a real KMS render node and branch — real GPU gets hardware
|
||||
# raster/compositing (CPU drops off software raster); VMs keep the software path
|
||||
# so weston + Chromium still render there. Pure runtime probe; no per-host state
|
||||
# is baked (invariant #1).
|
||||
GPU=0
|
||||
case "$(basename "$(readlink -f /sys/class/drm/card0/device/driver 2>/dev/null)" 2>/dev/null)" in
|
||||
i915|amdgpu|radeon|nouveau|xe) [ -e /dev/dri/renderD128 ] && GPU=1 ;;
|
||||
esac
|
||||
|
||||
# Lockdown flags (what the managed policy can't cover). The distro chromium
|
||||
# wrapper PREPENDS its own defaults ahead of these — including its own
|
||||
# --disable-features. Chromium does NOT union duplicate --disable-features (the
|
||||
# LAST occurrence wins), and ours is last, so we MERGE the wrapper's disables
|
||||
# (LensOverlay/Gemini/Compose/TabOrganization) into ours — otherwise they are
|
||||
# silently dropped. We deliberately pass NO --enable-features, so the wrapper's
|
||||
# VA-API enables (AcceleratedVideoDecodeLinuxGL, ...) stay in effect.
|
||||
# Phone-home suppression (background networking, component/auto update, domain
|
||||
# reliability, crash upload, pings, search-engine probe) + the comprehensive
|
||||
# lockdown live in the managed policy (/etc/chromium/policies/managed/).
|
||||
COMMON_FLAGS=(
|
||||
--kiosk --noerrdialogs --disable-infobars --incognito --no-first-run
|
||||
--disable-session-crashed-bubble --start-maximized
|
||||
--ozone-platform=wayland
|
||||
--disable-background-networking --disable-component-update
|
||||
--disable-sync --disable-domain-reliability --disable-breakpad
|
||||
--no-pings --no-default-browser-check --metrics-recording-only
|
||||
--disable-search-engine-collection
|
||||
"--disable-features=LensOverlay,OptimizationGuideOnDeviceModel,PromptAPIForGeminiNano,Compose,TabOrganization,Translate,OptimizationHints,OptimizationTargetPrediction,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions"
|
||||
--user-data-dir=/tmp/kiosk-profile --disk-cache-dir=/tmp/kiosk-cache
|
||||
)
|
||||
|
||||
if [ "$GPU" = 1 ]; then
|
||||
# Real GPU: hardware raster/compositing. HD620 (gen7 Intel) is sometimes
|
||||
# raster-blocklisted by Chromium, so override the blocklist. The GPU process
|
||||
# opens /dev/dri/renderD128 -> the kiosk user must be in group 'render' (set
|
||||
# at build, diskless.pkr.hcl). With --disable-gpu gone, the wrapper's VA-API
|
||||
# --enable-features finally engage (they need a live GPU process).
|
||||
GPU_FLAGS=(--ignore-gpu-blocklist --enable-gpu-rasterization --enable-zero-copy)
|
||||
else
|
||||
GPU_FLAGS=(--disable-gpu)
|
||||
fi
|
||||
|
||||
# Background: wait (up to ~60s) for weston's socket, then keep Chromium running.
|
||||
(
|
||||
for _ in $(seq 1 120); do
|
||||
@@ -34,30 +78,17 @@ BROWSER=$(command -v chromium-browser || command -v chromium || true)
|
||||
done
|
||||
export WAYLAND_DISPLAY="$SOCK"
|
||||
while :; do
|
||||
# --disable-* / --no-pings etc.: stop Chromium phoning home to Google
|
||||
# (background networking = variations/field-trials + GCM, component &
|
||||
# auto updates, domain reliability, crash upload, hyperlink pings). The
|
||||
# --disable-features list kills the Optimization Guide hints fetch
|
||||
# (optimizationguide-pa.googleapis.com), Translate, Media Router discovery
|
||||
# and Autofill server calls. The comprehensive lockdown (sign-in, metrics,
|
||||
# safe browsing, search suggest, ...) is the managed policy at
|
||||
# /etc/chromium/policies/managed/10-kiosk-lockdown.json; these flags cover
|
||||
# what policy can't. A fixed-URL kiosk needs none of it.
|
||||
"$BROWSER" \
|
||||
--kiosk --noerrdialogs --disable-infobars --incognito --no-first-run \
|
||||
--disable-session-crashed-bubble --start-maximized \
|
||||
--ozone-platform=wayland --disable-gpu \
|
||||
--disable-background-networking --disable-component-update \
|
||||
--disable-sync --disable-domain-reliability --disable-breakpad \
|
||||
--no-pings --no-default-browser-check --metrics-recording-only \
|
||||
--disable-search-engine-collection \
|
||||
--disable-features=Translate,OptimizationHints,OptimizationTargetPrediction,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions \
|
||||
--user-data-dir=/tmp/kiosk-profile --disk-cache-dir=/tmp/kiosk-cache \
|
||||
"$KIOSK_URL" || true
|
||||
"$BROWSER" "${COMMON_FLAGS[@]}" "${GPU_FLAGS[@]}" "$KIOSK_URL" || true
|
||||
sleep 2
|
||||
done
|
||||
) &
|
||||
|
||||
# weston as the foreground process (owns the seat; service restarts if it dies).
|
||||
# desktop-shell + explicit socket; --use-pixman = software rendering for VMs.
|
||||
exec weston --use-pixman --socket="$SOCK" --config=/etc/xdg/weston/weston.ini
|
||||
# desktop-shell + explicit socket. On real GPU use weston's GL renderer (enables
|
||||
# the zero-copy dmabuf handoff from Chromium so its frames aren't CPU-copied); on
|
||||
# VMs force --use-pixman (software) — there's no usable DRM/GL device there.
|
||||
if [ "$GPU" = 1 ]; then
|
||||
exec weston --socket="$SOCK" --config=/etc/xdg/weston/weston.ini
|
||||
else
|
||||
exec weston --use-pixman --socket="$SOCK" --config=/etc/xdg/weston/weston.ini
|
||||
fi
|
||||
|
||||
+1
-1
Submodule serving/kiosk-app updated: f0d0677950...538c7eb87d
Reference in New Issue
Block a user