vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Store repository secrets encrypted, with a CLI and a settings form
0c40a1d3b48eb0ade37334f62ee526c371ecb61c · Keenan Nemetz · 7d ago · parent 122cbe4f
Store repository secrets encrypted, with a CLI and a settings form A repository has values that cannot live in its code: a deploy key, a registry token, a staging password. They are now stored in data/secrets.db, sealed with AES-256-GCM under data/secrets.key, and read through one store that records every read and every change. The key is a separate file, and secrets_key can move it, so a backup of the data directory can be a backup of ciphertext rather than of plaintext. `vole secret list|set|get|rm` asks the instance over the key the caller already pushes with, since the store is server-side. set reads the value from stdin or a file and never from the argument list; get writes the value and nothing else. The maintainer role is the capability: the accounts that approve work and allow CI own the credentials those jobs will use. The repository's Settings tab gains the same list — names, versions, who changed one, when a value was last read — with set, replace and delete. It never renders a value, and a maintainer who is not an admin now reaches that page for this section alone. Values are limited to what one environment variable can carry: 128 KiB for NAME=value, valid UTF-8 and no NUL. A longer value could never reach a step, and the refusal says so. Slice 1 of #154.
server/admin_web_test.go +8−2
| @@ -173,8 +173,14 @@ func TestRepositoryAccessRequiresInstanceAdmin(t *testing.T) { | |||
| 173 | 173 | if resp, _ := f.browserGet(t, "/users", writerCookie); resp.StatusCode != 404 { | |
| 174 | 174 | t.Fatalf("non-admin users page = %d, want 404", resp.StatusCode) | |
| 175 | 175 | } | |
| 176 | if resp, _ := f.browserGet(t, "/r/"+f.repo+"/settings", writerCookie); resp.StatusCode != 404 { | ||
| 177 | t.Fatalf("maintainer settings = %d, want 404", resp.StatusCode) | ||
| 176 | // A maintainer may open the settings page for the repository's secrets, but the | ||
| 177 | // instance-admin half of it must not be there. | ||
| 178 | resp, maintainerBody := f.browserGet(t, "/r/"+f.repo+"/settings", writerCookie) | ||
| 179 | if resp.StatusCode != 200 || !strings.Contains(maintainerBody, `id="secrets"`) { | ||
| 180 | t.Fatalf("maintainer settings = %d", resp.StatusCode) | ||
| 181 | } | ||
| 182 | if strings.Contains(maintainerBody, `id="access"`) { | ||
| 183 | t.Fatal("a maintainer saw the instance-admin access section") | ||
| 178 | 184 | } | |
| 179 | 185 | if _, body := f.browserGet(t, "/", writerCookie); strings.Contains(body, `href="/users"`) { | |
| 180 | 186 | t.Fatal("non-admin sees the Users link") | |