Commit 1481a12712

1481a12712110d6489f4e6485040e10d6fc0e8a7

parent: 24b9cdbca1

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 09:09 UTC

e2e: a key file per instance; the archive carries secrets sealed and no key

Ref #273

Layout: unified · split

e2e/acme_test.go +2 −1
@@ -29,6 +29,7 @@ func TestACMEServe(t *testing.T) {
29[server] 29[server]
30root = %q 30root = %q
31site_url = "https://gitbay.example" 31site_url = "https://gitbay.example"
32secret_key_file = %q
32[ssh] 33[ssh]
33port = %d 34port = %d
34[http] 35[http]
@@ -36,7 +37,7 @@ addr = "127.0.0.1:%d"
36tls = "acme" 37tls = "acme"
37acme_email = "noreply@gitbay.example" 38acme_email = "noreply@gitbay.example"
38acme_http_addr = "127.0.0.1:%d" 39acme_http_addr = "127.0.0.1:%d"
39`, inst.root, inst.port, httpsPort, acmeHTTPPort) 40`, inst.root, inst.keyFile, inst.port, httpsPort, acmeHTTPPort)
40 if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil { 41 if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil {
41 t.Fatal(err) 42 t.Fatal(err)
42 } 43 }
e2e/backup_test.go +41 −1
@@ -1,16 +1,23 @@
1package e2e 1package e2e
2 2
3import ( 3import (
4 "bytes"
4 "fmt" 5 "fmt"
5 "net" 6 "net"
6 "os" 7 "os"
7 "os/exec" 8 "os/exec"
8 "path/filepath" 9 "path/filepath"
10 "regexp"
9 "strings" 11 "strings"
10 "testing" 12 "testing"
11 "time" 13 "time"
12) 14)
13 15
16// secretsCheckOneSealed matches "admin secrets check" reporting the one
17// build secret set in TestAdminBackup as sealed under some key, e.g.
18// "build_secrets.value: key 98e412e4 1".
19var secretsCheckOneSealed = regexp.MustCompile(`build_secrets\.value: key \S+ 1`)
20
14func TestAdminBackup(t *testing.T) { 21func TestAdminBackup(t *testing.T) {
15 t.Parallel() 22 t.Parallel()
16 inst := startInstance(t) 23 inst := startInstance(t)
@@ -35,6 +42,11 @@ func TestAdminBackup(t *testing.T) {
35 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/keep", "--title", "'survives backup'"); code != 0 { 42 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "create", "alice/keep", "--title", "'survives backup'"); code != 0 {
36 t.Fatal("issue create failed") 43 t.Fatal("issue create failed")
37 } 44 }
45 // A build secret, to show the archive carries it sealed and the key
46 // file not at all.
47 if _, errOut, code := inst.ssh(t, aliceKey, "hunter2-at-rest", "repo", "secret", "set", "alice/keep", "DEPLOY_TOKEN"); code != 0 {
48 t.Fatalf("secret set: %s", errOut)
49 }
38 50
39 // Back up while the daemon is running. 51 // Back up while the daemon is running.
40 archive := filepath.Join(t.TempDir(), "backup.tar.gz") 52 archive := filepath.Join(t.TempDir(), "backup.tar.gz")
@@ -64,6 +76,16 @@ func TestAdminBackup(t *testing.T) {
64 } 76 }
65 } 77 }
66 } 78 }
79 if strings.Contains(names, "secret.key") {
80 t.Fatalf("archive carries the key file:\n%s", names)
81 }
82 db, err := exec.Command("tar", "-xzOf", archive, "gitbay.db").Output()
83 if err != nil {
84 t.Fatal(err)
85 }
86 if bytes.Contains(db, []byte("hunter2-at-rest")) {
87 t.Fatal("the archived database carries the build secret in clear")
88 }
67 89
68 // Restore: extract into a fresh root and serve from it. 90 // Restore: extract into a fresh root and serve from it.
69 root2 := t.TempDir() 91 root2 := t.TempDir()
@@ -77,12 +99,13 @@ func TestAdminBackup(t *testing.T) {
77[server] 99[server]
78root = %q 100root = %q
79site_url = "https://gitbay.test" 101site_url = "https://gitbay.test"
102secret_key_file = %q
80[ssh] 103[ssh]
81port = %d 104port = %d
82[http] 105[http]
83addr = "127.0.0.1:%d" 106addr = "127.0.0.1:%d"
84tls = "off" 107tls = "off"
85`, root2, port2, httpPort2) 108`, root2, inst.keyFile, port2, httpPort2)
86 if err := os.WriteFile(config2, []byte(cfg), 0o600); err != nil { 109 if err := os.WriteFile(config2, []byte(cfg), 0o600); err != nil {
87 t.Fatal(err) 110 t.Fatal(err)
88 } 111 }
@@ -149,6 +172,23 @@ tls = "off"
149 if code != 0 || strings.TrimSpace(out2) != "alice" { 172 if code != 0 || strings.TrimSpace(out2) != "alice" {
150 t.Fatalf("whoami on restored instance: exit %d, %q, %s", code, out2, errOut) 173 t.Fatalf("whoami on restored instance: exit %d, %q, %s", code, out2, errOut)
151 } 174 }
175 // With the original key the restored secrets open; with another key
176 // they do not.
177 if out, err := exec.Command(inst.gitbayd, "--config", config2, "admin", "secrets", "check").CombinedOutput(); err != nil || !secretsCheckOneSealed.Match(out) {
178 t.Fatalf("secrets check on the restored instance: %v\n%s", err, out)
179 }
180 config3 := filepath.Join(root2, "config-wrong-key.toml")
181 wrong := strings.Replace(cfg, fmt.Sprintf("secret_key_file = %q", inst.keyFile),
182 fmt.Sprintf("secret_key_file = %q", filepath.Join(t.TempDir(), "other.key")), 1)
183 if err := os.WriteFile(config3, []byte(wrong), 0o600); err != nil {
184 t.Fatal(err)
185 }
186 if out, err := exec.Command(inst.gitbayd, "--config", config3, "admin", "secrets", "init").CombinedOutput(); err != nil {
187 t.Fatalf("init the wrong key: %v\n%s", err, out)
188 }
189 if out, err := exec.Command(inst.gitbayd, "--config", config3, "admin", "secrets", "check").CombinedOutput(); err == nil || !strings.Contains(string(out), "does not hold") {
190 t.Fatalf("secrets check with the wrong key: %v\n%s", err, out)
191 }
152 if out2, _, code = ssh2("repo", "log", "alice/keep"); code != 0 || !strings.Contains(out2, "keep me") { 192 if out2, _, code = ssh2("repo", "log", "alice/keep"); code != 0 || !strings.Contains(out2, "keep me") {
153 t.Fatalf("restored log: %d\n%s", code, out2) 193 t.Fatalf("restored log: %d\n%s", code, out2)
154 } 194 }
e2e/ssh_test.go +6 −1
@@ -24,6 +24,7 @@ type instance struct {
24 gitPort int 24 gitPort int
25 proc *exec.Cmd 25 proc *exec.Cmd
26 sshDir string // per-user client keys live here 26 sshDir string // per-user client keys live here
27 keyFile string // server.secret_key_file, outside root
27} 28}
28 29
29// nextPort hands out candidate ports. Seeded randomly so two test processes 30// nextPort hands out candidate ports. Seeded randomly so two test processes
@@ -89,11 +90,13 @@ func startInstanceWith(t *testing.T, extra string) *instance {
89 gitPort: ports[2], 90 gitPort: ports[2],
90 sshDir: t.TempDir(), 91 sshDir: t.TempDir(),
91 } 92 }
93 inst.keyFile = filepath.Join(t.TempDir(), "secret.key")
92 inst.config = filepath.Join(inst.root, "config.toml") 94 inst.config = filepath.Join(inst.root, "config.toml")
93 cfg := fmt.Sprintf(` 95 cfg := fmt.Sprintf(`
94[server] 96[server]
95root = %q 97root = %q
96site_url = "https://gitbay.test" 98site_url = "https://gitbay.test"
99secret_key_file = %q
97[ssh] 100[ssh]
98port = %d 101port = %d
99[http] 102[http]
@@ -102,12 +105,14 @@ tls = "off"
102[git_daemon] 105[git_daemon]
103enabled = true 106enabled = true
104port = %d 107port = %d
105`, inst.root, inst.port, inst.httpPort, inst.gitPort) 108`, inst.root, inst.keyFile, inst.port, inst.httpPort, inst.gitPort)
106 cfg += extra + "\n" 109 cfg += extra + "\n"
107 if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil { 110 if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil {
108 t.Fatal(err) 111 t.Fatal(err)
109 } 112 }
110 113
114 inst.admin(t, "admin", "secrets", "init")
115
111 inst.proc = exec.Command(inst.gitbayd, "--config", inst.config, "serve") 116 inst.proc = exec.Command(inst.gitbayd, "--config", inst.config, "serve")
112 inst.proc.Stderr = os.Stderr 117 inst.proc.Stderr = os.Stderr
113 if err := inst.proc.Start(); err != nil { 118 if err := inst.proc.Start(); err != nil {
e2e/system_test.go +2 −1
@@ -33,12 +33,13 @@ func TestSystemSSHMode(t *testing.T) {
33[server] 33[server]
34root = %q 34root = %q
35site_url = "https://gitbay.test" 35site_url = "https://gitbay.test"
36secret_key_file = %q
36[ssh] 37[ssh]
37mode = "system" 38mode = "system"
38[http] 39[http]
39addr = "127.0.0.1:%d" 40addr = "127.0.0.1:%d"
40tls = "off" 41tls = "off"
41`, inst.root, inst.httpPort) 42`, inst.root, inst.keyFile, inst.httpPort)
42 if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil { 43 if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil {
43 t.Fatal(err) 44 t.Fatal(err)
44 } 45 }