v0.105.0: fork-4 offsite password custody — hand-off + atomicity gate + DR inject + coord
Pairs with agent v0.77.0. StageEscrowSecret pushes the repo password to the agent (POST /escrow/stage-secret) at offsite-enable → EscrowState="pending". Atomicity gate: RunOffboxBackup (scheduler + handler) refuses until EscrowState="escrowed" (operator POST /backup/offbox/confirm-escrow after the escrow ceremony) — no un-recoverable offsite ciphertext can exist. DR: POST /backup/offbox/inject-password pre-places a recovered 64-hex password 0600 (honored by WriteOffboxSecrets' IsNotExist guard; refuses clobber without force). DR recipe gains non-secret offsite_restic coords (DRResticCoord); SFTP key regenerated at DR, not escrowed. New settings.OffboxTarget.EscrowState. Tests + atomicity & inject companion red-proofs green; UI gates pass. NOT yet live-validated (supervised ceremony). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -81,13 +81,71 @@ func (s *Server) offboxConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
tgt.LastRun, tgt.LastStatus, tgt.LastError = prev.LastRun, prev.LastStatus, prev.LastError
|
||||
tgt.LastDuration, tgt.RepoSizeHuman, tgt.SnapshotCount = prev.LastDuration, prev.RepoSizeHuman, prev.SnapshotCount
|
||||
tgt.LastWarning = prev.LastWarning
|
||||
tgt.EscrowState = prev.EscrowState
|
||||
}
|
||||
// fork-4: enabling offsite stages the repo password to the agent for the R-escrow ceremony and marks
|
||||
// it PENDING — no offsite RUN proceeds until escrow is confirmed (atomicity). Re-editing an already
|
||||
// escrowed target keeps it escrowed (WriteOffboxSecrets leaves the password unchanged). A stage-push
|
||||
// failure does NOT mark escrowed; it is surfaced (the run gate still protects data).
|
||||
stageErr := ""
|
||||
if tgt.Enabled {
|
||||
if tgt.EscrowState != "escrowed" {
|
||||
tgt.EscrowState = "pending"
|
||||
}
|
||||
if client, cerr := s.agentClient(); cerr != nil {
|
||||
stageErr = " — a kulcs letéti előkészítése nem sikerült (az ügynök nem elérhető); próbáld újra."
|
||||
s.logger.Printf("[WARN] [web] offbox escrow stage: agent client: %v", cerr)
|
||||
} else if err := s.backupMgr.PushOffboxPasswordForEscrow(r.Context(), client.StageEscrowSecret); err != nil {
|
||||
stageErr = " — a kulcs letéti előkészítése nem sikerült; próbáld újra."
|
||||
s.logger.Printf("[WARN] [web] offbox escrow stage: %v", err) // err carries no secret
|
||||
}
|
||||
}
|
||||
if err := s.settings.SetOffboxTarget(tgt); err != nil {
|
||||
offboxRedirect(w, r, "A beállítás mentése sikertelen.", true)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] off-box target configured: %s@%s:%s (port %d, enabled=%v)", user, host, repoPath, port, tgt.Enabled)
|
||||
offboxRedirect(w, r, "A NAS mentési cél elmentve.", false)
|
||||
s.logger.Printf("[INFO] [web] off-box target configured: %s@%s:%s (port %d, enabled=%v, escrow=%s)", user, host, repoPath, port, tgt.Enabled, tgt.EscrowState)
|
||||
offboxRedirect(w, r, "A NAS mentési cél elmentve."+stageErr, stageErr != "")
|
||||
}
|
||||
|
||||
// offboxConfirmEscrowHandler marks the offsite repo password as escrowed under R (fork-4). The operator
|
||||
// calls this after a successful escrow-create ceremony; offsite runs stay gated until then. (The
|
||||
// provisioning task should replace this with a hub-verified auto-confirm to remove the operator-forgets/
|
||||
// operator-lies footgun.)
|
||||
func (s *Server) offboxConfirmEscrowHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
|
||||
offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
|
||||
return
|
||||
}
|
||||
if err := s.settings.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.EscrowState = "escrowed" }); err != nil {
|
||||
offboxRedirect(w, r, "A beállítás mentése sikertelen.", true)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] off-box escrow confirmed — offsite runs enabled")
|
||||
offboxRedirect(w, r, "A kulcs letétbe helyezése megerősítve — a NAS-mentés mostantól futhat.", false)
|
||||
}
|
||||
|
||||
// offboxInjectPasswordHandler pre-places a RECOVERED repo password at the offbox password path (fork-4 DR
|
||||
// seam) so a subsequent configure uses it and the existing offsite repo opens. Operator/DR only; the value
|
||||
// is never logged. Body: {password, force?}.
|
||||
func (s *Server) offboxInjectPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil {
|
||||
offboxRedirect(w, r, "A mentéskezelő nem elérhető.", true)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
pw := r.FormValue("password")
|
||||
force := r.FormValue("force") == "on" || r.FormValue("force") == "true"
|
||||
if strings.TrimSpace(pw) == "" {
|
||||
offboxRedirect(w, r, "A repo jelszó kötelező.", true)
|
||||
return
|
||||
}
|
||||
if err := s.backupMgr.InjectOffboxPassword(pw, force); err != nil {
|
||||
offboxRedirect(w, r, "A jelszó beállítása sikertelen: "+err.Error(), true)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] off-box repo password injected (DR pre-place, force=%v)", force)
|
||||
offboxRedirect(w, r, "A helyreállított repo jelszó beállítva.", false)
|
||||
}
|
||||
|
||||
// offboxToggleHandler flips an app's off-box inclusion.
|
||||
@@ -112,6 +170,11 @@ func (s *Server) offboxRunHandler(w http.ResponseWriter, r *http.Request) {
|
||||
offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
|
||||
return
|
||||
}
|
||||
// fork-4 atomicity: refuse the run until the repo password is escrowed under R.
|
||||
if !s.backupMgr.OffboxRunnable() {
|
||||
offboxRedirect(w, r, "A NAS-mentés a kulcs letétbe helyezésére vár.", true)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Hour)
|
||||
defer cancel()
|
||||
|
||||
Reference in New Issue
Block a user