Commit 03e5ee3161
03e5ee3161bdeff7a3b87d3566dbe9fbe6ff67f1
parent: 7e5de5325c
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-28 21:21 UTC
config: [backup] age_recipients (filippo.io/age v1.3.2)
Ref #274
Layout: unified · split
go.mod
+2
| @@ -3,6 +3,7 @@ module gitbay.org/gitbay |
| 3 | 3 | go 1.27.0 |
| 4 | 4 | |
| 5 | 5 | require ( |
| 6 | filippo.io/age v1.3.2 |
| 6 | 7 | github.com/BurntSushi/toml v1.6.0 |
| 7 | 8 | github.com/ProtonMail/go-crypto v1.5.1 |
| 8 | 9 | github.com/alecthomas/chroma/v2 v2.27.0 |
| @@ -21,6 +22,7 @@ require ( |
| 21 | 22 | ) |
| 22 | 23 | |
| 23 | 24 | require ( |
| 25 | filippo.io/hpke v0.4.0 // indirect |
| 24 | 26 | github.com/aymerick/douceur v0.2.0 // indirect |
| 25 | 27 | github.com/cloudflare/circl v1.6.3 // indirect |
| 26 | 28 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect |
go.sum
+6
| @@ -1,3 +1,9 @@ |
| 1 | c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs= |
| 2 | c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo= |
| 3 | filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c= |
| 4 | filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk= |
| 5 | filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= |
| 6 | filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= |
| 1 | 7 | github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= |
| 2 | 8 | github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= |
| 3 | 9 | github.com/ProtonMail/go-crypto v1.5.1 h1:pTrLDQHyOT8y3DFYIpijgPBTw/7E2GLMimutvOlceuE= |
internal/config/config.go
+27
| @@ -14,6 +14,7 @@ import ( |
| 14 | 14 | "strings" |
| 15 | 15 | "time" |
| 16 | 16 | |
| 17 | "filippo.io/age" |
| 17 | 18 | "github.com/BurntSushi/toml" |
| 18 | 19 | ) |
| 19 | 20 | |
| @@ -40,6 +41,7 @@ type Config struct { |
| 40 | 41 | Deps Deps `toml:"deps"` |
| 41 | 42 | Retention Retention `toml:"retention"` |
| 42 | 43 | Push Push `toml:"push"` |
| 44 | Backup Backup `toml:"backup"` |
| 43 | 45 | // GoImport maps vanity Go module paths to repositories, e.g. |
| 44 | 46 | // "gitbay.org/gitbay" = "krz/gitbay". Requests carrying ?go-get=1 |
| 45 | 47 | // under a mapped path get a go-import meta tag. |
| @@ -297,6 +299,27 @@ func LoadAPNSKey(path string) (*ecdsa.PrivateKey, error) { |
| 297 | 299 | return key, nil |
| 298 | 300 | } |
| 299 | 301 | |
| 302 | // Backup configures gitbayd admin backup. |
| 303 | type Backup struct { |
| 304 | // AgeRecipients, when set, encrypts every archive to these age |
| 305 | // public keys (age1...). The matching identities stay off the host, |
| 306 | // so the host writes archives it cannot read. |
| 307 | AgeRecipients []string `toml:"age_recipients"` |
| 308 | } |
| 309 | |
| 310 | // Recipients parses AgeRecipients. |
| 311 | func (b Backup) Recipients() ([]age.Recipient, error) { |
| 312 | var rs []age.Recipient |
| 313 | for _, s := range b.AgeRecipients { |
| 314 | r, err := age.ParseX25519Recipient(s) |
| 315 | if err != nil { |
| 316 | return nil, fmt.Errorf("backup.age_recipients: %q: %w", s, err) |
| 317 | } |
| 318 | rs = append(rs, r) |
| 319 | } |
| 320 | return rs, nil |
| 321 | } |
| 322 | |
| 300 | 323 | // Default returns the configuration used when a key is absent from the file. |
| 301 | 324 | func Default() Config { |
| 302 | 325 | return Config{ |
| @@ -479,6 +502,10 @@ func (c Config) Validate() error { |
| 479 | 502 | } |
| 480 | 503 | } |
| 481 | 504 | |
| 505 | if _, err := c.Backup.Recipients(); err != nil { |
| 506 | errs = append(errs, err) |
| 507 | } |
| 508 | |
| 482 | 509 | // Contradictions. |
| 483 | 510 | if c.Mail.SMTPHost != "" && c.Mail.From == "" { |
| 484 | 511 | errs = append(errs, errors.New("[mail] from is required when smtp_host is set")) |
internal/config/config_test.go
+23
| @@ -10,6 +10,8 @@ import ( |
| 10 | 10 | "path/filepath" |
| 11 | 11 | "strings" |
| 12 | 12 | "testing" |
| 13 | |
| 14 | "filippo.io/age" |
| 13 | 15 | ) |
| 14 | 16 | |
| 15 | 17 | func writeConfig(t *testing.T, body string) string { |
| @@ -366,3 +368,24 @@ func TestSecretKeyFileSymlinks(t *testing.T) { |
| 366 | 368 | } |
| 367 | 369 | }) |
| 368 | 370 | } |
| 371 | |
| 372 | func TestBackupRecipients(t *testing.T) { |
| 373 | id, err := age.GenerateX25519Identity() |
| 374 | if err != nil { |
| 375 | t.Fatal(err) |
| 376 | } |
| 377 | cfg, err := Load(writeConfig(t, minimal+"[backup]\nage_recipients = [\""+id.Recipient().String()+"\"]\n")) |
| 378 | if err != nil { |
| 379 | t.Fatal(err) |
| 380 | } |
| 381 | rs, err := cfg.Backup.Recipients() |
| 382 | if err != nil || len(rs) != 1 { |
| 383 | t.Fatalf("Recipients = %v, %v", rs, err) |
| 384 | } |
| 385 | if _, err := Load(writeConfig(t, minimal+"[backup]\nage_recipients = [\"age1notakey\"]\n")); err == nil || !strings.Contains(err.Error(), "backup.age_recipients") { |
| 386 | t.Fatalf("a malformed recipient: %v", err) |
| 387 | } |
| 388 | if cfg, err := Load(writeConfig(t, minimal)); err != nil || len(cfg.Backup.AgeRecipients) != 0 { |
| 389 | t.Fatalf("default: %v, %v", cfg.Backup, err) |
| 390 | } |
| 391 | } |