hub v0.127.0: off-site key registrar (box never gets the storage password), password sealed at rest, daily key check, clean-up window (shipped off) — decisions 68-69, R-820/R-821/R-822
gates / gates (push) Successful in 29s
gates / gates (push) Successful in 29s
Part A evidence (migration spike, sftp-written repo through the pinned rclone key) and the hub red-proofs under documentation/audits/offsite-lock-build-2026-10-03/. Manifest bump follows after the image is built and Secret/offsite-secret-key exists. 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:
@@ -24,6 +24,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/offsite"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/offsiteheal"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/offsitekeys"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/pbsdrheal"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/poke"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
@@ -179,6 +180,26 @@ func main() {
|
||||
logger.Fatalf("[FATAL] Failed to initialize store: %v", err)
|
||||
}
|
||||
defer dataStore.Close()
|
||||
|
||||
// R-821 / decision 69 (v0.127.0): the off-site sub-account password is sealed at rest with a key
|
||||
// that is NOT in the database (Secret/offsite-secret-key). Without it the hub cannot store, read or
|
||||
// use any sub-account password — provisioning and the key registrar refuse (fail-closed), and the
|
||||
// legacy plaintext rows stay as they are until the key arrives.
|
||||
offsiteKeyReady := false
|
||||
if v := os.Getenv("OFFSITE_SECRET_KEY"); v == "" {
|
||||
logger.Printf("[ERROR] OFFSITE_SECRET_KEY unset — off-site passwords cannot be sealed; provisioning and the key registrar are DISABLED")
|
||||
} else if key, kerr := store.ParseOffsiteSecretKey(v); kerr != nil {
|
||||
logger.Printf("[ERROR] OFFSITE_SECRET_KEY invalid (%v) — provisioning and the key registrar are DISABLED", kerr)
|
||||
} else if kerr := dataStore.SetOffsiteSecretKey(key); kerr != nil {
|
||||
logger.Printf("[ERROR] OFFSITE_SECRET_KEY rejected (%v) — provisioning and the key registrar are DISABLED", kerr)
|
||||
} else {
|
||||
offsiteKeyReady = true
|
||||
if n, serr := dataStore.SealLegacyOffsiteSecrets(); serr != nil {
|
||||
logger.Printf("[ERROR] sealing legacy off-site secrets failed after %d row(s): %v", n, serr)
|
||||
} else {
|
||||
logger.Printf("[INFO] off-site secrets sealed at rest (%d legacy plaintext row(s) sealed now)", n)
|
||||
}
|
||||
}
|
||||
logger.Printf("[INFO] Database opened at %s", dbPath)
|
||||
|
||||
// Phase 2 managed updates: seed the global controller-version floor fallback (config/env). A
|
||||
@@ -363,6 +384,60 @@ func main() {
|
||||
// unconfigured or the customer has no offsite tier.
|
||||
apiHandler.SetOffsiteReissuer(webServer.ReissueOffsiteForCustomer)
|
||||
|
||||
// Decision 69 (v0.127.0): the off-site KEY REGISTRAR. The box sends its public key; the hub writes it
|
||||
// into the sub-account's authorized_keys pinned append-only; the daily check reads every file.
|
||||
if offsiteKeyReady {
|
||||
keySvc := &offsitekeys.Service{
|
||||
Store: dataStore, Reg: &offsitekeys.Registrar{Dialer: offsitekeys.SSHDialer{}}, Logger: logger,
|
||||
Emit: dispatcher.ProcessEvent,
|
||||
}
|
||||
apiHandler.SetOffsiteKeyService(keySvc)
|
||||
runKeyAudit := func(ctx context.Context) any {
|
||||
start := time.Now()
|
||||
out := keySvc.AuditAll(ctx, dataStore.OffsiteWindowOpen)
|
||||
logger.Printf("[INFO] off-site key check: %d sub-account(s) read in %s", len(out), time.Since(start).Round(time.Millisecond))
|
||||
type row struct {
|
||||
Customer string `json:"customer"`
|
||||
Lines int `json:"lines"`
|
||||
Pinned int `json:"pinned"`
|
||||
Findings []offsitekeys.Finding `json:"findings"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
var rows []row
|
||||
for _, o := range out {
|
||||
rr := row{Customer: o.CustomerID, Lines: o.Result.Lines, Pinned: o.Result.Pinned, Findings: o.Result.Findings}
|
||||
if o.Err != nil {
|
||||
rr.Error = o.Err.Error()
|
||||
}
|
||||
rows = append(rows, rr)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
webServer.SetOffsiteKeyAudit(runKeyAudit)
|
||||
webServer.SetOffsiteWindowAdmin(dataStore.GrantOffsiteWindowOnce, dataStore.SetOffsiteWindowsEnabled)
|
||||
// Decision 68: a window the box never closed is closed by the hub at its 20-minute bound.
|
||||
go func() {
|
||||
tk := time.NewTicker(60 * time.Second)
|
||||
defer tk.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tk.C:
|
||||
sctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
keySvc.SweepExpiredWindows(sctx)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
go scheduleDaily(ctx, "offsite-key-audit", "07:10", func() {
|
||||
actx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
runKeyAudit(actx)
|
||||
}, logger)
|
||||
logger.Printf("[INFO] Off-site key registrar enabled; daily key check at 07:10 Budapest")
|
||||
}
|
||||
|
||||
// Direction-2 immediate-sync (v0.58.0): one in-memory operator-intent notifier, shared by the
|
||||
// web handlers (which Bump it after every intent write) and the API handler (which long-polls it
|
||||
// at GET /api/v1/wait). In-memory BY DESIGN — a restart resets generations to zero; the box
|
||||
|
||||
Reference in New Issue
Block a user