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
33go 1.27.0
44
55require (
6 filippo.io/age v1.3.2
67 github.com/BurntSushi/toml v1.6.0
78 github.com/ProtonMail/go-crypto v1.5.1
89 github.com/alecthomas/chroma/v2 v2.27.0
@@ -21,6 +22,7 @@ require (
2122)
2223
2324require (
25 filippo.io/hpke v0.4.0 // indirect
2426 github.com/aymerick/douceur v0.2.0 // indirect
2527 github.com/cloudflare/circl v1.6.3 // indirect
2628 github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
go.sum +6
@@ -1,3 +1,9 @@
1c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs=
2c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
3filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c=
4filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk=
5filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
6filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
17github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
28github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
39github.com/ProtonMail/go-crypto v1.5.1 h1:pTrLDQHyOT8y3DFYIpijgPBTw/7E2GLMimutvOlceuE=
internal/config/config.go +27
@@ -14,6 +14,7 @@ import (
1414 "strings"
1515 "time"
1616
17 "filippo.io/age"
1718 "github.com/BurntSushi/toml"
1819)
1920
@@ -40,6 +41,7 @@ type Config struct {
4041 Deps Deps `toml:"deps"`
4142 Retention Retention `toml:"retention"`
4243 Push Push `toml:"push"`
44 Backup Backup `toml:"backup"`
4345 // GoImport maps vanity Go module paths to repositories, e.g.
4446 // "gitbay.org/gitbay" = "krz/gitbay". Requests carrying ?go-get=1
4547 // under a mapped path get a go-import meta tag.
@@ -297,6 +299,27 @@ func LoadAPNSKey(path string) (*ecdsa.PrivateKey, error) {
297299 return key, nil
298300}
299301
302// Backup configures gitbayd admin backup.
303type 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.
311func (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
300323// Default returns the configuration used when a key is absent from the file.
301324func Default() Config {
302325 return Config{
@@ -479,6 +502,10 @@ func (c Config) Validate() error {
479502 }
480503 }
481504
505 if _, err := c.Backup.Recipients(); err != nil {
506 errs = append(errs, err)
507 }
508
482509 // Contradictions.
483510 if c.Mail.SMTPHost != "" && c.Mail.From == "" {
484511 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 (
1010 "path/filepath"
1111 "strings"
1212 "testing"
13
14 "filippo.io/age"
1315)
1416
1517func writeConfig(t *testing.T, body string) string {
@@ -366,3 +368,24 @@ func TestSecretKeyFileSymlinks(t *testing.T) {
366368 }
367369 })
368370}
371
372func 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}