Keep backups where the control plane can see them

The first backup on the live control plane was written, and the control
plane reported none: /var/backups/llmgw already existed, made by an earlier
move to PostgreSQL for its own dumps, owned by postgres and 0700. MkdirAll
leaves an existing directory as it is, so the archive landed where the
llmgw user could not list it, and llmgw_backup_last_success_timestamp_seconds
said 0.

Opening that directory up would have exposed the dumps already in it, so
backups get their own, /var/backups/llmgw-server. And `llmgw server backup`
now warns when the directory it wrote to is one the control plane cannot
list, rather than leaving that to be found from an alert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-09-30 01:50:33 +02:00
1 parent 1da4a2e1ef
commit c3a4150eab
6 files changed
+40 -7

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=llmgw backup: the store and /etc/llmgw, to /var/backups/llmgw
Description=llmgw backup: the store and /etc/llmgw, to /var/backups/llmgw-server
After=llmgw-server.service postgresql.service
[Service]
+3 -3
View File
@@ -973,7 +973,7 @@ cannot be rebuilt without:
certificate authority's key (`pki/`). Nothing in llmgw can reconstruct that
key; losing it means reissuing every node's identity.
Archives go to `/var/backups/llmgw`, readable by root only because of the CA
Archives go to `/var/backups/llmgw-server`, readable by root only because of the CA
key, and the newest 14 are kept. Run it nightly with the timer:
```
@@ -981,7 +981,7 @@ sudo install -m 0644 deploy/systemd/llmgw-backup.service deploy/systemd/llmgw-ba
sudo systemctl daemon-reload
sudo systemctl enable --now llmgw-backup.timer
sudo systemctl start llmgw-backup.service # one now, to see it work
ls -l /var/backups/llmgw
ls -l /var/backups/llmgw-server
```
The control plane publishes when the newest archive was written as
@@ -1006,7 +1006,7 @@ them:
```
sudo systemctl stop llmgw-server
mkdir /tmp/restore && sudo tar -xzf /var/backups/llmgw/llmgw-backup-<host>-<time>.tar.gz -C /tmp/restore
mkdir /tmp/restore && sudo tar -xzf /var/backups/llmgw-server/llmgw-backup-<host>-<time>.tar.gz -C /tmp/restore
cat /tmp/restore/MANIFEST # version and time it was taken
# /etc/llmgw: configuration, secrets, certificates and the CA
+1 -1
View File
@@ -1272,7 +1272,7 @@ one leaves that engine to be installed by hand.
BackupDir is where `llmgw server backup` writes, and where the newest
archive's age is read for llmgw_backup_last_success_timestamp_seconds.
Empty means /var/backups/llmgw.
Empty means /var/backups/llmgw-server.
### `router`
+22 -1
View File
@@ -25,7 +25,11 @@ import (
// DefaultBackupDir is where backups are written and where the control plane
// looks for the newest one.
const DefaultBackupDir = "/var/backups/llmgw"
//
// Its own directory rather than /var/backups/llmgw, which a PostgreSQL move
// had already made for its own dumps, owned by postgres and closed to
// everyone else — so the control plane could not see a backup landing in it.
const DefaultBackupDir = "/var/backups/llmgw-server"
func newBackupCmd() *cobra.Command {
var (
@@ -56,6 +60,10 @@ func newBackupCmd() *cobra.Command {
if err != nil {
return err
}
if why := unlistable(dir); why != "" {
fmt.Fprintf(cmd.ErrOrStderr(), "warning: %s; the control plane will report no backup "+
"and BackupTooOld will fire\n", why)
}
removed, err := pruneBackups(dir, keep)
if err != nil {
return err
@@ -287,3 +295,16 @@ func NewestBackup(dir string) time.Time {
}
return newest
}
// unlistable says why the control plane, which does not run as root, could
// not see the archives in dir, or "" when it can.
func unlistable(dir string) string {
info, err := os.Stat(dir)
if err != nil {
return err.Error()
}
if info.Mode().Perm()&0o005 != 0o005 {
return fmt.Sprintf("%s is %v, so only its owner can list it", dir, info.Mode().Perm())
}
return ""
}
+12
View File
@@ -104,3 +104,15 @@ func TestOldBackupsArePrunedAndTheNewestIsFound(t *testing.T) {
t.Error("a missing directory reported a backup")
}
}
func TestABackupDirectoryTheControlPlaneCannotListIsSaid(t *testing.T) {
dir := t.TempDir()
_ = os.Chmod(dir, 0o700)
if unlistable(dir) == "" {
t.Error("a directory only its owner can list was taken as visible")
}
_ = os.Chmod(dir, 0o755)
if why := unlistable(dir); why != "" {
t.Errorf("a listable directory: %s", why)
}
}
+1 -1
View File
@@ -83,7 +83,7 @@ type Server struct {
// BackupDir is where `llmgw server backup` writes, and where the newest
// archive's age is read for llmgw_backup_last_success_timestamp_seconds.
// Empty means /var/backups/llmgw.
// Empty means /var/backups/llmgw-server.
BackupDir string `koanf:"backup_dir"`
Router Router `koanf:"router"`