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
3go 1.27.0 3go 1.27.0
4 4
5require ( 5require (
6 filippo.io/age v1.3.2
6 github.com/BurntSushi/toml v1.6.0 7 github.com/BurntSushi/toml v1.6.0
7 github.com/ProtonMail/go-crypto v1.5.1 8 github.com/ProtonMail/go-crypto v1.5.1
8 github.com/alecthomas/chroma/v2 v2.27.0 9 github.com/alecthomas/chroma/v2 v2.27.0
@@ -21,6 +22,7 @@ require (
21) 22)
22 23
23require ( 24require (
25 filippo.io/hpke v0.4.0 // indirect
24 github.com/aymerick/douceur v0.2.0 // indirect 26 github.com/aymerick/douceur v0.2.0 // indirect
25 github.com/cloudflare/circl v1.6.3 // indirect 27 github.com/cloudflare/circl v1.6.3 // indirect
26 github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 28 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=
1github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= 7github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
2github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 8github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
3github.com/ProtonMail/go-crypto v1.5.1 h1:pTrLDQHyOT8y3DFYIpijgPBTw/7E2GLMimutvOlceuE= 9github.com/ProtonMail/go-crypto v1.5.1 h1:pTrLDQHyOT8y3DFYIpijgPBTw/7E2GLMimutvOlceuE=
internal/config/config.go +27
@@ -14,6 +14,7 @@ import (
14 "strings" 14 "strings"
15 "time" 15 "time"
16 16
17 "filippo.io/age"
17 "github.com/BurntSushi/toml" 18 "github.com/BurntSushi/toml"
18) 19)
19 20
@@ -40,6 +41,7 @@ type Config struct {
40 Deps Deps `toml:"deps"` 41 Deps Deps `toml:"deps"`
41 Retention Retention `toml:"retention"` 42 Retention Retention `toml:"retention"`
42 Push Push `toml:"push"` 43 Push Push `toml:"push"`
44 Backup Backup `toml:"backup"`
43 // GoImport maps vanity Go module paths to repositories, e.g. 45 // GoImport maps vanity Go module paths to repositories, e.g.
44 // "gitbay.org/gitbay" = "krz/gitbay". Requests carrying ?go-get=1 46 // "gitbay.org/gitbay" = "krz/gitbay". Requests carrying ?go-get=1
45 // under a mapped path get a go-import meta tag. 47 // under a mapped path get a go-import meta tag.
@@ -297,6 +299,27 @@ func LoadAPNSKey(path string) (*ecdsa.PrivateKey, error) {
297 return key, nil 299 return key, nil
298} 300}
299 301
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
300// Default returns the configuration used when a key is absent from the file. 323// Default returns the configuration used when a key is absent from the file.
301func Default() Config { 324func Default() Config {
302 return Config{ 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 // Contradictions. 509 // Contradictions.
483 if c.Mail.SMTPHost != "" && c.Mail.From == "" { 510 if c.Mail.SMTPHost != "" && c.Mail.From == "" {
484 errs = append(errs, errors.New("[mail] from is required when smtp_host is set")) 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 "path/filepath" 10 "path/filepath"
11 "strings" 11 "strings"
12 "testing" 12 "testing"
13
14 "filippo.io/age"
13) 15)
14 16
15func writeConfig(t *testing.T, body string) string { 17func writeConfig(t *testing.T, body string) string {
@@ -366,3 +368,24 @@ func TestSecretKeyFileSymlinks(t *testing.T) {
366 } 368 }
367 }) 369 })
368} 370}
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}