hub v0.135.0: CSRF on the Basic-auth path (R-135), console passwords sealed at rest (R-133), boxes left behind listed and alarmed (R-604, R-530), no-e-mail banner (R-508)
gates / gates (push) Successful in 29s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 11:12:56 +02:00
parent 9bb45eaaa2
commit 3d7a2761fc
24 changed files with 1148 additions and 17 deletions
+70
View File
@@ -8,6 +8,8 @@ import (
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/osupdates"
"gitea.dooplex.hu/admin/felhom-hub/internal/semver"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
"gitea.dooplex.hu/admin/felhom-hub/internal/sysfacts"
)
@@ -35,6 +37,7 @@ type systemRow struct {
Held, RebootSince, KernelPanic, Oops cell
CrashRestarts24h, Guard cell
Bundle cell // R-840: the root-owned config bundle
Agent cell // R-530: the box's agent against the vouched one
// guest
GuestDebian, GuestRelease, GuestPending, GuestRestart cell
// docker
@@ -51,9 +54,62 @@ type OSSystemView interface {
Candidates() []osupdates.Status
Thresholds() (stale, reboot, notCovered time.Duration)
BundleThreshold() time.Duration
AgentThreshold() time.Duration
ApproveDocker() (string, error)
}
// agentCell is the "Agent" cell (R-530, hub v0.135.0): the box's agent against the vouched one, how far behind and
// since when. Amber while behind; red from the alarm's wait on (the operator alarm fires then). An unreadable
// version is "unknown", never a guess; nothing vouched → the version alone.
func agentCell(boxAgent, vouched string, since time.Time, after time.Duration, now time.Time) cell {
if !semver.Valid(boxAgent) {
return unknownCell("")
}
c := cell{Text: boxAgent}
if !semver.Valid(vouched) {
c.Title = "no vouched agent to compare with"
return c
}
if semver.Compare(boxAgent, vouched) >= 0 {
c.Title = "current (vouched " + vouched + ")"
return c
}
c.Class = "warn"
c.Text = boxAgent + " → " + vouched
c.Title = osupdates.ReleasesBehind(boxAgent, vouched) + " — sign an agent_update for this box"
if !since.IsZero() {
c.Text += " (since " + since.UTC().Format("2006-01-02") + ")"
if now.Sub(since) >= after {
c.Class = "bad"
}
}
return c
}
// floorRow is one line of the System page's "Version floors" table (R-604).
type floorRow struct {
CustomerID, CustomerName, Version string
Age cell
HeldBack bool // the override is BELOW the global floor: the global does not move this box
}
func buildFloorRows(ovs []store.CustomerFloorOverride, global string, now time.Time) []floorRow {
var out []floorRow
for _, o := range ovs {
r := floorRow{CustomerID: o.CustomerID, CustomerName: o.CustomerName, Version: o.Version}
if o.SetAt.IsZero() {
r.Age = cell{Text: "unknown", Class: "warn", Title: "set before hub v0.135.0 — the hub did not record when"}
} else {
r.Age = plain(ago(o.SetAt, now) + " (" + o.SetAt.UTC().Format("2006-01-02") + ")")
}
if semver.Valid(global) && semver.Valid(o.Version) && semver.Compare(global, o.Version) > 0 {
r.HeldBack = true
}
out = append(out, r)
}
return out
}
func plain(s string) cell { return cell{Text: s} }
// bundleCell is the "Root files" cell (R-840): the box's config bundle against the vouched agent's. Amber while behind,
@@ -240,12 +296,26 @@ func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) {
stale, reboot, notCov := view.Thresholds()
rows := buildSystemRows(lines, facts, names, stale, reboot, notCov, time.Now())
man := s.store.GetArtifactManifest()
agents := map[string]string{}
for _, h := range hosts {
agents[h.HostID] = h.AgentVersion
}
for i := range rows {
rows[i].Bundle = bundleCell(facts[rows[i].HostID], man.AgentVersion, man.BundleSHA256,
s.store.BundleBehindSince(rows[i].HostID), view.BundleThreshold(), time.Now())
rows[i].Agent = agentCell(agents[rows[i].HostID], man.AgentVersion,
s.store.AgentBehindSince(rows[i].HostID), view.AgentThreshold(), time.Now())
}
global := s.store.GetGlobalMinControllerVersion()
ovs, oerr := s.store.CustomerFloorOverrides()
if oerr != nil {
s.logger.Printf("[ERROR] system page: floor overrides: %v", oerr)
}
data := map[string]interface{}{
"Rows": rows,
"GlobalFloor": global,
"VouchedAgent": man.AgentVersion,
"Floors": buildFloorRows(ovs, global, time.Now()),
"Releases": view.Releases(),
"Cancelled": view.CancelledReleases(),
"Candidates": view.Candidates(),