vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software: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.
forge/repo/repo.go +14
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | ||
| 17 | 17 | "github.com/rodent-software/vole/core/git" | |
| 18 | 18 | "github.com/rodent-software/vole/forge/access" | |
| 19 | "github.com/rodent-software/vole/forge/secret" | ||
| 19 | 20 | ) | |
| 20 | 21 | ||
| 21 | 22 | const ( | |
| @@ -53,6 +54,9 @@ type Store struct { | |||
| 53 | 54 | // configuration the instance owns, keyed by account name, read on every push and | |
| 54 | 55 | // every request (see the access package). | |
| 55 | 56 | ACLs *access.Store | |
| 57 | // Secrets is where the repository's sealed values live, keyed by the same stable | ||
| 58 | // id the rules use. A deleted repository takes its secrets with it. | ||
| 59 | Secrets *secret.Store | ||
| 56 | 60 | } | |
| 57 | 61 | ||
| 58 | 62 | func New(root, binPath string, acls *access.Store) *Store { | |
| @@ -272,9 +276,19 @@ func (s *Store) Delete(name, actor string) error { | |||
| 272 | 276 | return err | |
| 273 | 277 | } | |
| 274 | 278 | } | |
| 279 | if s.Secrets != nil { | ||
| 280 | if err := s.Secrets.RecordRepoDelete(m.ID, actor); err != nil { | ||
| 281 | return err | ||
| 282 | } | ||
| 283 | } | ||
| 275 | 284 | if err := os.RemoveAll(m.Dir); err != nil { | |
| 276 | 285 | return err | |
| 277 | 286 | } | |
| 287 | if s.Secrets != nil { | ||
| 288 | if err := s.Secrets.DeleteRepo(m.ID); err != nil { | ||
| 289 | return err | ||
| 290 | } | ||
| 291 | } | ||
| 278 | 292 | if s.ACLs != nil { | |
| 279 | 293 | return s.ACLs.Delete(m.ID) | |
| 280 | 294 | } | |