backup: the panel's archive is the command's, private key included
Owner, 2026-10-11: the private key is needed, and a backup from the panel and one from the console have to be the same. The commit that made the panel's backup work skipped a file under certs/ the panel could not open - on the test server the private key, which the certificate hook leaves to root and Postfix's group. That gave an archive with a certificate and no key, and two kinds of archive. - Params.Unprivileged is gone. Nothing is left out on account of who makes the archive; the queue is out for everyone (previous commit). - When the panel cannot read a file under certs/, backup.Check says so before the download and the panel refuses with what to do on the host: chgrp 998 certs/privkey.pem && chmod 640. backup.UnreadableCert tells that case from any other failure. - 998 is the container's selfpost group, which holds the panel and Postfix. Its id was whatever groupadd gave; the Dockerfile now fixes it at the value it has always had, because an operator addresses it by number from the host. - Guide: method 1 is the same archive as method 2, with the permission the key needs and a note for whatever renews the certificate. Help and the box follow the design commit before this one. - e2e: the restore scenario first makes the key unreadable and expects the refusal naming the file and the way out, then takes the backup and expects the key in it; nothing is put back by hand before the restart. go vet ./internal/backup/ ./internal/web/handlers/: ok go test ./internal/backup/ ./cmd/... ./internal/web/...: ok make e2e on the test server: ok Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,14 +70,11 @@ func (h *Handlers) HandleBackup(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderBackupPageWith(w, r, http.StatusBadRequest, "", pwErr)
|
||||
return
|
||||
}
|
||||
// The panel is not root: its archive leaves out the Postfix queue and a
|
||||
// certificate file it may not open (backup.Params.Unprivileged).
|
||||
params := backup.Params{
|
||||
DataDir: h.cfg.DataDir,
|
||||
DBPath: h.cfg.DBPath,
|
||||
Version: h.cfg.Version,
|
||||
DeployRoot: h.cfg.DeployRoot,
|
||||
Unprivileged: true,
|
||||
DataDir: h.cfg.DataDir,
|
||||
DBPath: h.cfg.DBPath,
|
||||
Version: h.cfg.Version,
|
||||
DeployRoot: h.cfg.DeployRoot,
|
||||
OnWarn: func(msg string) {
|
||||
logf("panel: full backup: %s", msg)
|
||||
},
|
||||
@@ -90,11 +87,11 @@ func (h *Handlers) HandleBackup(w http.ResponseWriter, r *http.Request) {
|
||||
// Everything is read through once before the first byte goes out. Past the
|
||||
// headers a failure can only cut the download short - which is what the
|
||||
// button did from 1.3.0 to 2.0.0, with a 200 and ten bytes, because the
|
||||
// queue was not the panel's to read.
|
||||
// queue was not the panel's to read. Nothing is skipped to make the archive
|
||||
// possible: it is the one selfpost-backup makes, or it is refused.
|
||||
if err := backup.Check(params); err != nil {
|
||||
logf("panel: full backup: %v", err)
|
||||
h.renderBackupPageWith(w, r, http.StatusInternalServerError, "",
|
||||
"Could not read everything a backup holds. Nothing was downloaded; the reason is in the panel's log.")
|
||||
h.renderBackupPageWith(w, r, http.StatusInternalServerError, "", unreadableBackupErr(err, h.cfg.DeployRoot))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,6 +330,18 @@ func decryptErrorMessage(err error) string {
|
||||
}
|
||||
}
|
||||
|
||||
// unreadableBackupErr phrases what backup.Check found. The usual case is the
|
||||
// private key: the panel runs as its own user, and a key readable by root
|
||||
// alone is not its to read. The way out is on the host, so the message says
|
||||
// what to do there (guide § Taking a full backup).
|
||||
func unreadableBackupErr(err error, deployRoot string) string {
|
||||
if name, ok := backup.UnreadableCert(err, deployRoot); ok {
|
||||
return "The panel is not allowed to read " + name + ", and a backup without it would not bring the server back. " +
|
||||
"On the host, give the file to group 998 and make it group-readable (chgrp 998, chmod 640), then try again. Nothing was downloaded."
|
||||
}
|
||||
return "Could not read everything a backup holds. Nothing was downloaded; the reason is in the panel's log."
|
||||
}
|
||||
|
||||
// deployBackupErr phrases a pre-flight backup failure for the operator.
|
||||
func deployBackupErr(err error) string {
|
||||
if strings.Contains(err.Error(), "DeployRoot") || strings.Contains(err.Error(), "deploy root") {
|
||||
|
||||
Reference in New Issue
Block a user