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/web/status.go +5
| @@ -284,6 +284,10 @@ func (w *Web) statusConfig() *statusConfigView { | |||
| 284 | 284 | if strings.TrimSpace(cfg.ACMECacheDir) == "" { | |
| 285 | 285 | acmeCacheNote = "derived from data_dir" | |
| 286 | 286 | } | |
| 287 | secretsKeyNote := "keep this out of a backup of data_dir" | ||
| 288 | if strings.TrimSpace(cfg.SecretsKey) == "" { | ||
| 289 | secretsKeyNote = "derived from data_dir; keep it out of the data backup" | ||
| 290 | } | ||
| 287 | 291 | pagesNote := "" | |
| 288 | 292 | if strings.TrimSpace(cfg.PagesDomain) == "" { | |
| 289 | 293 | pagesNote = "sites disabled" | |
| @@ -353,6 +357,7 @@ func (w *Web) statusConfig() *statusConfigView { | |||
| 353 | 357 | }}, | |
| 354 | 358 | {Name: "Storage and push limits", Settings: []statusSetting{ | |
| 355 | 359 | configSetting("data_dir", cfg.DataDir, ""), | |
| 360 | configSetting("secrets_key", cfg.SecretsKeyPath(), secretsKeyNote), | ||
| 356 | 361 | configSetting("max_blob_bytes", byteSetting(cfg.MaxBlobBytes), ""), | |
| 357 | 362 | configSetting("receive_max_bytes", byteSetting(cfg.ReceiveMaxBytes), ""), | |
| 358 | 363 | configSetting("lfs_object_max_bytes", byteSetting(cfg.LFSObjectMaxBytes), ""), | |