Merge pull request 'plymouth-working' (#2) from plymouth-working into main

Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/2
This commit is contained in:
audiobuzz
2026-06-18 09:38:48 +00:00
17 changed files with 423 additions and 19 deletions
+11 -2
View File
@@ -49,8 +49,16 @@ env:
# per-environment DNS name (e.g. nvme.fleet.lab) resolved at boot so one UKI # per-environment DNS name (e.g. nvme.fleet.lab) resolved at boot so one UKI
# works across environments — the serving stack's dnsmasq resolves it. # works across environments — the serving stack's dnsmasq resolves it.
NVMET_TRADDR: "192.168.50.10" NVMET_TRADDR: "192.168.50.10"
# URL the fullscreen Chromium kiosk loads at boot. Empty disables the kiosk. # URL the fullscreen Chromium kiosk loads at boot. Defaults to the test page
KIOSK_URL: "https://www.reddit.com" # served by the boot HTTP server (serving/http/kiosk) at /kiosk/; boot.fleet.lab
# resolves to the serving host via the stack's dnsmasq. Empty disables the kiosk.
KIOSK_URL: "http://boot.fleet.lab/kiosk/"
# Plymouth boot splash. "false" => verbose console boot (rhgb quiet NOT baked;
# plymouth.enable=0 instead) — kept OFF in CI while the network-manager port is
# being bench-verified, so the boot chain stays debuggable. Flip to "true" to
# ship the graphical splash once boot is trusted. (Plymouth is in the image
# either way; this only flips the cmdline.)
BOOT_SPLASH: "true"
# =========================================================================== # ===========================================================================
# TEMPORARY: 802.1X + MACsec are DISABLED for this build (rd.dot1x=0) so the # TEMPORARY: 802.1X + MACsec are DISABLED for this build (rd.dot1x=0) so the
# UKI / rootfs / NVMe-oF boot path can be validated on an OPEN switch port. # UKI / rootfs / NVMe-oF boot path can be validated on an OPEN switch port.
@@ -178,4 +186,5 @@ jobs:
-var nvmet_traddr="${{ env.NVMET_TRADDR }}" \ -var nvmet_traddr="${{ env.NVMET_TRADDR }}" \
-var ssh_authorized_key="${{ secrets.SSH_AUTHORIZED_KEY }}" \ -var ssh_authorized_key="${{ secrets.SSH_AUTHORIZED_KEY }}" \
-var kiosk_url="${{ env.KIOSK_URL }}" \ -var kiosk_url="${{ env.KIOSK_URL }}" \
-var boot_splash=${{ env.BOOT_SPLASH }} \
. .
+31 -3
View File
@@ -28,6 +28,9 @@ files/etc/dot1x/ Supplicant config + RADIUS CA (placeho
files/etc/dracut.conf.d/90-diskless.conf no-hostonly + drivers + modules files/etc/dracut.conf.d/90-diskless.conf no-hostonly + drivers + modules
(NM unmanaged split is now runtime: (NM unmanaged split is now runtime:
95dot1x-macsec/nm-unmanaged.sh, secure-mode only) 95dot1x-macsec/nm-unmanaged.sh, secure-mode only)
files/plymouth/themes/fleet/ Custom boot splash (scripted security
checklist; driven by hook status tokens)
files/kiosk/ weston+Chromium kiosk + chromium lockdown policy
scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline
scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image
serving/ Lab serving stack: dnsmasq + nginx + SPDK serving/ Lab serving stack: dnsmasq + nginx + SPDK
@@ -149,6 +152,13 @@ instead of working around it.
build; the supplicant config carries both options. build; the supplicant config carries both options.
- erofs module availability on the target RHEL kernel (fallback: - erofs module availability on the target RHEL kernel (fallback:
journal-less ext4 in `extract-artifacts.sh`). journal-less ext4 in `extract-artifacts.sh`).
- `fleet` Plymouth theme: confirm (a) the script plugin package actually
installed (`/usr/lib64/plymouth/script.so` — else the build fell back to
`spinner`, logged as a WARNING), (b) the scripted theme + a font got bundled
into the initramfs (`lsinitrd | grep -E 'fleet.script|script.so'`), and (c) it
renders (text via `Image.Text` needs the label plugin + a font in the early
initramfs). If blank, the splash silently no-ops but boot continues; ESC shows
the log. Untested in this workspace (needs a display).
- `systemd.verity_root_options=hash-offset` is **NOT supported** by Rocky 9.8 - `systemd.verity_root_options=hash-offset` is **NOT supported** by Rocky 9.8
systemd-veritysetup (verified: "unknown option hash-offset=" → "Failed to load systemd-veritysetup (verified: "unknown option hash-offset=" → "Failed to load
verity superblock"). Resolved by the documented fallback — data and hash are verity superblock"). Resolved by the documented fallback — data and hash are
@@ -161,9 +171,10 @@ instead of working around it.
`network-manager` module hands its initramfs connections to the real-root NM `network-manager` module hands its initramfs connections to the real-root NM
(state under `/run/NetworkManager`), which renews the lease. No more (state under `/run/NetworkManager`), which renews the lease. No more
`dhcp-client`/`dhclient` (deprecated). The hook drives macsec0 DHCP via `nmcli` `dhcp-client`/`dhclient` (deprecated). The hook drives macsec0 DHCP via `nmcli`
(not `ifup`). See invariant on the managed/unmanaged split. BENCH-VERIFY: lease (not `ifup`). See invariant on the managed/unmanaged split. Test path
actually renews past expiry (test path); macsec0 managed + `en*` unmanaged with VERIFIED on the bench 2026-06-17 (lease renews past expiry; host stays up).
MKA surviving switch-root (secure path). STILL BENCH-VERIFY when `disable_dot1x=false`: macsec0 managed + `en*`
unmanaged with MKA surviving switch-root (secure path).
- Writable root overlay is `systemd.volatile=overlay`, **not** `rootovl`: - Writable root overlay is `systemd.volatile=overlay`, **not** `rootovl`:
RHEL/Rocky dracut ships no `overlayfs`/`rootovl` module. Needs RHEL/Rocky dracut ships no `overlayfs`/`rootovl` module. Needs
`systemd-volatile-root.service` in the initramfs (bundled by recent dracut; `systemd-volatile-root.service` in the initramfs (bundled by recent dracut;
@@ -194,6 +205,23 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress.
stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving
stack lives in `serving/` (docker compose). Re-enable security by dropping the stack lives in `serving/` (docker compose). Re-enable security by dropping the
var / setting `DISABLE_DOT1X: "false"` in the CI workflow. var / setting `DISABLE_DOT1X: "false"` in the CI workflow.
- **Boot splash toggle**: `boot_splash=true` (default) bakes `rhgb quiet` for the
Plymouth splash; `false` bakes `plymouth.enable=0` for a verbose console boot —
keep OFF while debugging the boot chain. Plymouth is always in the image +
initramfs (the `plymouth` dracut module, which pulls `drm`); only the cmdline
changes. Press ESC at boot to see details behind a splash. ks.cfg no longer
excludes `plymouth`.
- **Splash theme = custom `fleet`** (`files/plymouth/themes/fleet/`, script
plugin, asset-free — drawn with `Image.Text`). It renders a security-handshake
checklist (TPM → 802.1X → MACsec → Network → NVMe-oF) driven by best-effort
`plymouth update --status=<token>` calls from the dracut hooks
(`dot1x-macsec-up.sh`, `nvmf-connect.sh`). Tokens: `mode-test` (marks
auth/MACsec skipped), `tpm|dot1x|macsec|net|nvme`-`active|done`. **Rules:** the
emits are non-fatal (`plymouth … || true`) and must never reorder the
load-bearing boot sequence; show only STATES, never per-host data (invariant
#1). The provisioner installs the script plugin best-effort and **falls back to
the stock `spinner` theme** if `script.so` is absent, so a plugin-package-name
change can't break the build.
## Testing changes ## Testing changes
+40 -4
View File
@@ -120,8 +120,14 @@ variable "enable_kiosk" {
variable "kiosk_url" { variable "kiosk_url" {
type = string type = string
description = "URL the fullscreen Chromium kiosk loads at boot (cage Wayland session on tty1, baked into the immutable root). Empty disables the kiosk install entirely. A runtime config-agent could override it later via /etc/kiosk/kiosk.env." description = "URL the fullscreen Chromium kiosk loads at boot (weston session on tty1, baked into the immutable root). Defaults to the self-contained test page served by the lab boot HTTP server at /kiosk/ (boot.fleet.lab resolves to the serving host via the stack's dnsmasq) — validates the boot end-to-end without external connectivity. Empty disables the kiosk install entirely. A runtime config-agent could override it later via /etc/kiosk/kiosk.env."
default = "https://www.reddit.com" default = "http://boot.fleet.lab/kiosk/"
}
variable "boot_splash" {
type = bool
description = "true (default): bake 'rhgb quiet' into the UKI cmdline so Plymouth shows a graphical boot splash (covers the network-auth + NVMe/TCP + verity phase). false: bake 'plymouth.enable=0' for a fully verbose console boot — use while debugging the boot chain. Plymouth itself is always in the image; this only flips the cmdline. (The cmdline is immutable once signed, so this is a build-time choice.)"
default = true
} }
variable "sb_key" { variable "sb_key" {
@@ -206,6 +212,13 @@ build {
destination = "/tmp/" 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" {
source = "files/plymouth"
destination = "/tmp/"
}
# --- 2. Packages, configs, diskless adaptations -------------------------- # --- 2. Packages, configs, diskless adaptations --------------------------
provisioner "shell" { provisioner "shell" {
inline = [ inline = [
@@ -222,6 +235,22 @@ build {
# dracut warns and silently falls back to gzip (a larger initramfs). # dracut warns and silently falls back to gzip (a larger initramfs).
"dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network NetworkManager zstd openssl", "dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network NetworkManager zstd openssl",
# Plymouth boot splash. The custom scripted 'fleet' theme renders the
# security-handshake checklist driven by the dracut hooks
# (`plymouth update --status=...`). plymouth-plugin-label gives Image.Text
# its font rendering; dejavu fonts back it. The script-plugin package name
# has varied across releases, so install it best-effort and FALL BACK to the
# stock 'spinner' theme if script.so is absent — never break the build over
# the theme. Set the default theme BEFORE the dracut build below so the
# (--no-hostonly) initramfs bundles the theme + its plugin + a font; no -R
# (we rebuild the initramfs explicitly). Whether the splash shows at boot is
# gated by the cmdline (boot_splash var), not by installing this.
"dnf -y install plymouth plymouth-scripts plymouth-plugin-label dejavu-sans-fonts",
"dnf -y install plymouth-plugin-script 2>/dev/null || dnf -y install plymouth-theme-script 2>/dev/null || true",
"cp -a /tmp/plymouth/themes/fleet /usr/share/plymouth/themes/fleet",
"if [ -e /usr/lib64/plymouth/script.so ]; then plymouth-set-default-theme fleet; echo '==> plymouth theme: fleet (scripted security splash)'; else dnf -y install plymouth-theme-spinner && plymouth-set-default-theme spinner; echo 'WARNING: plymouth script plugin (script.so) missing — using spinner; fleet security splash unavailable' >&2; fi",
"rm -rf /tmp/plymouth",
# Stage etc files into place # Stage etc files into place
"install -D -m0644 /tmp/etc-stage/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem", "install -D -m0644 /tmp/etc-stage/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem",
"install -D -m0644 /tmp/etc-stage/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf", "install -D -m0644 /tmp/etc-stage/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf",
@@ -266,6 +295,8 @@ build {
# a dracut hook). # a dracut hook).
"grep -q 'nm-unmanaged.sh' /tmp/lsinitrd.txt || { echo 'ERROR: nm-unmanaged.sh hook missing from initramfs' >&2; exit 1; }", "grep -q 'nm-unmanaged.sh' /tmp/lsinitrd.txt || { echo 'ERROR: nm-unmanaged.sh hook missing from initramfs' >&2; exit 1; }",
"grep -q 'nvmf-connect.service' /tmp/lsinitrd.txt || { echo 'ERROR: nvmf-connect.service missing from initramfs' >&2; exit 1; }", "grep -q 'nvmf-connect.service' /tmp/lsinitrd.txt || { echo 'ERROR: nvmf-connect.service missing from initramfs' >&2; exit 1; }",
# Plymouth must be in the initramfs to splash during the early boot phase.
"grep -q 'plymouthd' /tmp/lsinitrd.txt || { echo 'ERROR: plymouth missing from initramfs (plymouth dracut module not installed)' >&2; exit 1; }",
# ---- Build hygiene --------------------------------------------------- # ---- Build hygiene ---------------------------------------------------
"dnf clean all", "dnf clean all",
@@ -308,12 +339,17 @@ build {
"install -D -m0755 /tmp/kiosk/kiosk-session /usr/local/bin/kiosk-session", "install -D -m0755 /tmp/kiosk/kiosk-session /usr/local/bin/kiosk-session",
"install -D -m0644 /tmp/kiosk/kiosk.service /etc/systemd/system/kiosk.service", "install -D -m0644 /tmp/kiosk/kiosk.service /etc/systemd/system/kiosk.service",
"install -D -m0644 /tmp/kiosk/weston.ini /etc/xdg/weston/weston.ini", "install -D -m0644 /tmp/kiosk/weston.ini /etc/xdg/weston/weston.ini",
# Chromium managed policy: stops the browser phoning home to Google
# (sign-in, variations, optimization-guide, component/auto update, metrics,
# safe browsing). EPEL chromium reads /etc/chromium/policies/managed/*.json.
# Pairs with the hardening flags in kiosk-session.
"install -D -m0644 /tmp/kiosk/chromium-kiosk-policy.json /etc/chromium/policies/managed/10-kiosk-lockdown.json",
# URL from the var (weston renderer is set via --use-pixman in the unit). # URL from the var (weston renderer is set via --use-pixman in the unit).
"install -d -m0755 /etc/kiosk", "install -d -m0755 /etc/kiosk",
"printf 'KIOSK_URL=%s\\n' \"$KIOSK_URL\" > /etc/kiosk/kiosk.env", "printf 'KIOSK_URL=%s\\n' \"$KIOSK_URL\" > /etc/kiosk/kiosk.env",
"chmod 0644 /etc/kiosk/kiosk.env", "chmod 0644 /etc/kiosk/kiosk.env",
# Label the new files so they survive into the enforcing erofs. # Label the new files so they survive into the enforcing erofs.
"restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/xdg/weston /etc/kiosk", "restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/xdg/weston /etc/kiosk /etc/chromium",
# Boot graphical and start the kiosk (replaces the tty1 getty). # Boot graphical and start the kiosk (replaces the tty1 getty).
"systemctl enable kiosk.service", "systemctl enable kiosk.service",
"systemctl set-default graphical.target", "systemctl set-default graphical.target",
@@ -402,7 +438,7 @@ build {
# home dirs) — we dropped mkfs.erofs --all-root, which used to mask this by # home dirs) — we dropped mkfs.erofs --all-root, which used to mask this by
# flattening everything to root:root. sudo is a no-op if already root, and # flattening everything to root:root. sudo is a no-op if already root, and
# the runner has passwordless sudo (cf. the `sudo apt-get` install step). # the runner has passwordless sudo (cf. the `sudo apt-get` install step).
"sudo bash -c \"NVMET_TRADDR='${var.nvmet_traddr}' bash scripts/extract-artifacts.sh output-qemu/diskless-build.qcow2 ${var.artifact_dir}/${var.image_version} ${var.disable_dot1x}\"", "sudo bash -c \"NVMET_TRADDR='${var.nvmet_traddr}' BOOT_SPLASH='${var.boot_splash}' bash scripts/extract-artifacts.sh output-qemu/diskless-build.qcow2 ${var.artifact_dir}/${var.image_version} ${var.disable_dot1x}\"",
# Hand the OUTPUT FILES back to the build user (their in-erofs ownership is # Hand the OUTPUT FILES back to the build user (their in-erofs ownership is
# already baked correctly) so build-oci.sh can write the UKI and push with # already baked correctly) so build-oci.sh can write the UKI and push with
# the build user's `buildah login`. # the build user's `buildah login`.
+27 -9
View File
@@ -5,13 +5,18 @@ Persistent backlog, kept in sync with the working session task tracker. Status:
--- ---
## [~] 1. Port the dracut module off `network-legacy` → `network-manager` ## [x] 1. Port the dracut module off `network-legacy` → `network-manager`
**IMPLEMENTED 2026-06-17 — awaiting boot-bench verification.** Fixes the **DONE — test path VERIFIED ON THE BENCH 2026-06-17** (host stays up past the
confirmed lease-expiry outage: network-legacy's one-shot DHCP lease was never DHCP lease time; no more lease-expiry outage). Fixed the confirmed outage:
renewed post-switch-root, so the address dropped at lease expiry → NVMe/TCP root network-legacy's one-shot DHCP lease was never renewed post-switch-root, so the
died ("keep alive timeout" / "no route to host"). NM hands its initramfs address dropped at lease expiry → NVMe/TCP root died ("keep alive timeout" /
connections to the real-root NM (`/run/NetworkManager` state), which renews. "no route to host"). NM hands its initramfs connections to the real-root NM
(`/run/NetworkManager` state), which renews.
REMAINING: the **secure path** (macsec0 DHCP via `nmcli`, physical NIC unmanaged
via `nm-unmanaged.sh`) is only exercised once `disable_dot1x=false` — verify on
the swtpm + hostapd + FreeRADIUS bench when re-enabling security (see #2/cleanup).
Done: Done:
- `90-diskless.conf` `add_dracutmodules`: `network-legacy` → `network-manager`. - `90-diskless.conf` `add_dracutmodules`: `network-legacy` → `network-manager`.
@@ -125,8 +130,11 @@ Path to getting here (don't re-discover):
smaller image. **Re-enable with `-var enable_kiosk=true` when returning to smaller image. **Re-enable with `-var enable_kiosk=true` when returning to
kiosk work** — CI does not pass the var, so CI currently builds lean (no kiosk work** — CI does not pass the var, so CI currently builds lean (no
kiosk) too; add `-var enable_kiosk=true` to the Gitea workflow to ship it. kiosk) too; add `-var enable_kiosk=true` to the Gitea workflow to ship it.
- `kiosk_url` Packer var (default `https://www.reddit.com`; empty disables the - `kiosk_url` Packer var (default `http://boot.fleet.lab/kiosk/` — the
whole install). Wired in CI as `KIOSK_URL` env. self-contained test page served by the boot HTTP server at
`serving/http/kiosk/`, via the `/kiosk/` nginx location; validates a boot
without external connectivity. Empty disables the whole install). Wired in CI
as `KIOSK_URL` env.
- Provisioner enables EPEL, installs `weston chromium mesa-dri-drivers - Provisioner enables EPEL, installs `weston chromium mesa-dri-drivers
mesa-libEGL mesa-libgbm dejavu-sans-fonts`, creates a passwordless mesa-libEGL mesa-libgbm dejavu-sans-fonts`, creates a passwordless
unprivileged `kiosk` user (video/input groups). unprivileged `kiosk` user (video/input groups).
@@ -147,6 +155,15 @@ Path to getting here (don't re-discover):
avc`; the kiosk user is unconfined so likely none — but do NOT set permissive avc`; the kiosk user is unconfined so likely none — but do NOT set permissive
if they appear; add a policy module). Admin SSH (the key user) still works for if they appear; add a policy module). Admin SSH (the key user) still works for
debug even if the kiosk fails (it lands at graphical.target either way). debug even if the kiosk fails (it lands at graphical.target either way).
- **Phone-home lockdown (done 2026-06-17):** Chromium was dialing Google
(accounts/variations/optimization-guide/component+auto update/safe browsing —
seen in dnsmasq logs). Stopped via a managed policy
(`files/kiosk/chromium-kiosk-policy.json` → `/etc/chromium/policies/managed/`)
plus hardening flags in `kiosk-session` (`--disable-background-networking`,
`--disable-component-update`, `--disable-features=OptimizationHints,...`, etc.).
Verify with the dnsmasq query log after a rebuild — only `boot.fleet.lab`
(kiosk page) + chrony NTP should remain. NTP is the OS (chrony), not Chromium;
optionally point it at the serving host to fully contain egress.
- **Image size:** Chromium+Mesa add hundreds of MB to the *uncompressed* erofs — - **Image size:** Chromium+Mesa add hundreds of MB to the *uncompressed* erofs —
feeds the "shrink the rootfs" concern (and #7); reconsider whether a lighter feeds the "shrink the rootfs" concern (and #7); reconsider whether a lighter
browser/render path is viable. browser/render path is viable.
@@ -156,7 +173,8 @@ Path to getting here (don't re-discover):
Make the diskless workstation boot straight into a locked-down kiosk: a Make the diskless workstation boot straight into a locked-down kiosk: a
fullscreen Chromium showing a configured site (initial target: Reddit). All fullscreen Chromium showing a configured site (default: the boot HTTP server's
`/kiosk/` test page). All
baked at **build time** into the root tree (immutable erofs/verity root; runtime baked at **build time** into the root tree (immutable erofs/verity root; runtime
is the tmpfs overlay), fleet-generic (no per-host state). is the tmpfs overlay), fleet-generic (no per-host state).
@@ -15,6 +15,11 @@ STAMP=/run/dot1x/.done
[ -e "$STAMP" ] && exit 0 [ -e "$STAMP" ] && exit 0
mkdir -p /run/dot1x /run/wpa_supplicant mkdir -p /run/dot1x /run/wpa_supplicant
# Best-effort boot-splash status for the 'fleet' Plymouth theme. NEVER fatal and
# NEVER reorders anything — a splash call must not perturb the load-bearing
# auth -> macsec -> DHCP -> NVMe sequence below. No-op if plymouth isn't running.
ply() { plymouth update --status="$1" 2>/dev/null || true; }
# --- knobs (override on the kernel cmdline) --------------------------------- # --- knobs (override on the kernel cmdline) ---------------------------------
NIC=$(getarg rd.dot1x.if=) NIC=$(getarg rd.dot1x.if=)
if [ -z "$NIC" ]; then if [ -z "$NIC" ]; then
@@ -42,6 +47,7 @@ info "dot1x-macsec: using interface $NIC, NV index $NVINDEX"
# networks only. Remove rd.dot1x=0 from the cmdline to restore full security. # networks only. Remove rd.dot1x=0 from the cmdline to restore full security.
if [ "$(getarg rd.dot1x=)" = "0" ]; then if [ "$(getarg rd.dot1x=)" = "0" ]; then
warn "dot1x-macsec: 802.1X/MACsec DISABLED (rd.dot1x=0) — TEST MODE, no link security" warn "dot1x-macsec: 802.1X/MACsec DISABLED (rd.dot1x=0) — TEST MODE, no link security"
ply mode-test # splash: mark TPM/802.1X/MACsec rows as skipped (not failed)
# Networking in test mode is owned ENTIRELY by `ip=dhcp` on the cmdline: # Networking in test mode is owned ENTIRELY by `ip=dhcp` on the cmdline:
# dracut's network module brings the NIC up, gets a lease and writes # dracut's network module brings the NIC up, gets a lease and writes
# resolv.conf. Do NOT `ifup`/`ip link set up` the NIC here — a second # resolv.conf. Do NOT `ifup`/`ip link set up` the NIC here — a second
@@ -70,6 +76,7 @@ if [ "$(getarg rd.dot1x=)" = "0" ]; then
fi fi
# --- 1. client certificate out of TPM NV ------------------------------------ # --- 1. client certificate out of TPM NV ------------------------------------
ply tpm-active
if [ ! -s /run/dot1x/client.pem ]; then if [ ! -s /run/dot1x/client.pem ]; then
if ! tpm2_nvread -T device:/dev/tpmrm0 "$NVINDEX" -o /run/dot1x/client.der; then if ! tpm2_nvread -T device:/dev/tpmrm0 "$NVINDEX" -o /run/dot1x/client.der; then
warn "dot1x-macsec: tpm2_nvread failed — cannot authenticate" warn "dot1x-macsec: tpm2_nvread failed — cannot authenticate"
@@ -77,6 +84,7 @@ if [ ! -s /run/dot1x/client.pem ]; then
fi fi
openssl x509 -inform DER -in /run/dot1x/client.der -out /run/dot1x/client.pem openssl x509 -inform DER -in /run/dot1x/client.der -out /run/dot1x/client.pem
fi fi
ply tpm-done
# --- 2. supplicant as a root storage daemon --------------------------------- # --- 2. supplicant as a root storage daemon ---------------------------------
ip link set "$NIC" up ip link set "$NIC" up
@@ -88,6 +96,7 @@ if [ ! -e /run/dot1x/wpa.pid ]; then
-c /etc/dot1x/wpa_supplicant-dot1x.conf \ -c /etc/dot1x/wpa_supplicant-dot1x.conf \
-P /run/dot1x/wpa.pid" & -P /run/dot1x/wpa.pid" &
fi fi
ply dot1x-active
# --- 3. wait for authentication + macsec0 ----------------------------------- # --- 3. wait for authentication + macsec0 -----------------------------------
i=0 i=0
@@ -104,9 +113,13 @@ if [ "$i" -ge "$TIMEOUT" ]; then
exit 1 exit 1
fi fi
info "dot1x-macsec: port authorized, macsec0 up after ${i}s" info "dot1x-macsec: port authorized, macsec0 up after ${i}s"
ply dot1x-done
ply macsec-active
# --- 4. address the secure interface; netroot rides on macsec0 -------------- # --- 4. address the secure interface; netroot rides on macsec0 --------------
ip link set macsec0 up ip link set macsec0 up
ply macsec-done
ply net-active
# DHCP on macsec0 via NetworkManager (NOT the old network-legacy `ifup`): NM owns # DHCP on macsec0 via NetworkManager (NOT the old network-legacy `ifup`): NM owns
# macsec0, so it RENEWS the lease in the running system after switch-root — the # macsec0, so it RENEWS the lease in the running system after switch-root — the
# whole point of the network-manager port. macsec0 was created externally by # whole point of the network-manager port. macsec0 was created externally by
@@ -127,6 +140,7 @@ if [ "$i" -ge "$TIMEOUT" ]; then
exit 1 exit 1
fi fi
info "dot1x-macsec: macsec0 addressed via NetworkManager (DHCP)" info "dot1x-macsec: macsec0 addressed via NetworkManager (DHCP)"
ply net-done
# --- 5. derive per-host NVMe identity from the TPM certificate -------------- # --- 5. derive per-host NVMe identity from the TPM certificate --------------
# Keeps the image fleet-generic: the host NQN is a function of the machine # Keeps the image fleet-generic: the host NQN is a function of the machine
@@ -29,6 +29,12 @@ _trsvcid=${_rest##*,}
# nvme_tcp is usually auto-loaded; ensure it so /dev/nvme-fabrics exists. # nvme_tcp is usually auto-loaded; ensure it so /dev/nvme-fabrics exists.
modprobe nvme_tcp 2>/dev/null modprobe nvme_tcp 2>/dev/null
# Best-effort boot-splash status for the 'fleet' Plymouth theme (never fatal). By
# the time this runs the ip=dhcp link is up, so mark Network done and NVMe active.
ply() { plymouth update --status="$1" 2>/dev/null || true; }
ply net-done
ply nvme-active
# Keep-alive / reconnect tuning for the ROOT device. These persist across # Keep-alive / reconnect tuning for the ROOT device. These persist across
# switch-root (the in-kernel nvme-tcp controller and its opts survive). # switch-root (the in-kernel nvme-tcp controller and its opts survive).
# -k 30 keep-alive timeout (s). The 5s default means a KA every 2.5s; under # -k 30 keep-alive timeout (s). The 5s default means a KA every 2.5s; under
@@ -53,6 +59,7 @@ while [ "$_i" -lt 60 ]; do
if [ -b "$_ns" ]; then if [ -b "$_ns" ]; then
mkdir -p /run/dot1x mkdir -p /run/dot1x
: > /run/dot1x/.nvmf-connected : > /run/dot1x/.nvmf-connected
ply nvme-done
echo "nvmf-connect: connected ${_trtype}/${_traddr}/${_trsvcid} ($_ns)" echo "nvmf-connect: connected ${_trtype}/${_traddr}/${_trsvcid} ($_ns)"
exit 0 exit 0
fi fi
+8
View File
@@ -15,6 +15,14 @@ hostonly="no"
# lease was never renewed post-switch-root -> the address was dropped at lease # lease was never renewed post-switch-root -> the address was dropped at lease
# expiry -> NVMe/TCP root died ("keep alive timeout" / "no route to host"). # expiry -> NVMe/TCP root died ("keep alive timeout" / "no route to host").
add_dracutmodules+=" dot1x-macsec network-manager nvmf systemd-veritysetup " add_dracutmodules+=" dot1x-macsec network-manager nvmf systemd-veritysetup "
# plymouth: graphical boot splash, shown from early initramfs through the long
# network-auth + NVMe/TCP + verity phase until the kiosk/login takes over. The
# plymouth dracut module depends on 'drm', which in --no-hostonly mode pulls a
# broad set of DRM/KMS drivers (incl. the VM's bochs/virtio-gpu); if a specific
# fleet GPU's splash doesn't render, add its drm kmod to add_drivers like the
# NICs. Whether the splash actually displays is gated by the cmdline (rhgb quiet
# vs plymouth.enable=0), set from the boot_splash var in extract-artifacts.sh.
add_dracutmodules+=" plymouth "
filesystems+=" erofs overlay " filesystems+=" erofs overlay "
add_drivers+=" macsec nvme-tcp nvme-fabrics dm-verity @NIC_DRIVERS@ " add_drivers+=" macsec nvme-tcp nvme-fabrics dm-verity @NIC_DRIVERS@ "
install_items+=" /etc/dot1x/radius-ca.pem /etc/dot1x/wpa_supplicant-dot1x.conf " install_items+=" /etc/dot1x/radius-ca.pem /etc/dot1x/wpa_supplicant-dot1x.conf "
+34
View File
@@ -0,0 +1,34 @@
{
"_comment": "Chromium managed policy for the diskless kiosk. Installed to /etc/chromium/policies/managed/. Stops Chromium phoning home to *.google.com / *.googleapis.com (sign-in, variations, optimization guide, component/auto updates, metrics, safe browsing) — none of which a fixed-URL kiosk needs. Pairs with the hardening flags in kiosk-session. Unknown keys are ignored by Chromium, so this is forward/backward safe.",
"MetricsReportingEnabled": false,
"UrlKeyedAnonymizedDataCollectionEnabled": false,
"DeviceMetricsReportingEnabled": false,
"BrowserSignin": 0,
"SyncDisabled": true,
"SafeBrowsingProtectionLevel": 0,
"SafeBrowsingExtendedReportingEnabled": false,
"ComponentUpdatesEnabled": false,
"ChromeVariations": 2,
"BrowserNetworkTimeQueriesEnabled": false,
"SearchSuggestEnabled": false,
"DefaultSearchProviderEnabled": false,
"SpellCheckServiceEnabled": false,
"TranslateEnabled": false,
"NetworkPredictionOptions": 2,
"BackgroundModeEnabled": false,
"PromotionalTabsEnabled": false,
"WelcomePageOnOSUpgradeEnabled": false,
"DefaultBrowserSettingEnabled": false,
"AutofillAddressEnabled": false,
"AutofillCreditCardEnabled": false,
"PasswordManagerEnabled": false,
"ShoppingListEnabled": false,
"EnableMediaRouter": false
}
+14
View File
@@ -34,10 +34,24 @@ BROWSER=$(command -v chromium-browser || command -v chromium || true)
done done
export WAYLAND_DISPLAY="$SOCK" export WAYLAND_DISPLAY="$SOCK"
while :; do 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" \ "$BROWSER" \
--kiosk --noerrdialogs --disable-infobars --incognito --no-first-run \ --kiosk --noerrdialogs --disable-infobars --incognito --no-first-run \
--disable-session-crashed-bubble --start-maximized \ --disable-session-crashed-bubble --start-maximized \
--ozone-platform=wayland --disable-gpu \ --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 \ --user-data-dir=/tmp/kiosk-profile --disk-cache-dir=/tmp/kiosk-cache \
"$KIOSK_URL" || true "$KIOSK_URL" || true
sleep 2 sleep 2
@@ -0,0 +1,8 @@
[Plymouth Theme]
Name=Fleet
Description=Cyber sci-fi secure diskless bring-up splash (TPM / 802.1X / MACsec / NVMe-oF). Driven by `plymouth update --status=` tokens from the dracut hooks.
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/fleet
ScriptFile=/usr/share/plymouth/themes/fleet/fleet.script
+109
View File
@@ -0,0 +1,109 @@
# Fleet — cyber sci-fi secure-bring-up splash (Plymouth "script" plugin).
#
# Asset-free on purpose: everything is drawn with Image.Text, so the repo carries
# no binary images. The dracut hooks drive it with `plymouth update --status=<tok>`
# (see dot1x-macsec-up.sh / nvmf-connect.sh); this script maps each token to a
# security-handshake checklist and pulses the in-progress step. Tokens are
# best-effort and order-independent — anything not reported just stays "pending",
# and in rd.dot1x=0 test mode the auth/MACsec rows are marked "skipped" (never
# "failed"). No per-host data is ever shown (only states), per the no-state rule.
# ---- background (dark teal -> black) ----
Window.SetBackgroundTopColor(0.00, 0.05, 0.07);
Window.SetBackgroundBottomColor(0.00, 0.00, 0.00);
sw = Window.GetWidth();
sh = Window.GetHeight();
# ---- title / subtitle ----
title_img = Image.Text("F L E E T", 0.0, 1.0, 0.85, 1, "Sans Bold 36");
title = Sprite(title_img);
title.SetX(sw / 2 - title_img.GetWidth() / 2);
title.SetY(sh / 2 - 150);
sub_img = Image.Text("secure diskless bring-up", 0.30, 0.65, 0.65, 1, "Sans 14");
sub = Sprite(sub_img);
sub.SetX(sw / 2 - sub_img.GetWidth() / 2);
sub.SetY(sh / 2 - 102);
# ---- handshake stages ----
# state per stage: 0 pending, 1 active, 2 done, 3 skipped
n = 5;
label[0] = "TPM identity";
label[1] = "802.1X EAP-TLS";
label[2] = "MACsec link";
label[3] = "Network (DHCP)";
label[4] = "NVMe-oF root";
col_x = sw / 2 - 150;
row_y = sh / 2 - 36;
row_h = 34;
fun draw(i) {
glyph = "[ ]"; r = 0.22; g = 0.42; b = 0.42; # pending (dim teal)
if (state[i] == 1) { glyph = "[>>]"; r = 1.00; g = 0.72; b = 0.10; } # active (amber)
if (state[i] == 2) { glyph = "[OK]"; r = 0.10; g = 1.00; b = 0.50; } # done (neon green)
if (state[i] == 3) { glyph = "[--]"; r = 0.30; g = 0.30; b = 0.36; } # skipped (grey)
img = Image.Text(glyph + " " + label[i], r, g, b, 1, "Sans 16");
row[i].SetImage(img);
row[i].SetX(col_x);
row[i].SetY(row_y + i * row_h);
row[i].SetOpacity(1.0);
}
i = 0;
while (i < n) {
state[i] = 0;
row[i] = Sprite();
draw(i);
i = i + 1;
}
# ---- footer ----
foot_img = Image.Text("erofs + dm-verity • NVMe/TCP • no per-host state",
0.25, 0.45, 0.45, 1, "Sans 11");
foot = Sprite(foot_img);
foot.SetX(sw / 2 - foot_img.GetWidth() / 2);
foot.SetY(sh / 2 + 150);
# ---- status tokens from the boot hooks ----
fun set(i, s) { state[i] = s; draw(i); }
fun on_status(t) {
if (t == "mode-test") { set(0, 3); set(1, 3); set(2, 3); }
if (t == "tpm-active") set(0, 1);
if (t == "tpm-done") set(0, 2);
if (t == "dot1x-active") set(1, 1);
if (t == "dot1x-done") set(1, 2);
if (t == "macsec-active") set(2, 1);
if (t == "macsec-done") set(2, 2);
if (t == "net-active") set(3, 1);
if (t == "net-done") set(3, 2);
if (t == "nvme-active") set(4, 1);
if (t == "nvme-done") set(4, 2);
}
Plymouth.SetUpdateStatusFunction(on_status);
# ---- optional free-text line (e.g. warnings via `plymouth message`) ----
msg = Sprite();
fun on_message(text) {
img = Image.Text(text, 0.55, 0.80, 0.80, 1, "Sans 12");
msg.SetImage(img);
msg.SetX(sw / 2 - img.GetWidth() / 2);
msg.SetY(sh / 2 + 182);
}
Plymouth.SetMessageFunction(on_message);
# ---- pulse the in-progress row ----
tick = 0;
fun on_refresh() {
tick = tick + 1;
o = 1.0;
if (tick % 24 < 12) o = 0.45;
i = 0;
while (i < n) {
if (state[i] == 1) row[i].SetOpacity(o);
i = i + 1;
}
}
Plymouth.SetRefreshFunction(on_refresh);
-1
View File
@@ -32,7 +32,6 @@ reboot
kernel kernel
openssh-server openssh-server
chrony chrony
-plymouth
-firewalld -firewalld
%end %end
+15
View File
@@ -23,6 +23,10 @@ DISABLE_DOT1X=${3:-false}
# environments. Defaults to the literal placeholder so build-oci.sh's warning # environments. Defaults to the literal placeholder so build-oci.sh's warning
# fires if it was never set. # fires if it was never set.
NVMET_TRADDR=${NVMET_TRADDR:-NVMET_TRADDR} NVMET_TRADDR=${NVMET_TRADDR:-NVMET_TRADDR}
# Plymouth boot splash toggle (from the boot_splash Packer var). "true" bakes
# "rhgb quiet" so Plymouth shows the graphical splash; anything else bakes
# "plymouth.enable=0" for a fully verbose console boot (boot-chain debugging).
BOOT_SPLASH=${BOOT_SPLASH:-true}
# Stable namespace UUIDs. Devices are referenced by-id # Stable namespace UUIDs. Devices are referenced by-id
# (/dev/disk/by-id/nvme-uuid.<uuid>) rather than /dev/nvmeNnM because NVMe # (/dev/disk/by-id/nvme-uuid.<uuid>) rather than /dev/nvmeNnM because NVMe
# controller/namespace indices are NOT stable (a transient discovery controller # controller/namespace indices are NOT stable (a transient discovery controller
@@ -142,6 +146,16 @@ jq -n --arg hash "$ROOT_HASH" --arg erofs_uuid "$FIXED_UUID" \
# The device node assumes the root namespace is the host's first/only # The device node assumes the root namespace is the host's first/only
# NVMe/TCP namespace; adjust if not. NVMET_TRADDR: set per environment # NVMe/TCP namespace; adjust if not. NVMET_TRADDR: set per environment
# *before* the UKI build, since the cmdline is immutable once signed. # *before* the UKI build, since the cmdline is immutable once signed.
# Plymouth: rhgb (Red Hat graphical boot) + quiet for a clean splash, or
# plymouth.enable=0 for a verbose debug boot. Press ESC at boot to see details
# behind the splash; or override at runtime via the SMBIOS Type 11
# io.systemd.stub.kernel-cmdline-extra OEM string (adds args without a rebuild).
if [ "$BOOT_SPLASH" = "true" ]; then
SPLASH_FRAGMENT="rhgb quiet "
else
SPLASH_FRAGMENT="plymouth.enable=0 "
fi
DOT1X_FRAGMENT="" DOT1X_FRAGMENT=""
if [ "$DISABLE_DOT1X" = "true" ]; then if [ "$DISABLE_DOT1X" = "true" ]; then
echo "==> WARNING: building TEST cmdline with rd.dot1x=0 (802.1X + MACsec DISABLED)" >&2 echo "==> WARNING: building TEST cmdline with rd.dot1x=0 (802.1X + MACsec DISABLED)" >&2
@@ -154,6 +168,7 @@ if [ "$DISABLE_DOT1X" = "true" ]; then
DOT1X_FRAGMENT="rd.dot1x=0 ip=dhcp " DOT1X_FRAGMENT="rd.dot1x=0 ip=dhcp "
fi fi
printf '%s' \ printf '%s' \
"$SPLASH_FRAGMENT" \
"$DOT1X_FRAGMENT" \ "$DOT1X_FRAGMENT" \
"rd.neednet=1 systemd.volatile=overlay " \ "rd.neednet=1 systemd.volatile=overlay " \
"rd.dot1x.nvindex=0x01800001 rd.dot1x.timeout=60 " \ "rd.dot1x.nvindex=0x01800001 rd.dot1x.timeout=60 " \
+3
View File
@@ -58,6 +58,9 @@ docker compose ps
# HTTP: UKI is served # HTTP: UKI is served
curl -sI http://${SERVER_IP}/fleet-${IMAGE_VERSION}.efi | head -1 # 200 OK curl -sI http://${SERVER_IP}/fleet-${IMAGE_VERSION}.efi | head -1 # 200 OK
# HTTP: kiosk test page is served (what the diskless kiosk loads by default)
curl -sI http://${SERVER_IP}/kiosk/ | head -1 # 200 OK
# NVMe/TCP: target is listening + discoverable # NVMe/TCP: target is listening + discoverable
ss -lntp | grep 4420 ss -lntp | grep 4420
sudo nvme discover -t tcp -a ${SERVER_IP} -s 4420 # lists the subsystem sudo nvme discover -t tcp -a ${SERVER_IP} -s 4420 # lists the subsystem
+4
View File
@@ -89,6 +89,10 @@ services:
subpath: artifacts # mount only /artifacts from the image subpath: artifacts # mount only /artifacts from the image
# Fallback (Docker < 28 / offline): bind the local Packer output dir: # Fallback (Docker < 28 / offline): bind the local Packer output dir:
# - ${ARTIFACT_SRC}:/usr/share/nginx/html:ro # - ${ARTIFACT_SRC}:/usr/share/nginx/html:ro
# Serve the kiosk test page at /kiosk/ alongside the artifacts. Config
# adds the location; the page is lab content (NOT in the artifact image).
- ./http/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./http/kiosk:/usr/share/nginx/kiosk:ro
# For HTTPS boot (production), terminate TLS here with the boot-infra CA's # For HTTPS boot (production), terminate TLS here with the boot-infra CA's
# cert and switch the dhcp-boot URL above to https://. Left HTTP for the lab. # cert and switch the dhcp-boot URL above to https://. Left HTTP for the lab.
+26
View File
@@ -0,0 +1,26 @@
# Lab boot HTTP server (nginx). Replaces the stock default.conf so one server on
# :80 serves BOTH:
# / -> the OCI release artifacts (UKI fleet-<ver>.efi, etc.), mounted
# read-only from the image volume at /usr/share/nginx/html
# /kiosk/ -> the diskless kiosk test page (lab content bind-mounted at
# /usr/share/nginx/kiosk, deliberately NOT part of the artifact
# image, which stays FROM-scratch).
# A separate kiosk container was considered, but every service here uses host
# networking, so a second nginx would fight for :80; a single location is cleaner.
server {
listen 80 default_server;
server_name _;
# UKI + release artifacts (firmware fetches /fleet-<ver>.efi from here).
location / {
root /usr/share/nginx/html;
autoindex on;
}
# Kiosk test page. The diskless kiosk (kiosk_url) points at
# http://boot.fleet.lab/kiosk/ — resolved to this host by the stack's dnsmasq.
location /kiosk/ {
alias /usr/share/nginx/kiosk/;
index index.html;
}
}
+72
View File
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<!--
Kiosk test page — served by the lab boot HTTP server (nginx) at /kiosk/, NOT
baked into the OCI artifact image (that stays FROM-scratch: UKI + rootfs only).
The diskless kiosk (weston + Chromium) loads this instead of an internet site,
so a boot is validated end-to-end without external connectivity: DNS resolves
boot.fleet.lab via dnsmasq, HTTP fetches from the serving host, and Chromium
renders it. Fully self-contained (no external fonts/CDN) so it works offline.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fleet Kiosk — Boot OK</title>
<style>
:root { --fg: #00ffd0; --dim: #4a8c84; --bg: #040a0d; --accent: #ff2e88; }
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: radial-gradient(circle at 50% 30%, #0a1620 0%, var(--bg) 70%);
color: var(--fg);
font-family: "DejaVu Sans Mono", ui-monospace, monospace;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; gap: 1.2rem; padding: 2rem;
overflow: hidden;
}
h1 { font-size: clamp(2rem, 7vw, 5rem); margin: 0; letter-spacing: .15em;
text-shadow: 0 0 18px var(--fg); }
.ok { color: var(--accent); text-shadow: 0 0 18px var(--accent); }
.sub { color: var(--dim); font-size: clamp(.9rem, 2.2vw, 1.3rem);
letter-spacing: .1em; }
.grid { display: grid; grid-template-columns: auto auto; gap: .35rem 1.2rem;
margin-top: 1rem; font-size: clamp(.8rem, 1.8vw, 1.1rem);
text-align: left; }
.grid .k { color: var(--dim); }
.clock { font-size: clamp(1.4rem, 4vw, 2.4rem); text-shadow: 0 0 12px var(--fg); }
.bar { width: min(60vw, 520px); height: 3px; background: var(--dim); opacity:.4;
border-radius: 2px; }
footer { position: fixed; bottom: 1rem; color: var(--dim); font-size: .8rem;
letter-spacing: .1em; }
</style>
</head>
<body>
<h1><span class="ok">▣</span> FLEET KIOSK ONLINE</h1>
<div class="sub">diskless boot validated &mdash; weston + chromium rendering local HTTP</div>
<div class="bar"></div>
<div class="clock" id="clock">--:--:--</div>
<div class="grid">
<span class="k">served from</span><span id="origin">&hellip;</span>
<span class="k">page url</span><span id="href">&hellip;</span>
<span class="k">user agent</span><span id="ua">&hellip;</span>
<span class="k">screen</span><span id="screen">&hellip;</span>
</div>
<footer>boot HTTP server &middot; replace this page or point kiosk_url elsewhere</footer>
<script>
// Live clock proves JS executes; the origin/href confirm where it was fetched.
function pad(n){ return String(n).padStart(2,'0'); }
function tick(){
var d = new Date();
document.getElementById('clock').textContent =
pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());
}
setInterval(tick, 1000); tick();
document.getElementById('origin').textContent = location.host || '(file)';
document.getElementById('href').textContent = location.href;
document.getElementById('ua').textContent = navigator.userAgent;
document.getElementById('screen').textContent =
window.innerWidth + '×' + window.innerHeight;
</script>
</body>
</html>