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/config/config.go +21
| @@ -153,6 +153,11 @@ type Config struct { | |||
| 153 | 153 | // makes dev_login an explicit decision rather than a setting somebody copied | |
| 154 | 154 | // from an example: without it, dev_login is refused at startup. | |
| 155 | 155 | DevOnly bool `toml:"dev_only"` | |
| 156 | // SecretsKey names the file holding the key that seals repository secrets. | ||
| 157 | // Empty means <data_dir>/secrets.key. It is a separate setting because the key | ||
| 158 | // can then be kept somewhere the data directory's backups do not reach, which | ||
| 159 | // is the difference between a backup of secrets and a backup of ciphertext. | ||
| 160 | SecretsKey string `toml:"secrets_key"` | ||
| 156 | 161 | ||
| 157 | 162 | // Path is the file Load was asked to read. Loaded reports whether it found a | |
| 158 | 163 | // non-empty file there; false means the built-in defaults are in use, which | |
| @@ -249,6 +254,22 @@ func (c *Config) UsersDB() string { return filepath.Join(c.DataDir, "users.db") | |||
| 249 | 254 | func (c *Config) AuditLog() string { return filepath.Join(c.DataDir, "audit.log") } | |
| 250 | 255 | func (c *Config) IndexDB() string { return filepath.Join(c.DataDir, "index.db") } | |
| 251 | 256 | ||
| 257 | // SecretsDB holds each repository's sealed secrets. It is a separate file from | ||
| 258 | // the roster because it is the one database whose contents are encrypted, and a | ||
| 259 | // copy of it without SecretsKeyPath is ciphertext. | ||
| 260 | func (c *Config) SecretsDB() string { return filepath.Join(c.DataDir, "secrets.db") } | ||
| 261 | |||
| 262 | // SecretsKeyPath is the AES-256 key that seals those values. It is created on | ||
| 263 | // first use at 0600; losing it means losing every stored value, and restoring a | ||
| 264 | // copy of the database beside a newly generated key is a restore that cannot read | ||
| 265 | // what it restored. | ||
| 266 | func (c *Config) SecretsKeyPath() string { | ||
| 267 | if path := strings.TrimSpace(c.SecretsKey); path != "" { | ||
| 268 | return path | ||
| 269 | } | ||
| 270 | return filepath.Join(c.DataDir, "secrets.key") | ||
| 271 | } | ||
| 272 | |||
| 252 | 273 | // RunsDB holds the CI queue and finished runs. It is authoritative for work that has | |
| 253 | 274 | // not happened yet — a queued or leased job exists only there — while the finished | |
| 254 | 275 | // half is derived from the records on each repository's metadata branch, and pending | |