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.
docs/operations.md +18−5
| @@ -54,6 +54,8 @@ vole admin account backup backup/users.db # accounts, keys, sessions, rule | |||
| 54 | 54 | sqlite3 data/runs.db ".backup 'backup/runs.db'" # CI history, queue, and approvals | |
| 55 | 55 | cp -a data/logs backup/ # CI attempt logs | |
| 56 | 56 | cp data/ssh_host_ed25519_key backup/ # the SSH host key | |
| 57 | sqlite3 data/secrets.db ".backup 'backup/secrets.db'" # sealed repository secrets | ||
| 58 | cp data/secrets.key backup/ # the key those secrets need | ||
| 57 | 59 | cp -a data/acme backup/ 2>/dev/null # TLS account key and certificates, if ACME is on | |
| 58 | 60 | cp data/audit.log backup/ 2>/dev/null # history of admin actions | |
| 59 | 61 | ``` | |
| @@ -63,6 +65,12 @@ cp data/audit.log backup/ 2>/dev/null # history of admin actions | |||
| 63 | 65 | database file yet. Do the same for `runs.db` with SQLite's backup command; copying it | |
| 64 | 66 | by hand while the server is writing can miss WAL pages. | |
| 65 | 67 | ||
| 68 | Keep `data/secrets.key` apart from the copies of `data/`. A backup that holds both | ||
| 69 | the database and its key is a backup of plaintext: the encryption protects a copy | ||
| 70 | that loses the key, not one that keeps it. Restoring `data/secrets.db` beside a | ||
| 71 | newly generated key reads nothing, and Vole says so rather than handing a job an | ||
| 72 | empty value. | ||
| 73 | |||
| 66 | 74 | If you lose the account database, you need to recreate accounts and re-enroll | |
| 67 | 75 | their passkeys. You do not lose repositories. The backup contains account metadata, | |
| 68 | 76 | public keys, and hashes of session and enrollment tokens; it does not contain a | |
| @@ -76,8 +84,9 @@ must be backed up with it. | |||
| 76 | 84 | ## Restore an instance | |
| 77 | 85 | ||
| 78 | 86 | 1. Restore `data/users.db`, `data/runs.db`, `data/logs/`, | |
| 79 | `data/ssh_host_ed25519_key`, `data/audit.log`, and `data/acme/` if the instance | ||
| 80 | obtains its own TLS certificates. | ||
| 87 | `data/ssh_host_ed25519_key`, `data/audit.log`, `data/secrets.db` with the | ||
| 88 | `data/secrets.key` that sealed it, and `data/acme/` if the instance obtains its | ||
| 89 | own TLS certificates. | ||
| 81 | 90 | 2. Restore the repositories under `data/repos/` and the object store under | |
| 82 | 91 | `data/lfs/` if you keep CI artifacts. | |
| 83 | 92 | 3. Run `vole admin index rebuild`. | |
| @@ -149,9 +158,13 @@ it cannot restore CI history. | |||
| 149 | 158 | ||
| 150 | 159 | ## Security notes | |
| 151 | 160 | ||
| 152 | - The SSH host key is the only long-term secret Vole stores. Keep it readable | ||
| 153 | only by the Vole user (`0600`). Browser session and enrollment tokens are | ||
| 154 | stored as hashes. | ||
| 161 | - Two long-term secrets are stored: the SSH host key, and the key that seals | ||
| 162 | repository secrets. Keep both readable only by the Vole user (`0600`). Browser | ||
| 163 | session and enrollment tokens are stored as hashes. | ||
| 164 | - Repository secrets are encrypted at rest, which protects a copy of `data/` that | ||
| 165 | leaves the host. It does not protect a host somebody already owns: file modes | ||
| 166 | are the boundary, and the default key location is beside the database. Anything | ||
| 167 | that can read the data directory can read the values. | ||
| 155 | 168 | - Vole creates its own secrets at `0600` and its data directory at `0700`: the | |
| 156 | 169 | roster (`users.db`), the audit log, the SSH host key and the ACME account | |
| 157 | 170 | directory. It also tightens an existing instance on startup, and logs the paths | |