Add ntp time source and browser policy

This commit is contained in:
Audiobuzz
2026-06-18 09:35:58 +00:00
parent 8f8e24ac28
commit a53357216d
10 changed files with 222 additions and 10 deletions
+7
View File
@@ -152,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`).
- NTP via DHCP option 42 → chrony (no public-pool egress): confirm on a booted
host `chronyc sources` lists the DHCP-handed lab server (not `*.pool.ntp.org`).
Assumes (a) NM exports the lease's NTP servers to the dispatcher as
`$DHCP4_NTP_SERVERS`, and (b) RHEL 9 `/etc/chrony.conf` honours
`sourcedir /run/chrony-dhcp` (the provisioner appends it if absent). If sources
are empty, the dispatcher didn't fire or the env var name differs — check
`journalctl -u NetworkManager` and `ls /run/chrony-dhcp/`.
- `fleet` Plymouth theme: confirm (a) the script plugin package actually - `fleet` Plymouth theme: confirm (a) the script plugin package actually
installed (`/usr/lib64/plymouth/script.so` — else the build fell back to 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 `spinner`, logged as a WARNING), (b) the scripted theme + a font got bundled
+15 -2
View File
@@ -262,6 +262,16 @@ build {
# secure mode, reserving the physical NIC for wpa_supplicant. # secure mode, reserving the physical NIC for wpa_supplicant.
"chmod 0755 /usr/lib/dracut/modules.d/95dot1x-macsec/*.sh", "chmod 0755 /usr/lib/dracut/modules.d/95dot1x-macsec/*.sh",
# ---- NTP from DHCP (option 42) -> chrony, no public-pool egress --------
# NM dispatcher writes DHCP-provided NTP servers into chrony's sourcedir
# (replaces the removed dhclient.d/chrony.sh bridge). Strip the baked public
# pool so the host syncs ONLY from the lab NTP server handed via DHCP — the
# workstation never reaches *.pool.ntp.org.
"install -D -m0755 /tmp/etc-stage/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp",
"restorecon /etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp",
"sed -i -E 's/^[[:space:]]*(pool|server)[[:space:]]/#&/' /etc/chrony.conf",
"grep -q '^sourcedir /run/chrony-dhcp' /etc/chrony.conf || echo 'sourcedir /run/chrony-dhcp' >> /etc/chrony.conf",
# Inject the fleet NIC drivers into the dracut config # Inject the fleet NIC drivers into the dracut config
"sed -i 's/@NIC_DRIVERS@/${var.nic_drivers}/' /etc/dracut.conf.d/90-diskless.conf", "sed -i 's/@NIC_DRIVERS@/${var.nic_drivers}/' /etc/dracut.conf.d/90-diskless.conf",
@@ -341,15 +351,18 @@ build {
"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 # Chromium managed policy: stops the browser phoning home to Google
# (sign-in, variations, optimization-guide, component/auto update, metrics, # (sign-in, variations, optimization-guide, component/auto update, metrics,
# safe browsing). EPEL chromium reads /etc/chromium/policies/managed/*.json. # safe browsing). The managed-policy dir differs across chromium builds
# (/etc/chromium vs /etc/chromium-browser), and a policy in the wrong dir is
# silently ignored — so install to BOTH. Verify at runtime via chrome://policy.
# Pairs with the hardening flags in kiosk-session. # 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", "install -D -m0644 /tmp/kiosk/chromium-kiosk-policy.json /etc/chromium/policies/managed/10-kiosk-lockdown.json",
"install -D -m0644 /tmp/kiosk/chromium-kiosk-policy.json /etc/chromium-browser/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 /etc/chromium", "restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/xdg/weston /etc/kiosk /etc/chromium /etc/chromium-browser",
# 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",
+25 -8
View File
@@ -155,15 +155,32 @@ 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 - **Phone-home lockdown (done 2026-06-17/18):** Chromium was dialing Google
(accounts/variations/optimization-guide/component+auto update/safe browsing — (accounts/variations/optimization-guide/component+auto update/safe browsing —
seen in dnsmasq logs). Stopped via a managed policy seen in dnsmasq logs). Two layers:
(`files/kiosk/chromium-kiosk-policy.json` → `/etc/chromium/policies/managed/`) - **Flags** in `kiosk-session` (`--disable-background-networking`,
plus hardening flags in `kiosk-session` (`--disable-background-networking`, `--disable-component-update`, `--disable-features=OptimizationHints,...`) —
`--disable-component-update`, `--disable-features=OptimizationHints,...`, etc.). confirmed killing variations/optimization-guide/update queries.
Verify with the dnsmasq query log after a rebuild — only `boot.fleet.lab` - **Managed policy** `files/kiosk/chromium-kiosk-policy.json` for the
(kiosk page) + chrony NTP should remain. NTP is the OS (chrony), not Chromium; policy-governed bits (sign-in `accounts.google.com`, sync). FIX 2026-06-18:
optionally point it at the serving host to fully contain egress. the managed-policy dir differs by build, so it's installed to BOTH
`/etc/chromium/policies/managed/` AND `/etc/chromium-browser/policies/managed/`
(a policy in the wrong dir is silently ignored — the reason
`accounts.google.com`/`android.clients.google.com` persisted). Verify via
`chrome://policy` (Status: OK per entry).
- **Serving-side sinkhole** (defense-in-depth, `serving/docker-compose.yml`
dnsmasq `--address=/google.com|googleapis.com|gstatic.com/0.0.0.0`): the
kiosk is local-only, so block egress at the resolver. Stops the *leak*; the
query still logs (answered 0.0.0.0, not forwarded) — eliminating the query
is the browser policy's job. Production: do this at the boot-LAN firewall.
- **NTP egress closed (done 2026-06-18):** the `*.pool.ntp.org` lookups were
the OS (chrony), not Chromium. Now: a `chrony` NTP server in the serving
stack (`serving/chrony/`), dnsmasq hands it via **DHCP option 42**, and the
image's NM dispatcher (`files/etc/NetworkManager/dispatcher.d/10-chrony-dhcp-ntp`)
feeds it into chrony's `sourcedir /run/chrony-dhcp`; the baked public pool is
stripped from `/etc/chrony.conf`. Result: the workstation syncs time only
from the lab server — no external NTP egress. BENCH-VERIFY: on a booted host
`chronyc sources` shows the lab server (the DHCP one), not a public pool.
- **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.
@@ -0,0 +1,36 @@
#!/bin/bash
# NetworkManager dispatcher: feed DHCP option 42 (NTP servers) into chrony.
#
# Replaces the old dhclient.d/chrony.sh bridge — we use NetworkManager's internal
# DHCP now (the network-manager dracut port), and NM exports the lease's NTP
# servers to dispatcher scripts as $DHCP4_NTP_SERVERS. chrony.conf carries
# `sourcedir /run/chrony-dhcp`, so writing <iface>.sources there and reloading
# makes chronyd use the DHCP-provided server. The image's baked public pool is
# stripped (see the provisioner), so the host syncs ONLY from the lab NTP server
# — no time egress from the workstation.
#
# Installed 0755 root:root at /etc/NetworkManager/dispatcher.d/. Lenient on
# purpose: a hiccup here must never break networking.
interface="$1"
action="$2"
case "$action" in
up|dhcp4-change|reapply) ;;
*) exit 0 ;;
esac
dir=/run/chrony-dhcp
file="$dir/${interface}.sources"
mkdir -p "$dir"
if [ -n "${DHCP4_NTP_SERVERS:-}" ]; then
: > "$file"
for s in $DHCP4_NTP_SERVERS; do
printf 'server %s iburst\n' "$s" >> "$file"
done
# Pick up the new sources without restarting chronyd.
chronyc reload sources >/dev/null 2>&1 || true
fi
exit 0
+1
View File
@@ -6,6 +6,7 @@
"DeviceMetricsReportingEnabled": false, "DeviceMetricsReportingEnabled": false,
"BrowserSignin": 0, "BrowserSignin": 0,
"SigninAllowed": false,
"SyncDisabled": true, "SyncDisabled": true,
"SafeBrowsingProtectionLevel": 0, "SafeBrowsingProtectionLevel": 0,
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# preview-plymouth.sh — fast local preview of the 'fleet' boot splash WITHOUT
# building/booting the full diskless image. The splash is just plymouthd + the
# theme + status tokens, so it runs on any Linux box with plymouth installed
# (root), driven by the same `plymouth update --status=` tokens the dracut hooks
# emit (dot1x-macsec-up.sh / nvmf-connect.sh).
#
# sudo bash scripts/preview-plymouth.sh [secure|test]
#
# Seeing it:
# - Windowed: needs the X11 renderer (Debian/Ubuntu: `apt install plymouth-x11`,
# run inside an X session). Fedora/RHEL often don't package it.
# - No X11 renderer: switch to a spare VT (Ctrl+Alt+F3), run there, watch,
# Ctrl+Alt+F1 back — plymouthd grabs DRM on that VT.
# - Headless/CI screenshot: Xvfb + x11 renderer + a grab tool.
#
# Iterate: edit files/plymouth/themes/fleet/fleet.script, re-run this. Seconds,
# not a 30-min image build. (On an already-booted diskless host the root overlay
# is writable, so you can scp the theme to /usr/share/plymouth/themes/fleet and
# run this there for a real distro/font/plugin stack.)
set -euo pipefail
[ "$(id -u)" -eq 0 ] || { echo "run as root (plymouthd needs it)" >&2; exit 1; }
mode=${1:-secure}
theme_src=${THEME_SRC:-files/plymouth/themes/fleet}
dest=/usr/share/plymouth/themes/fleet
command -v plymouthd >/dev/null 2>&1 || {
echo "ERROR: plymouthd not found — install plymouth, plymouth-scripts," >&2
echo " plymouth-plugin-label, and the script plugin first." >&2
exit 1
}
[ -f "$theme_src/fleet.script" ] || {
echo "ERROR: $theme_src/fleet.script not found (run from the repo root, or set THEME_SRC)." >&2
exit 1
}
echo "==> installing theme: $theme_src -> $dest"
mkdir -p "$dest"
cp -a "$theme_src"/. "$dest"/
plymouth-set-default-theme fleet 2>/dev/null || \
echo "WARNING: plymouth-set-default-theme failed (plymouth-scripts missing?) — continuing" >&2
if [ ! -e /usr/lib64/plymouth/script.so ] && [ ! -e /usr/lib/plymouth/script.so ]; then
echo "WARNING: plymouth script plugin (script.so) not found — the theme will not render." >&2
fi
echo "==> starting plymouthd (debug log: /tmp/plymouth-preview.log)"
plymouth quit 2>/dev/null || true
plymouthd --no-daemon --debug --debug-file=/tmp/plymouth-preview.log &
ply_pid=$!
trap 'plymouth quit 2>/dev/null || true; kill "$ply_pid" 2>/dev/null || true' EXIT
sleep 1
plymouth show-splash
sleep 1
emit() {
echo " --status=$1"
plymouth update --status="$1" || true
sleep "${2:-1.2}"
}
echo "==> driving the '$mode' handshake sequence"
if [ "$mode" = test ]; then
emit mode-test 0.6
emit net-active; emit net-done
emit nvme-active; emit nvme-done 1.5
else
emit tpm-active; emit tpm-done
emit dot1x-active 1.6; emit dot1x-done
emit macsec-active; emit macsec-done
emit net-active 1.6; emit net-done
emit nvme-active 1.6; emit nvme-done 2
fi
echo "==> sequence complete; press Enter to quit the splash"
read -r _ || true
+3
View File
@@ -61,6 +61,9 @@ 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) # HTTP: kiosk test page is served (what the diskless kiosk loads by default)
curl -sI http://${SERVER_IP}/kiosk/ | head -1 # 200 OK curl -sI http://${SERVER_IP}/kiosk/ | head -1 # 200 OK
# NTP: lab time server answers (diskless hosts get it via DHCP option 42)
chronyc -h ${SERVER_IP} tracking 2>/dev/null || ntpdate -q ${SERVER_IP}
# 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
+9
View File
@@ -0,0 +1,9 @@
# Minimal chrony NTP server for the lab boot LAN. Diskless workstations get this
# host as their time source via DHCP option 42 (dnsmasq), so they never reach the
# public NTP pool (no workstation egress). Config is bind-mounted from
# chrony.conf in docker-compose.yml. Build our own so the version is pinned.
FROM alpine:3.20
RUN apk add --no-cache chrony
# -d: run in the foreground (don't daemonise) so the container stays up and logs
# to stdout for `docker compose logs`.
ENTRYPOINT ["chronyd", "-d", "-f", "/etc/chrony/chrony.conf"]
+20
View File
@@ -0,0 +1,20 @@
# Lab NTP server (serving stack). Syncs from upstream and serves the boot LAN so
# diskless workstations get time INTERNALLY (handed out as DHCP option 42 by
# dnsmasq) instead of egressing to the public pool. Only this host reaches the
# internet for time; the workstations never do.
# Upstream time source (this serving host has internet; the workstations don't).
pool pool.ntp.org iburst
# Serve the boot LAN. This is an isolated lab/boot network, so allow all; scope
# to your subnet (e.g. `allow 192.168.50.0/24`) for anything less trusted.
allow all
# Serve our own clock at a high stratum if upstream is unreachable, so a diskless
# boot never blocks waiting for time.
local stratum 10
# Step the clock on large initial offsets (containers can start far off), then
# slew. driftfile is best-effort (container fs is ephemeral).
makestep 1.0 3
driftfile /var/lib/chrony/drift
+27
View File
@@ -63,11 +63,24 @@ services:
# resolve the per-environment DNS name to this serving host. # resolve the per-environment DNS name to this serving host.
- --address=/${NVME_HOST}/${SERVER_IP} - --address=/${NVME_HOST}/${SERVER_IP}
- --address=/${BOOT_HOST}/${SERVER_IP} - --address=/${BOOT_HOST}/${SERVER_IP}
# Kiosk is LOCAL-ONLY (loads boot.fleet.lab), so sinkhole Chromium's
# call-home to 0.0.0.0 — telemetry can't leave the lab even if the browser
# tries. Defense-in-depth behind the in-image Chromium policy/flags; in
# production this belongs in the boot-LAN firewall's egress rules. NOTE: the
# client still *emits* the query (you'll see it in this log answered locally,
# not "forwarded"); stopping the query itself is the browser policy's job.
- --address=/google.com/0.0.0.0
- --address=/googleapis.com/0.0.0.0
- --address=/gstatic.com/0.0.0.0
# ---- DHCP ---- # ---- DHCP ----
- --dhcp-authoritative - --dhcp-authoritative
- --dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},${DHCP_LEASE} - --dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},${DHCP_LEASE}
- --dhcp-option=option:router,${GATEWAY_IP} - --dhcp-option=option:router,${GATEWAY_IP}
- --dhcp-option=option:dns-server,${SERVER_IP} - --dhcp-option=option:dns-server,${SERVER_IP}
# NTP server (option 42): point diskless hosts at the lab 'ntp' service
# below so they sync time internally and never reach the public NTP pool.
# The image's NM dispatcher (10-chrony-dhcp-ntp) feeds this into chrony.
- --dhcp-option=option:ntp-server,${SERVER_IP}
# ---- UEFI HTTP Boot ---- # ---- UEFI HTTP Boot ----
# Firmware sends vendor-class "HTTPClient"; we must echo it back (opt 60) # Firmware sends vendor-class "HTTPClient"; we must echo it back (opt 60)
# and hand over the UKI URL (opt 67). IP in the URL avoids a DNS round-trip # and hand over the UKI URL (opt 67). IP in the URL avoids a DNS round-trip
@@ -96,6 +109,20 @@ services:
# 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.
# --- NTP server (serves time to the boot LAN; handed out via DHCP opt 42) ---
ntp:
build: ./chrony
network_mode: host # serve NTP on udp/123 across the boot LAN
cap_add:
- SYS_TIME # discipline the (host-shared) clock it serves
restart: unless-stopped
volumes:
- ./chrony/chrony.conf:/etc/chrony/chrony.conf:ro
# NOTE: with host networking + SYS_TIME this chronyd disciplines the Docker
# HOST clock (the clock isn't namespaced) — intended: the serving host is the
# lab time source. Drop SYS_TIME (and add `chronyd -x`) if the host already
# runs its own NTP and you only want this to relay.
# --- NVMe/TCP target (serves rootfs.erofs, read-only) ---------------------- # --- NVMe/TCP target (serves rootfs.erofs, read-only) ----------------------
nvmeof: nvmeof:
# Prebuilt by CI (.gitea/workflows/spdk-image.yaml) and pulled here — no # Prebuilt by CI (.gitea/workflows/spdk-image.yaml) and pulled here — no