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:
+54
-10
@@ -65,16 +65,21 @@ type Server struct {
|
||||
versionChecker *VersionChecker
|
||||
templateFetcher *TemplateFetcher
|
||||
assetsMgr *assets.Manager
|
||||
gitea *gitea.Client // optional; enables the Day-0 artifact version dropdowns
|
||||
offsite *offsite.Provisioner // optional; enables Hetzner offsite provisioning (SLICE 1)
|
||||
offsiteBox func() (monitor.BoxSnapshot, bool) // optional (v0.64.0, R-5); the restic pool-box aggregate snapshot accessor
|
||||
pbsdrBox func() (monitor.PBSBoxSnapshot, bool) // optional (v0.65.0, R-5); the PBS-DR datastore fill snapshot accessor
|
||||
tenantsync tenancyProvisioner // optional; enables PBS DR tier provisioning (web/pbsdr.go)
|
||||
claimEngine *claim.Engine // optional; enables the customer-claim resend button (v0.50.0)
|
||||
selfBindMailer SelfBindMailer // optional; enables the customer self-bind link button (v0.66.0, R-27)
|
||||
bindLimiter *bindRateLimiter // per-IP throttle for the PUBLIC /bind/ surface (v0.66.0, R-27)
|
||||
bindResendMu sync.Mutex // R-719: the fresh-link resend limiter
|
||||
bindResendAt map[string]time.Time // customer → last fresh-link mail (R-719)
|
||||
gitea *gitea.Client // optional; enables the Day-0 artifact version dropdowns
|
||||
offsite *offsite.Provisioner // optional; enables Hetzner offsite provisioning (SLICE 1)
|
||||
// offsiteKeyAudit runs the daily off-site key check on demand (decision 69). nil → 503.
|
||||
offsiteKeyAudit func(ctx context.Context) any
|
||||
// offsiteWindowAdmin: operator one-shot grant / weekly switch (decision 68). nil → 503.
|
||||
offsiteWindowGrant func(customerID string) error
|
||||
offsiteWindowSwitch func(on bool) error
|
||||
offsiteBox func() (monitor.BoxSnapshot, bool) // optional (v0.64.0, R-5); the restic pool-box aggregate snapshot accessor
|
||||
pbsdrBox func() (monitor.PBSBoxSnapshot, bool) // optional (v0.65.0, R-5); the PBS-DR datastore fill snapshot accessor
|
||||
tenantsync tenancyProvisioner // optional; enables PBS DR tier provisioning (web/pbsdr.go)
|
||||
claimEngine *claim.Engine // optional; enables the customer-claim resend button (v0.50.0)
|
||||
selfBindMailer SelfBindMailer // optional; enables the customer self-bind link button (v0.66.0, R-27)
|
||||
bindLimiter *bindRateLimiter // per-IP throttle for the PUBLIC /bind/ surface (v0.66.0, R-27)
|
||||
bindResendMu sync.Mutex // R-719: the fresh-link resend limiter
|
||||
bindResendAt map[string]time.Time // customer → last fresh-link mail (R-719)
|
||||
// intentHub (v0.58.0, Direction-2 immediate-sync) is Bumped by every operator-intent handler
|
||||
// (config save/delete, claim resend, offsite re-issue/freeze, floor, block/unblock, log pull)
|
||||
// so a box long-polling GET /api/v1/wait wakes in seconds. Shared with the API handler. nil =
|
||||
@@ -194,6 +199,14 @@ func (s *Server) SetAssetManager(am *assets.Manager) {
|
||||
// offsite enabled returns an error (offsite not configured on this hub).
|
||||
func (s *Server) SetOffsiteProvisioner(p *offsite.Provisioner) { s.offsite = p }
|
||||
|
||||
// SetOffsiteKeyAudit wires the on-demand run of the daily off-site key check (decision 69).
|
||||
func (s *Server) SetOffsiteKeyAudit(fn func(ctx context.Context) any) { s.offsiteKeyAudit = fn }
|
||||
|
||||
// SetOffsiteWindowAdmin wires the operator's window controls (decision 68).
|
||||
func (s *Server) SetOffsiteWindowAdmin(grant func(string) error, sw func(bool) error) {
|
||||
s.offsiteWindowGrant, s.offsiteWindowSwitch = grant, sw
|
||||
}
|
||||
|
||||
// SetOffsiteBox wires the pool-box aggregate snapshot accessor (v0.64.0, R-5): the checker's cached
|
||||
// snapshot, read on the Offsite tab + the Dashboard tile. nil (no HETZNER_TOKEN/box id) → both render an
|
||||
// honest "not configured". The web layer NEVER fetches from Hetzner — it only reads this cache.
|
||||
@@ -604,6 +617,37 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
s.handleConfigEditForm(w, r, customerID)
|
||||
}
|
||||
case strings.HasPrefix(path, "/offsite/window-grant/") || path == "/offsite/windows-enabled":
|
||||
// Operator (decision 68): a one-shot grant lets the customer's NEXT window request through;
|
||||
// the switch turns the WEEKLY window on/off fleet-wide (off = the interim: nothing prunes).
|
||||
if r.Method != http.MethodPost || s.offsiteWindowGrant == nil {
|
||||
http.Error(w, "unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if path == "/offsite/windows-enabled" {
|
||||
err = s.offsiteWindowSwitch(r.FormValue("on") == "1")
|
||||
} else {
|
||||
err = s.offsiteWindowGrant(strings.TrimPrefix(path, "/offsite/window-grant/"))
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte("{\"ok\":true}\n"))
|
||||
case path == "/offsite/key-audit":
|
||||
// Operator: run the daily off-site key check now (decision 69). Same code path as the 07:10 job.
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if s.offsiteKeyAudit == nil {
|
||||
http.Error(w, "off-site key check not configured", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(s.offsiteKeyAudit(r.Context()))
|
||||
case strings.HasPrefix(path, "/configs/") && strings.HasSuffix(path, "/offsite-reissue"):
|
||||
customerID := strings.TrimPrefix(path, "/configs/")
|
||||
customerID = strings.TrimSuffix(customerID, "/offsite-reissue")
|
||||
|
||||
Reference in New Issue
Block a user