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/server.go +13−8
| @@ -20,6 +20,7 @@ import ( | |||
| 20 | 20 | "github.com/rodent-software/vole/forge/index" | |
| 21 | 21 | "github.com/rodent-software/vole/forge/pageview" | |
| 22 | 22 | "github.com/rodent-software/vole/forge/repo" | |
| 23 | "github.com/rodent-software/vole/forge/secret" | ||
| 23 | 24 | "github.com/rodent-software/vole/forge/session" | |
| 24 | 25 | "github.com/rodent-software/vole/internal/compress" | |
| 25 | 26 | "github.com/rodent-software/vole/server/githttp" | |
| @@ -40,13 +41,16 @@ type Server struct { | |||
| 40 | 41 | LFS *lfs.Store | |
| 41 | 42 | Locks *lfs.LockStore | |
| 42 | 43 | PageViews *pageview.Store | |
| 43 | Pages *pages.Server | ||
| 44 | Proxy *githttp.Proxy | ||
| 45 | SSH *ssh.Server | ||
| 46 | Web *web.Web | ||
| 47 | Log *slog.Logger | ||
| 48 | mux *http.ServeMux | ||
| 49 | acme *acmeSetup | ||
| 44 | // Secrets is the per-repository store behind the settings page and the | ||
| 45 | // `vole secret` verbs. Values never leave it except to a caller it has audited. | ||
| 46 | Secrets *secret.Store | ||
| 47 | Pages *pages.Server | ||
| 48 | Proxy *githttp.Proxy | ||
| 49 | SSH *ssh.Server | ||
| 50 | Web *web.Web | ||
| 51 | Log *slog.Logger | ||
| 52 | mux *http.ServeMux | ||
| 53 | acme *acmeSetup | ||
| 50 | 54 | // metadataWake nudges the metadata event loop after a browser write. Pushes | |
| 51 | 55 | // deliver the same information through the spool; this is the path a write made | |
| 52 | 56 | // without a push uses. | |
| @@ -145,6 +149,7 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) | |||
| 145 | 149 | wp.Locks = inst.Locks | |
| 146 | 150 | wp.Sessions = inst.Sessions | |
| 147 | 151 | wp.PageViews = inst.PageViews | |
| 152 | wp.Secrets = inst.Secrets | ||
| 148 | 153 | wp.Logger = func(format string, args ...any) { log.Warn(fmt.Sprintf(format, args...)) } | |
| 149 | 154 | if cfg.DevLogin != "" { | |
| 150 | 155 | log.Warn("development login enabled", "account", cfg.DevLogin, "http_addr", cfg.HTTPAddr) | |
| @@ -158,7 +163,7 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) | |||
| 158 | 163 | s := &Server{ | |
| 159 | 164 | Cfg: cfg, Repos: inst.Repos, Identity: inst.Identity, Sessions: inst.Sessions, | |
| 160 | 165 | Runs: inst.Runs, Index: inst.Index, acme: acme, | |
| 161 | LFS: inst.LFS, Locks: inst.Locks, PageViews: inst.PageViews, | ||
| 166 | LFS: inst.LFS, Locks: inst.Locks, PageViews: inst.PageViews, Secrets: inst.Secrets, | ||
| 162 | 167 | Proxy: proxy, SSH: ssh, Web: wp, Log: log, | |
| 163 | 168 | metadataWake: make(chan struct{}, 1), | |
| 164 | 169 | } | |