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") {
|
||||
|
||||
@@ -193,7 +193,8 @@ var serverTopics = []HelpTopic{
|
||||
". The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, "+
|
||||
"so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server "+
|
||||
"starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in ", Code("certs/"),
|
||||
" is usually missing too, because the panel is not allowed to read it; ", Code("selfpost-backup"), " run in the container takes the key. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as ", Code(".spbk"),
|
||||
" has to be readable by the panel: give it to group 998 on the host (", Code("chgrp 998"), ", ", Code("chmod 640"),
|
||||
"). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as ", Code(".spbk"),
|
||||
", the password is not stored, and without it nothing can be recovered. To restore, unpack it into an empty project directory (decrypt a ",
|
||||
Code(".spbk"), " first with ", Code("selfpost-backup -decrypt"),
|
||||
") and start the same SelfPost version that made it; the panel refuses data from another version. The reverse-proxy configuration and ",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="column is-6">
|
||||
{{template "box_open" .Full}}
|
||||
{{template "body_open"}}
|
||||
<p class="mb-3"><code>data/</code> (database, DKIM keys, credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code> in one archive. The Postfix queue is left out: it cannot be copied reliably while the server is running. So is a certificate file the panel may not read. Restore into an empty project directory and start the <strong>same SelfPost version</strong>. The reverse-proxy vhost is not included.</p>
|
||||
<p class="mb-3"><code>data/</code> (database, DKIM keys, credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code> in one archive. The Postfix queue is left out: it cannot be copied reliably while the server is running. Restore into an empty project directory and start the <strong>same SelfPost version</strong>. The reverse-proxy vhost is not included.</p>
|
||||
<form class="sp-form" method="post" action="{{.FullAction}}">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="encrypt" value="1"{{if .Encrypt}} checked{{end}} data-encrypt-toggle> Encrypt with a password (<code>.spbk</code>)</label></div>
|
||||
<div data-encrypt-fields>
|
||||
|
||||
Reference in New Issue
Block a user