Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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