Commit Graph
239 Commits
Author SHA1 Message Date
audiobuzz 9ce493ef6f Merge pull request 'poam-hardening' (#21) from poam-hardening into main
Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/21
2026-07-12 06:30:53 +00:00
Audiobuzz 0fc54c64b7 docs: TLS audit offload bench-verified (poam-hardening-5bc6d27) — all gates passed 2026-07-12 06:28:18 +00:00
Audiobuzz 4fe6a5ae16 issue-cert: probe both smallstep password locations (secrets/password on older images) 2026-07-12 06:05:40 +00:00
Audiobuzz dd3a672a82 Merge remote-tracking branch 'origin/main' into poam-hardening
# Conflicts:
#	CLAUDE.md
2026-07-12 05:54:41 +00:00
audiobuzz 7bd42abf4d Merge pull request 'secure-ntp' (#20) from secure-ntp into main
Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/20
2026-07-12 05:48:24 +00:00
Audiobuzz 7b034a9249 docs(ssp): flip F-03/R-17 NTS to bench-verified (2026-07-12, clean build 5294860)
NTS authenticated time is now bench-verified end-to-end unattended on a clean
build (comet-beacon-54, 5294860): chronyc -N authdata shows ntp.fleet.lab in NTS
mode with cookies > 0, sources read under enforcing SELinux, no lo.sources, no
manual step. This clears the two dispatcher-integration defects found on the way
(distro 20-chrony-dhcp clobber; /run/chrony-dhcp SELinux label).

Flips F-03/P-18 / R-17 from "bench-verify pending" to bench-verified in the SSP
status line + register and the Annex §2.9 control row + R-17 register + summary,
mirroring the F-02 treatment. Rated Low. Residual (defence-in-depth, not blocking):
issue the per-environment NTS server cert, and exercise the cold-clock
nocerttimecheck bootstrap + active-spoof rejection.
2026-07-12 05:47:11 +00:00
Audiobuzz 5bc6d27f77 Adding rsyslog tls 2026-07-12 14:04:19 +09:30
Audiobuzz 5294860e74 secure-ntp: label /run/chrony-dhcp so chronyd can read the NTS source under SELinux
Second bench defect after removing the distro 20-chrony-dhcp clobberer
(secure-ntp-5a3eaed, comet-beacon-54): the dispatcher wrote the correct
`server ntp.fleet.lab iburst nts` line, but chrony loaded ZERO sources --
`chronyc reload sources` returned 200 OK yet authdata/sources stayed empty.

Root cause (AVC): chronyd_t was DENIED read on the .sources files because they and
/run/chrony-dhcp itself were labeled generic var_run_t; chronyd_t may only read its
own chronyd_var_run_t. The dispatcher's `mkdir -p` creates the dir as var_run_t and
files inherit it. The distro 20-chrony-dhcp we removed had been restorecon-ing them
to the readable label, so removing it exposed a latent labelling gap.

Stock RHEL policy already maps /run/chrony-dhcp -> chronyd_var_run_t (a restorecon
relabels it and chrony immediately reaches NTS Cook 8). Fix: ship
/etc/tmpfiles.d/chrony-dhcp.conf so systemd-tmpfiles pre-creates the dir with that
fcontext at sysinit, before any NM event -- the dispatcher's files then inherit
chronyd_var_run_t and chronyd can read them. No SELinux allow rules needed
(bench-verified: after the fix a real NM event wrote the source as chronyd_var_run_t,
no AVC, authdata -> NTS Cook 8). Applies in both secure_ntp modes.

Also: the dispatcher now skips `lo` -- it was writing a spurious lo.sources
duplicate of the same server (the distro script had skipped loopback).

BENCH-VERIFY on the next clean build: a normal boot leaves the source file labeled
chronyd_var_run_t (ls -Z), no lo.sources, and authdata reaches NTS/cookies>0 with no
manual step.
2026-07-12 02:53:05 +00:00
Audiobuzz 5a3eaedec5 secure-ntp: remove the chrony pkg NM dispatcher that clobbered the NTS source
NTS was inactive on the clean-build deploy (secure-ntp-d1621e9, comet-beacon-54):
chronyc authdata showed a plain IP source in Mode '-', Cook 0, with no NTS-KE
attempt at all -- chrony was on plain NTP.

Root cause: NetworkManager runs dispatcher scripts from BOTH
/etc/NetworkManager/dispatcher.d and /usr/lib/NetworkManager/dispatcher.d, merged
and sorted by filename. The chrony RPM ships
/usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp, which writes the DHCP
option-42 server to the SAME /run/chrony-dhcp/<iface>.sources file our
10-chrony-dhcp-ntp uses. Sorting after 10-, it ran last and overwrote our
`server ntp.fleet.lab iburst nts` line with `server 192.168.50.10 iburst`, then
reloaded chrony -- last writer wins on the shared filename, silently defeating
authenticated time.

Isolation proof: removing the distro script + hand-writing the NTS source brought
NTS up immediately (ntp.fleet.lab NTS, Cook 8, zero NAKs), confirming the host
image, the fleet-signed NTS-KE cert, DNS, and tcp/4460 were all already correct --
the collision was the sole defect.

Fix: the provisioner removes /usr/lib/NetworkManager/dispatcher.d/20-chrony-dhcp
at build (our dispatcher supersedes it in both modes); 20-chrony-onoffline is left
in place (harmless). Documents the finding in the dispatcher header,
docs/secure-ntp-design.md, and the CLAUDE.md secure_ntp note. BENCH-VERIFY on the
next clean build: a normal boot reaches NTS/cookies>0 with no manual step.
2026-07-11 14:02:40 +00:00
Audiobuzz d1621e9b74 Merge remote-tracking branch 'origin/main' into secure-ntp 2026-07-11 21:03:23 +09:30
Audiobuzz 0a3deca335 docs: cite clean build 28d31e4 for F-02 bench-verification
F-02/P-17 SSH host certs re-verified on the rebuilt 28d31e4 image (not the
hand-fixed host): the auto-derived FQDN principal is baked, so by-FQDN no-TOFU
admin login survives a reboot with zero manual steps. Cite the digest in the
design-doc status + bench-outcome, SSP v0.4 status line + R-16 row, and Annex A
2.4 row + R-16 register row.
2026-07-11 11:31:57 +00:00
audiobuzz b0890cfc09 Merge pull request 'poam-hardening' (#19) from poam-hardening into main
Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/19
2026-07-11 11:29:29 +00:00
Audiobuzz f9f7c7c940 secure-ntp: fix NTS defects surfaced by the F-03 bench
NTS proven end-to-end on comet-beacon-54 (chronyc authdata cookies > 0); these
close the gaps the bench found so a clean build/serving run reaches that state
without manual patching:

- dispatcher (10-chrony-dhcp-ntp): fall back to resolv.conf search/domain when NM
  doesn't export DHCP4_DOMAIN_NAME on the firing event, so the NTS source is
  written reliably at boot (tonight it needed a manual dispatcher run).
- STIG vs secure_ntp (P2, security): STIG's chronyd_specify_remote_server re-injects
  an UNAUTHENTICATED 'server 0.us.pool.ntp.mil maxpoll 16' AFTER the secure_ntp
  chrony step commented servers out -> a silent fallback that defeats NTS's
  DoS-only guarantee (host not fail-closed). Unselect chronyd_specify_remote_server
  + chronyd_or_ntpd_set_maxpoll in diskless-tailoring.xml, and re-strip any
  server/pool in 2c-STIG-postfix when secure_ntp=true. Documented: stig-deviations #17.
- nts-cert.sh (P3): the container's chrony user (non-root) can't read a 0640
  host-owned key -> NTS-KE TLS aborts ('non-properly terminated'). chown the key to
  the ntp container's chrony uid + keep 0640 (0644 works but chrony warns
  world-readable); 0644 fallback if the uid can't be found.

Bench-verified 2026-07-11; docs updated in docs/ssh-host-certificate-design.md's
sibling secure-ntp-design.md flow. F-03/P-18 stays open until a clean build shows
chrony.conf has no plain server and authdata cookies>0 as the only source.
2026-07-11 11:21:46 +00:00
Audiobuzz aab6c9919c docs(ssp): flip F-02 SSH host certs to bench-verified (2026-07-11)
F-02/P-17 (SSH host certificates, R-16) bench-verified end-to-end on
comet-beacon-54: admin login by FQDN with no TOFU, per-boot auto-re-sign across
reboot, SELinux confinement clean. Updates the SSP v0.4 status line + R-16
summary row and Annex A version note, the 2.4 host-key-trust row, the R-16
register row (treatment -> roll anchor + broker-side principal validation TODO)
and the 3.1 narrative. F-03/P-18 (NTS, R-17) stays bench-verify pending -- not
yet tested. Bench-surfaced code fixes are on branch poam-hardening (28d31e4).
2026-07-11 09:55:43 +00:00
Audiobuzz 8e1418c19d docs(ssh-host-cert): record F-02 bench-verified end-to-end (2026-07-11)
SSH host-cert flow proven on comet-beacon-54 (TPM-mTLS -> broker -> step-ca ->
cert install -> admin login by FQDN, no TOFU; per-boot auto-re-sign across
reboot). Design doc gains a Bench outcome section (defects+fixes table, retracted
empty-principals finding, post-boot TOFU window, deploy caveat); status flips to
BENCH-VERIFIED. TODO 'SSH host keys' caveat marked RESOLVED (F-02/P-17). The
bench-surfaced code/serving fixes live on branch poam-hardening (28d31e4).
2026-07-11 09:50:33 +00:00
Audiobuzz 28d31e436c ssh-hostcert + enrol-broker: fix defects surfaced by the F-02 bench
F-02/P-17 SSH host-cert flow was proven end-to-end on comet-beacon-54
(2026-07-11); these fix the correctness/scoping/cleanup defects the bench
surfaced:

- ssh-hostcert: derive the FQDN principal <hostname>.<domain> from the DHCP
  search domain at runtime (resolv.conf), alongside the short hostname, so an
  admin `@cert-authority *.<domain>` line accepts by-FQDN logins instead of
  "name is not a listed principal". Never baked (invariant #1). + tests.
- ssh-ca-bootstrap.sh: export the SSH host CA public key in OpenSSH wire format
  (ssh-keygen -i -m PKCS8), not the PEM/PKCS8 that `step crypto keypair` writes
  -- a PEM @cert-authority anchor is unparseable and silently falls back to TOFU.
- ssh-ca-bootstrap.sh: fail loudly (and verify enableSSHCA=true) if the
  provisioner SSH-enable step fails, instead of swallowing it and shipping a CA
  that 502s every runtime sign.
- dot1x-bench: point the co-located broker at https://localhost:8443, not the
  fleet dnsmasq name ca.fleet.lab (unresolvable from the broker's host netns ->
  "no such host" at sign time). localhost is in the CA cert SAN.
- serving: remove the mock-signer enrol-broker copy -- two host-networked
  brokers collide on 9090/9443 and the mock issued untrusted certs. The real
  broker lives with step-ca (dot1x-bench); nginx /enrol/ still proxies to it on
  loopback.
- enrol-broker: retire the HTML admin console; the fleet-manager passkey SPA now
  drives the mTLS JSON API (/pending, /approve|deny). Removes a weaker,
  client-cert-only parallel human-approve path.

Design-doc bench note deferred: docs/ssh-host-certificate-design.md lives on the
secure-ntp branch (fbb8100), not here -- to be added there / on reconcile.
2026-07-11 09:44:33 +00:00
Audiobuzz fbb8100ecd Adding secure NTP 2026-07-11 17:32:05 +09:30
Audiobuzz 8325818c1b fix pipeline use of bash 2026-07-11 16:24:07 +09:30
Audiobuzz 2e318fba00 adding ssh host signing 2026-07-11 16:20:21 +09:30
audiobuzz b5d6c790c4 Merge pull request 'fleet-manager' (#18) from fleet-manager into main
Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/18
2026-07-10 23:16:44 +00:00
Audiobuzz 57fb5c3a3e docs(ssp): v0.3 — add the fleet-management plane + July hardening changes
The SSP (v0.2, 2026-06-27) and annex predated the entire management/enrolment
plane and the July hardening work. Bring both to v0.3 (2026-07-10):

SSP:
- Scope/boundary: on-host fleet-agent now IN scope; server-side manager/broker/
  step-ca/InfluxDB named as connected systems; new host interface (g) = outbound
  mTLS to the management plane. Boot chain gains step 8 (post-switch-root agent).
- Data: management telemetry (operational metadata, mTLS-encrypted, no per-host
  secret); the agent stream is the one non-plaintext transit path.
- §5.2 Protect: agent reuses the TPM identity for mTLS; WebAuthn-passkey admin
  MFA (first human-admin MFA); 3 local SELinux modules; the 2026-07-04 fapolicyd
  overlay watch_fs fix (app-control was inactive on the root until then).
- §5.3 Detect: operational health telemetry signal (distinct from auditd/Wazuh).
- §6 Essential Eight: MFA + admin rows updated (passkeys now enforced on mgmt UI).
- §7: new R-14 (mgmt plane = connected system pending authorisation) + R-15
  (deferred agent actuators = future remote-mutation channel); R-05 MFA clause met.

Annex:
- §2.3 hardening: fapolicyd overlay-effectiveness fix + residual; 3 local SELinux
  allow modules on the MAC row.
- §2.4 auth: agent mTLS on the machine-auth row; MFA row now Implemented for the
  mgmt UI (passkeys), Partial for host SSH.
- §2.5 mgmt: new rows for TPM enrolment lifecycle (broker) and server-authoritative
  management plane. §2.6: operational health-monitoring row. §2.7: mgmt-plane
  segmentation (OPNsense L3, outbound-only). §2.9: agent mTLS, fleet CA (step-ca),
  cert-lifecycle auto-renewal.
- §3 register: R-14, R-15 added; R-05 updated; heatmap + note + 3 new assessor
  notes (scope the mgmt plane, witness fapolicyd deny, confirm actuators inert).
2026-07-10 22:56:34 +00:00
Audiobuzz 3284a2713b docs: reflect chrony/uptime metrics, agent-cert renewer, and the chrony-access findings
Recent work added telemetry fields and a cert path not yet in the docs:
- files/agent/README.md: add uptime to the health list; document that chrony is
  read from /var/log/chrony/tracking.log (not the command socket) and needs BOTH
  the chrony supplementary group (DAC) and the fleet_agent_chronylog SELinux allow
  (TE), plus a reachable lab NTP (123/udp firewall); note the chrony group on the
  unit row.
- serving/fleet-manager/README.md: add uptime + chrony detail to the health list;
  note the cert-auto agent-cert-renewer sidecar + GetCertificate hot-reload
  (certreload.go) in the bring-up and checkpoint 7.
- docs/TODO.md #14: probe list gains uptime + chrony-from-tracking.log; new dated
  block records uptime/reboot-pending, the 4-layer chrony-access chase (all
  silent-failing; inventory.json is the isolator), and the agent-plane cert
  auto-renewal, all bench-verified 2026-07-10.
- docs/fleet-management-design.md: health record gains uptime + chrony
  source/stratum/offset detail.
2026-07-10 22:45:00 +00:00
Audiobuzz a46e5edf69 agent: add chrony supplementary group so it can read tracking.log (DAC)
The tracking.log read was still returning empty under enforcing with NO AVC —
the tell of a DAC (not SELinux) denial. Root cause: /var/log/chrony is mode
0750 chrony:chrony (others have no access), so the agent's DynamicUser (group
tss only) can't even traverse the directory to reach the 0644 tracking.log —
EACCES on open, which produces no audit record. Manager confirmed genuinely on
fleet-manager-1db3977 (chrony-capable), and uptime flowed while chrony didn't,
proving the agent was sending empty chrony (failed read), not a manager gap.
Earlier manual reads worked only as root (sudo) or because fleetadmin had been
usermod'd into chrony during the socket test (reverted on reboot — /etc/group
is on the volatile overlay).

Fix: SupplementaryGroups=tss chrony on the unit — the chrony group has r-x on
the 0750 dir so the agent can traverse it; the log file is world-readable. This
is the DAC half; the fleet_agent_chronylog SELinux module (02a6621) is the TE
half — BOTH are required (verified: group-only -> TE-denied; module-only ->
DAC-denied silently). Corrected the now-stale 'no group' code comment; recorded
the two-layer requirement in the .te and stig-deviations #16. Needs a
workstation image rebuild + reboot.
2026-07-10 12:45:44 +00:00
Audiobuzz 02a6621a19 image: SELinux allow fleet-agent to read chrony tracking.log under enforcing
The tracking.log approach (4ca0b6d) works in permissive but is blocked under
enforcing: chrony's log is labeled chronyd_var_log_t, confined by the shipped
policy to chronyd_t despite the 0644 mode. Verified on comet-beacon-54 — a
non-root read succeeds permissive, but under enforcing even a STIG-confined
admin (staff_t) gets Permission denied (ls -Z on the file is itself blocked).
The agent runs unconfined_service_t (confirmed via ps -eZ) and is denied the
same way.

Ship a minimal read-only allow module (files/stig/fleet-agent-chronylog.te):
allow unconfined_service_t chronyd_var_log_t:file { open read getattr } + dir
search to reach it. No write, no other chrony access, and no domain transition
(the agent reads a file; it does not exec chronyc) — narrower than the
abandoned nnp-transition module. Built + semodule -i'd in 2c-STIG alongside the
existing chrony_dhcp_dispatcher / fapolicyd_tmpfs modules. docs/stig-deviations
#16. Needs a workstation image rebuild + reboot; same policy-store-rebuild
reproducibility re-check as #14/#15.
2026-07-10 09:57:34 +00:00
Audiobuzz 8eb28c0a6b serving: document opening 123/udp on the host firewall for the NTP server
The ntp service is host-networked, so (like the audit collector's 514/tcp)
Docker doesn't open the port — the serving host's own ufw/firewalld governs
123/udp. Without a rule, workstations are handed 192.168.50.10 as their NTP
server via DHCP opt 42 but their packets are dropped: chrony logs 'Added source
… / unreachable' and never syncs (seen on comet-beacon-54). Add the boot-LAN
firewall rule to the ntp service NOTE, mirroring the audit-collector README.
2026-07-10 09:42:26 +00:00
Audiobuzz 4ca0b6dd06 agent: read chrony status from tracking.log instead of the command socket
Bench on comet-beacon-54 proved every command-socket route is blocked for the
hardened agent: chronyd's unix socket is owner-write-only (0755, so even a
chrony-group member can't connect), and STIG closes the UDP command port
(cmdport 0). Rather than re-open a port or expose chrony's privileged socket,
enable chronyd's tracking log and have the agent READ it — no port, no socket,
no group, no SELinux domain transition, full hardening intact.

- chrony.conf: enable 'log tracking' (idempotent) so chronyd appends
  source/stratum/offset to /var/log/chrony/tracking.log. Write volume is a ~130B
  line per clock update (backing off to ~maxpoll when stable) and capped by the
  chrony RPM's logrotate — negligible on the RAM overlay. cmdport 0 stays, so no
  STIG deviation.
- agent: chronyTracking() now reads the last data line of tracking.log via
  parseTrackingLog() (source f[2], stratum f[3], offset f[6], leap/sync f[7]);
  replaces the chronyc exec + parseChrony. Unit test swapped accordingly.
- revert the two abandoned approaches: drop the bindcmdaddress line (394ad10)
  and the fleet_agent_chronyc SELinux nnp_transition module + provisioner block
  (e6fd4a2, .te deleted) — no chronyc exec means no transition to allow. Removed
  stig-deviations #16 and #17 (both waivers no longer needed).

Needs a workstation image rebuild + reboot. BENCH-VERIFY: tracking.log is
world-readable so the DynamicUser agent can read it; under enforcing
unconfined_service_t can read chronyd_var_log_t (add a minimal file-read allow
if denied); and the offset sign matches chronyc on a synced host.
2026-07-10 09:36:37 +00:00
Audiobuzz 394ad10824 agent: query chrony over loopback UDP command socket (unix socket unusable under sandbox)
Root cause of the blank chrony fields (found on comet-beacon-54): chrony 4.x
(RHEL 9) defaults its command interface to the UNIX socket only
(/run/chrony/chronyd.sock), usable by root or the chrony group with a writable
/run/chrony. The fleet-agent unit is hardened — DynamicUser (group tss, not
chrony) + ProtectSystem=strict (/run read-only) — so it cannot open the unix
socket nor create chronyc's reply socket. Bench proof: normal-user 'chronyc
tracking' -> 506 Cannot talk to daemon; only sudo worked; 'chronyc -h 127.0.0.1'
failed and ss showed no UDP :323. So the probe always failed and every chrony_*
field stayed empty. SELinux permissive did NOT help — it was never (only) an
SELinux problem; the nnp_transition module (prev commit) is still needed for the
enforcing exec transition.

Fix: bind chronyd's command socket on 127.0.0.1 (bindcmdaddress in chrony.conf,
idempotent in the 2c provisioner) and point the probe at it (chronyc -n -h
127.0.0.1 tracking). UDP needs no fs write or group and rides the unit's
already-allowed AF_INET. Loopback + monitoring-only (state changes still need the
root unix socket), so exposure is minimal. Rejected the unix-socket route (chrony
group + ReadWritePaths=/run/chrony) because /run/chrony is 0750 (group has no
write). Documented in docs/stig-deviations.md #17; .te bench note updated for the
enforcing UDP path. Needs a workstation image rebuild + reboot.
2026-07-10 09:02:05 +00:00
Audiobuzz e6fd4a2dde image: SELinux allow fleet-agent chronyc probe under NoNewPrivileges
fleet-agent reports clock health via 'chronyc -n tracking', but the hardened
unit runs with NoNewPrivileges (explicit + forced by DynamicUser/SystemCallFilter/
MemoryDenyWriteExecute). SELinux transitions the exec'd chronyc into chronyc_t,
and NNP forbids that domain change without process2:nnp_transition — which the
base policy doesn't grant for unconfined_service_t -> chronyc_t. Under enforcing
the exec is denied (avc: denied { nnp_transition } comm=fleet-agent, seen on
comet-beacon-54), so chronyc never runs and every chrony_* health field stays
empty (Monitor chrony row shows '—'). uptime/cpu/mem are unaffected (no exec).

Ship a minimal local module (files/stig/fleet-agent-chronyc.te) granting only
that one nnp_transition to the MORE-confined chronyc_t domain — the safe
direction the permission exists to gate — loosening nothing else in the agent
sandbox. Built + semodule -i'd in 2c-STIG alongside the existing
chrony_dhcp_dispatcher / fapolicyd_tmpfs modules. Justified in
docs/stig-deviations.md #16. Needs a workstation image rebuild + reboot; same
policy-store-rebuild reproducibility re-check as #14/#15.
2026-07-10 08:39:27 +00:00
Audiobuzz 1db3977238 fleet-manager: auto-renew the agent-plane server cert (step-ca sidecar + hot-reload)
The agent-plane server cert was issued by hand (server-pki.sh / docker exec
step ca certificate), which needs the step CLI on the host, re-specifies SANs
each time, and silently expires at the CA's 90-day cap — the exact failure that
just broke agent TLS (cert minted with a stale SAN, missing manager.fleet.lab).

Automate it:
- agent-cert-renewer sidecar (smallstep step-cli image, compose profile
  cert-auto): issues the cert once from the co-located fleet step-ca, then
  'step ca renew --daemon' rotates it before the cap. Renewal preserves the
  subject + SANs, so SAN drift cannot recur. Chowns to the manager's nonroot
  uid so the distroless manager reads the key. Opt-in + only valid where the CA
  is co-located; server-pki.sh stays as the manual/remote-CA path.
- manager hot-reloads via GetCertificate (certreload.go): a rotated cert on
  disk is picked up on the next handshake with no restart and no dropped agent
  streams; a half-written pair keeps the last good cert serving. Unit-tested.
- defaults.env documents the cert-auto knobs; DEPLOY.md §2a recommends the
  renewer and flags the mandatory manager.fleet.lab SAN.
2026-07-10 07:42:18 +00:00
Audiobuzz 1c68c37824 fleet-manager: chrony tracking metrics relative to the NTP source
The agent only reported chrony as synced|unsynced — nothing about how well the
clock tracks. Parse `chronyc tracking` for the source-relative metrics.

- proto: HealthSnapshot.chrony_offset_seconds (16, signed system-time offset,
  fast=+), chrony_source (17, reference name/IP), chrony_stratum (18); regenerated.
- agent: replace chronyState() with chronyTracking()/parseChrony() — one exec,
  parses Leap status + Reference ID + Stratum + System time offset (+ test).
- manager: Health.{ChronyOffset,ChronySource,ChronyStratum}; snapshotToHealth maps
  them; influx writes chrony_offset_seconds/_stratum/_source and whitelists the
  offset + stratum as graphable fields.
- UI: chronySummary() + offsetMs() — the chrony row now shows
  "synced · 192.168.50.10 · s3 · +0.01ms" instead of just "synced".
2026-07-10 06:47:27 +00:00
Audiobuzz df7dad81b7 fleet-manager: host uptime metric + reboot-pending status
The manager lets an admin schedule a reboot (desired.reboot_at) but never showed
whether it had happened. Add host uptime to the health snapshot and derive a
reboot status from it.

- proto: HealthSnapshot.uptime_seconds (15); regenerated pb.go (manager + agent copy).
- agent: probe /proc/uptime -> uptime_seconds (with parser + test).
- manager: Health.UptimeSeconds cached in inventory; snapshotToHealth maps it;
  influx writes + whitelists uptime_seconds as a queryable field.
- UI: fmt.js dur() + rebootStatus() — boot_time = last_seen - uptime (manager clock
  frame, no host/manager sync needed), compared to reboot_at. Monitor host detail
  shows an "uptime" row and a "reboot" row: pending in X / overdue by X / rebooted.

Observability-only — works before the reboot ACTUATOR lands (deferred). Once a host
boots after the scheduled time (BMC/manual now, agent later), the badge flips to
"rebooted" automatically.
2026-07-10 06:41:09 +00:00
Audiobuzz e6ae8c4b23 fleet-manager UI: auto-refresh the selected host's metric graphs
The Monitor tab polled /api/hosts every 10s (roster + host-detail refresh) but
only fetched a host's metric series on click, so the tmpfs/cpu/temp graphs went
stale until you re-selected. Extract loadMetrics() and call it from the same 10s
tick when a host is selected, so the graphs stay live.
2026-07-10 06:22:33 +00:00
Audiobuzz d85a1e5b23 serving: defaults.env header + Makefile + .gitignore + doc refs
Completes the rename commit (which carried only the file move): defaults.env
layering header, docker-compose usage comment, serving/Makefile (sets
COMPOSE_ENV_FILES), serving/.gitignore (.env), and the README/TODO references.
2026-07-10 06:19:11 +00:00
Audiobuzz cbc679c5bd serving: rename .env.example -> defaults.env (committed defaults + .env overrides)
Same layering as the fleet-manager stack: defaults.env holds the committed
defaults, .env holds only per-LAN overrides + secrets, layered via
COMPOSE_ENV_FILES=defaults.env,.env. Changing a default here reaches every run
without re-copying.

- git mv serving/.env.example -> serving/defaults.env; header explains the layering.
- docker-compose.yml usage comment -> the COMPOSE_ENV_FILES / Makefile flow.
- serving/Makefile: sets COMPOSE_ENV_FILES; help/up/down/restart/pull/logs/ps/config
  with a clear .env-missing guard (mirrors serving/fleet-manager/Makefile).
- serving/.gitignore: ignore .env (defaults.env is committed).
- README + TODO references updated. serving/siem/.env.example is a separate
  wazuh1-node file, left as-is.
2026-07-10 06:18:15 +00:00
Audiobuzz 1b8cf2f344 agent: dial the manager by a fleet DNS name (manager.fleet.lab)
The agent's baked default pointed at the PUBLIC admin-UI name
(diskless-server.bitasylum.org), which resolves to the serving host's outbound IP
— unreachable from the fleet LAN. There's no split-horizon to "automate": the
manager agent plane is just another *.fleet.lab, served by the fleet dnsmasq like
boot/nvme/audit.

- serving/.env.example: MGMT_HOST=manager.fleet.lab.
- serving/docker-compose.yml: dnsmasq --address=/${MGMT_HOST}/${SERVER_IP}.
- files/agent/agent.env + main.go default: FLEET_AGENT_MANAGER=manager.fleet.lab:9600.
- server-pki.sh: AGENT_SANS default -> manager.fleet.lab (cert SAN matches the
  baked name, so no FLEET_AGENT_SERVER_NAME override).
- docs/TODO #14 gotcha #4 updated: fleet name, not the IP+server-name workaround.

The public diskless-server.bitasylum.org stays the ADMIN-UI name (Traefik/:443).
2026-07-10 06:13:46 +00:00
Audiobuzz 0068a15917 docs/TODO #14: on-host agent telemetry BENCH-VERIFIED on real hardware
Record the 2026-07-10 end-to-end verification (Dell 820 comet-beacon-54, real
TPM): TPM-identity mTLS -> enrolled -> streaming health + receiving desired-state,
manager logs "agent connected". Add the "deploying the agent plane" gotcha list
(placeholder CA, root+intermediate client CA, distroless PKI perms, split-horizon
DNS, out-of-band inventory seeding) — all automated by commit a89c3d2 — so the
next deploy doesn't rediscover the debug chain.
2026-07-10 06:08:48 +00:00
Audiobuzz a89c3d2af1 fleet-manager: fix CA install, add register-host, surface agent status
Four fixes from the on-host agent bring-up:

1. Bake the real fleet CA into the image. New `fleet_ca` build var (default =
   the committed placeholder) file-provisions the trust anchor to
   /etc/dot1x/radius-ca.pem — used by wpa_supplicant (RADIUS) AND the fleet-agent
   (verify the manager). CI materializes a FLEET_CA secret to a runner-temp file
   and passes -var fleet_ca; unset => placeholder (lab). No CA material committed.

2. server-pki.sh installs the agent CLIENT CA as root+intermediate (not root
   only). Host TPM certs are leaf-only in NV and signed by the step-ca
   intermediate, so a root-only client CA makes the manager advertise only the
   root in its CertificateRequest -> Go clients send no cert ("tls: certificate
   required") and it couldn't verify a leaf-only cert anyway. bootstrap.sh now
   exports intermediate_ca.crt so FLEET_CA_INTERMEDIATE resolves.

3. New `fleet-manager register-host <serial> [-name] [-hostnqn]` subcommand:
   seeds/updates an inventory record for a host enrolled out of band (bench
   broker, migrated fleet) through the real store, so its agent stream is
   accepted. The manual counterpart to the authorise-approve flow.

4. The agent surfaces the authoritative recv-side gRPC status instead of the
   bare io.EOF gRPC returns on the send side when the server ends the stream —
   so "host is not enrolled" (PermissionDenied) shows up instead of "EOF".
2026-07-10 05:51:57 +00:00
Audiobuzz 4b99816394 Merge in main 2026-07-10 07:10:40 +09:30
audiobuzz 08f2d82bd1 Merge pull request 'optimise-ram' (#11) from optimise-ram into main
Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/11
2026-07-05 06:46:52 +00:00
Audiobuzz 254ec79cae fleet-agent: install agent.env from /tmp/agent (complete staging fix)
Companion to the previous commit's file move: point the 2b-agent install at
/tmp/agent/agent.env (the agent's own upload) instead of the deleted
/tmp/etc-stage path, update the file-provisioner comment, the enable_agent var
description, and the TODO source-path reference. packer validate passes.
2026-07-05 06:36:54 +00:00
Audiobuzz d0f39443ce fleet-agent: fix 2b-agent config staging (build failed)
The 2b-agent provisioner installed agent.env from /tmp/etc-stage, but the 2a
package step deletes /tmp/etc-stage before 2b runs, so the build failed with
"cannot stat '/tmp/etc-stage/fleet-agent/agent.env'". 2b-hostapi avoids this by
installing from its own upload dir; do the same: move agent.env into files/agent/
so it rides the agent's file upload (/tmp/agent), which survives until 2b-agent
cleans it. Runtime path (/etc/fleet-agent/agent.env) is unchanged.
2026-07-05 06:36:18 +00:00
audiobuzz 6b675a1bda Merge pull request 'fapolicyd-tmpfs' (#16) from fapolicyd-tmpfs into main
Reviewed-on: https://git.bitasylum.org/audiobuzz/diskless-workstation/pulls/16
2026-07-05 03:21:46 +00:00
Audiobuzz d5c833addf fleet-agent: on-host TPM-mTLS telemetry client (v1)
Build the on-host agent that pairs with fleet-hostapi and feeds the
fleet-manager control plane (TODO #14 "STILL TO BUILD (a)"). Nothing on the
workstation talked to the manager before this; Monitor + InfluxDB stayed empty.

Scope: telemetry only. The agent reuses the host TPM identity for mTLS (read-only
tpmkey.LoadSigner at 0x81000100 + cert from NV 0x01800100; the private key never
leaves the TPM), opens the long-lived FleetAgent.Session bidi stream, sends Hello
+ periodic HealthSnapshot (load/cpu/temp/mem/tmpfs/nvme/auth/selinux/chrony/kiosk,
probed read-only from /proc+/sys plus hostapi facts), and LOGS but does not act on
the DesiredState/Command it receives. Reconcile actuators (kiosk URL, reboot,
drain) are deferred until the telemetry half is bench-verified.

- files/agent/: new module (go-tpm + grpc), static CGO_ENABLED=0 like enrol/hostapi.
  Copies the enrol tpmkey (+ read-only LoadSigner) and the generated fleet.agent.v1
  proto so it stays self-contained without importing the initramfs enrol module.
- files/agent/simtest/: proves the TPM-signed cert completes a real mTLS client-auth
  handshake (RequireAndVerifyClientCert) against the go-tpm software simulator.
- files/etc/fleet-agent/agent.env: fleet-generic config (manager DNS name, fleet CA
  reused from radius-ca.pem, TPM handle/nvindex, interval). No per-host state.
- fleet-agent.service: hardened unit (tss group for /dev/tpmrm0, INET egress).
- CI builds fleet-agent out-of-VM + runs vet/test + the simulator handshake test.
- Packer: enable_agent flag (default on) + 2b-agent provisioner + fapolicyd trust.
  Post-switch-root only, so no dracut/initramfs changes.
- docs: TODO #14 + fleet-management-design as-built updated.

Requires the host already enrolled (broker approve births the inventory row) —
else the manager returns PermissionDenied, the correct "not enrolled yet" signal.
2026-07-05 03:15:13 +00:00
Audiobuzz 8ab11a74c5 fleet-manager: add Makefile that sets COMPOSE_ENV_FILES for you
`make up` / `logs` / `restart` / `pull` / `build` / `config` / `register-admin`
/ `backup` — each exports COMPOSE_ENV_FILES=defaults.env,.env so plain `make ...`
layers the committed defaults under .env with no manual export or --env-file.
Guards on .env existing (COMPOSE_ENV_FILES requires it) with a clear message.
`make help` lists targets. DEPLOY §1 points at it.
2026-07-05 02:14:22 +00:00
Audiobuzz 45d7101472 fleet-manager: layer config as defaults.env (committed) + .env (overrides)
Rename .env.example -> defaults.env and make it the committed defaults layer.
Compose reads defaults.env then .env (later wins) via COMPOSE_ENV_FILES, feeding
BOTH container env and ${...} interpolation — so a changed default here reaches
every run without re-copying, and .env shrinks to just overrides + secrets.

No compose service restructure: the existing interpolated environment: blocks
already build the container env; the layering just supplies their values from two
files. (env_file: directive alone wouldn't work — it doesn't feed the image-tag /
cross-service Influx / ACME_EMAIL interpolation this compose relies on.)

Docs (DEPLOY §1, compose header, README) updated: export
COMPOSE_ENV_FILES=defaults.env,.env, keep only secrets/overrides in .env.
2026-07-05 02:08:25 +00:00
Audiobuzz 688ff1aa88 fleet-manager: mirror the distroless runtime base through Harbor
Route the gcr.io distroless runtime image through the Harbor mirror
(harbor.bitasylum.org/gcr.io/distroless/static-debian12:nonroot) per the standing
rule, completing the fleet-manager Dockerfile's base-image mirroring.
2026-07-05 01:47:28 +00:00
Audiobuzz e6b1f0308e fleet-manager: finish self-TLS cleanup (compose, env, docs, delete acme/)
Follow the code removal through the rest of the tree now that Traefik owns UI TLS:
- compose: drop FLEET_MGR_ADMIN_BEHIND_PROXY + ADMIN_CERT/_KEY from the manager
  env; correct the ./pki mount comment (agent + broker certs only).
- .env.example: drop the BEHIND_PROXY + cert vars + self-TLS note.
- DEPLOY.md / README: remove the self-TLS "alternative" and the BEHIND_PROXY /
  ADMIN_CERT config rows.
- server-pki.sh: point the admin UI cert at Traefik, not ./pki/admin.
- delete serving/fleet-manager/acme/ (the retired ACME side-car).
2026-07-05 01:46:52 +00:00
Audiobuzz 76c3a6aaa7 fleet-manager: drop self-TLS admin mode — the manager never terminates TLS
Traefik (and the k8s Ingress later) owns UI TLS, so remove the vestigial
self-TLS path: adminTLS(), the certReloader type, the adminCert/adminKey config
+ FLEET_MGR_ADMIN_CERT/_KEY env, and the now-redundant adminBehindProxy flag.
The admin plane always serves plaintext now — two modes: INSECURE (dev, no auth)
or default (passkey behind a trusted TLS proxy). Drops the crypto/tls, fmt, sync
imports and the errString helper.
2026-07-05 01:44:23 +00:00
Audiobuzz 24d67d07db serving: mirror the remaining base/service images through Harbor
Route the pre-existing non-CI image pulls through harbor.bitasylum.org per the
standing rule (harbor.bitasylum.org/<registry>/<namespace>/<image>):
- enrol-broker/Dockerfile: golang:1.23-alpine -> dockerhub/library/...,
  smallstep/step-cli:latest -> dockerhub/smallstep/...
- dot1x-bench/docker-compose.yml: smallstep/step-ca + freeradius/freeradius-server
  -> dockerhub/<namespace>/...
CI-built git.bitasylum.org/audiobuzz/* images and ${OCI_IMAGE}/${IPXE_IMAGE}
type:image volume mounts are unchanged.
2026-07-05 01:34:10 +00:00
Audiobuzz 08847532ee fleet-manager: pull non-CI images through the Harbor mirror
Route the ad-hoc images I introduced through harbor.bitasylum.org (the standing
rule): backup.sh uses harbor.bitasylum.org/dockerhub/library/alpine:3.20 (via an
overridable ALPINE_IMAGE); the DEPLOY.md volume-chown fix + the goacme/lego prose
use the Harbor paths too. Compose (influxdb/traefik) + Dockerfile bases are already
harbor-mirrored; git.bitasylum.org/audiobuzz/* stays (CI-built).
2026-07-05 01:28:06 +00:00