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.

Code

395 commit(s) on main

update readme Keenan Nemetz · 25m ago 32290114
source preview history blame raw 16.6 KiB
1# Operations
2
3A forge you own should be easy to back up and recover. Vole keeps the important
4state small: an account database, a handful of files, and ordinary Git repositories.
5These are the tasks that keep an instance healthy.
6
7## Audit log
8
9`data/audit.log` records every privileged action — roster and key changes, access
10rules, forced lock releases, forced pushes, release rewrites, admin commands,
11sessions, and ordinary pushes — as one tab-separated line:
12
13```text
14timestamp action actor subject detail
15```
16
17- **One shape.** Every writer goes through the same code, so a parser can split
18 on tabs. `action` is a lower-case dotted name (`acl.update`, `push.force`,
19 `session.create`, `admin`); `actor` is the account or OS user, `-` when there
20 is none; `subject` is the repository, ref, or account the action was about.
21- **Fields are escaped and truncated.** A key comment, an LFS lock path, a
22 repository name, or an admin argument cannot add a column or a line, and a
23 field is capped at 4 KiB.
24- **Mode 0600, owner only.** The file is created that way and tightened on every
25 write, so an instance upgraded from an older version stops being world-readable
26 the first time it writes. Startup refuses a log that is a symlink, a directory,
27 a non-regular file, or owned by another account.
28- **Privileged changes fail closed.** A roster, key, access-rule, session, forced
29 lock release, or admin command that cannot be recorded is refused, and the
30 command says so: `the audit log could not be written, so nothing was changed`.
31 A push that overrides a lock, an approval, a release, or history is refused
32 unless its override can be written. There is no setting that turns this off,
33 because an override is exactly what an attacker with config access would set.
34- **Ordinary pushes and web writes are reported, not refused.** Those changes
35 are already durable in Git; a failed write appears as `audit log write failed`
36 in the server log (or on the pusher's terminal for `post-receive`) rather than
37 making a working push look rejected.
38- **A broken log stops the instance.** `vole serve` and `vole admin` refuse to
39 start when the log cannot be opened for append, which turns a silent, long-lived
40 gap into a startup failure with a path in it.
41
42An instance admin can read the log in the browser at **Admin → Audit log**. The
43page shows one record per row, newest first, and pages back through the file. The
44Admin overview repeats the newest few, so a review can start there. Both are
45read-only views: they never edit, truncate, or rewrite a line, and a record the
46reader cannot parse is skipped rather than hidden from the rest of the page.
47
48Ship the file off the host — a read-only mount, a log shipper, or a periodic
49`rsync` of an append-only copy — and alert on the absence of expected lines
50as well as on the presence of unexpected ones. Local append-only storage is not
51tamper-evident: anyone who can write `data/` can edit it, so keep an off-host copy
52that the service account cannot reach.
53
54## Back up an instance
55
56Back up the instance state:
57
58```sh
59vole admin account backup backup/users.db # accounts, keys, sessions, rules, locks, views
60sqlite3 data/runs.db ".backup 'backup/runs.db'" # CI history, queue, and approvals
61cp -a data/logs backup/ # CI attempt logs
62cp data/ssh_host_ed25519_key backup/ # the SSH host key
63sqlite3 data/secrets.db ".backup 'backup/secrets.db'" # sealed repository secrets
64cp data/secrets.key backup/ # the key those secrets need
65cp -a data/acme backup/ 2>/dev/null # TLS account key and certificates, if ACME is on
66cp data/audit.log backup/ 2>/dev/null # history of admin actions
67```
68
69`vole admin account backup` takes a consistent copy, even while Vole is running. Copying
70`users.db` by hand can miss recent changes that have not been written to the main
71database file yet. Do the same for `runs.db` with SQLite's backup command; copying it
72by hand while the server is writing can miss WAL pages.
73
74Keep `data/secrets.key` apart from the copies of `data/`. A backup that holds both
75the database and its key is a backup of plaintext: the encryption protects a copy
76that loses the key, not one that keeps it. Restoring `data/secrets.db` beside a
77newly generated key reads nothing, and Vole says so rather than handing a job an
78empty value.
79
80If you lose the account database, you need to recreate accounts and re-enroll
81their passkeys. You do not lose repositories. The backup contains account metadata,
82public keys, and hashes of session and enrollment tokens; it does not contain a
83usable password or a plaintext session token. Treat it as private anyway.
84
85Repositories are ordinary Git repositories under `data/repos/`. You can back them
86up by copying or cloning. Issues, change requests, and releases travel with them
87because they are stored in Git. CI history does not: it lives in `data/runs.db` and
88must be backed up with it.
89
90## Restore an instance
91
921. Restore `data/users.db`, `data/runs.db`, `data/logs/`,
93 `data/ssh_host_ed25519_key`, `data/audit.log`, `data/secrets.db` with the
94 `data/secrets.key` that sealed it, and `data/acme/` if the instance obtains its
95 own TLS certificates.
962. Restore the repositories under `data/repos/` and the object store under
97 `data/lfs/` if you keep CI artifacts.
983. Run `vole admin index rebuild`.
994. Run `vole admin account revoke-sessions --all` so sessions from the restored copy
100 cannot outlive their cookies.
1015. Start `vole serve`.
102
103Release notes for an upgrade: an older `users.db` is upgraded in place when it is
104opened. Accounts used to have an internal id and an optional display label; both
105are gone, and every key, passkey, enrollment link, session and admin grant is rebuilt
106around the registered name. Access-rule entries and file locks in that database
107are rewritten through the same mapping, and the retired `user_names` alias table
108is dropped. Take a backup first if you might roll the binary back. Approval files
109live in Git and are not rewritten; an approval written under an old account id
110stops matching, so ask for a fresh approval.
111
112Contributor branches moved from `refs/heads/contrib/<account-id>/` to
113`refs/heads/contrib/<name>/`. Old refs stay in the repository and can still be
114fetched, but only a maintainer can push them; existing change requests still name
115the same head commit. A contributor who wants to keep working pushes the same
116commit under their new folder and opens a request from it.
117
118Large files and build artifacts live in `data/lfs/`; CI logs live in `data/logs/`.
119Back both directories up too if you need those files. `vole admin gc` treats files
120reachable from a repository or a published artifact as things to keep; logs are
121ordinary files and are not swept.
122
123## Restart the server
124
125A clean stop (`systemctl restart`, or Ctrl-C on a development instance) stops
126handing out new CI jobs and waits up to a minute for the jobs already leased to
127report their result. A job that finishes in that window is stored and is not run
128again. A job still running when the window ends loses its runner connection and
129is requeued as another attempt once its lease expires. The wait is best effort;
130start long builds well before a planned restart.
131
132## Clean up stored files
133
134```sh
135vole admin gc --dry-run # show what would be removed
136vole admin gc # remove unused files
137vole admin gc --grace 24h # keep unused files for a day
138```
139
140`vole admin gc` cleans the shared storage used by LFS and build artifacts. CI logs are
141ordinary files under `data/logs/` and are not part of the sweep. It
142also repairs each repository's links into that storage, so it helps when files
143are missing or links are broken.
144
145The default grace period is one hour. A recent upload that has not been pushed yet
146will not be removed.
147
148You can run `vole admin gc` while people are pushing. There is no need to schedule it;
149storage only grows when someone pushes files.
150
151## Rebuild indexes
152
153`data/index.db` is generated by Vole and can be deleted safely:
154
155```sh
156vole admin index rebuild
157```
158
159`data/runs.db` holds the CI queue, finished-run records, and contributor-branch CI
160approvals. It is a source of truth, not a rebuildable index: deleting it loses CI
161history as well as attempts that were in progress. Back it up with the rest of the
162instance. `vole admin index rebuild` rebuilds only the repository and issue index;
163it cannot restore CI history.
164
165## Security notes
166
167- Two long-term secrets are stored: the SSH host key, and the key that seals
168 repository secrets. Keep both readable only by the Vole user (`0600`). Browser
169 session and enrollment tokens are stored as hashes.
170- Repository secrets are encrypted at rest, which protects a copy of `data/` that
171 leaves the host. It does not protect a host somebody already owns: file modes
172 are the boundary, and the default key location is beside the database. Anything
173 that can read the data directory can read the values.
174- Vole creates its own secrets at `0600` and its data directory at `0700`: the
175 roster (`users.db`), the audit log, the SSH host key and the ACME account
176 directory. It also tightens an existing instance on startup, and logs the paths
177 whose mode it changed, so an upgrade from an earlier version cannot leave a
178 private key world-readable. If a mode cannot be changed, startup fails rather
179 than serving keys it cannot protect.
180 A site directory that is deliberately shared with a backup group is fine at
181 `0750`: group access is left alone, world access is not.
182- Nothing Vole writes is signed: not pushes, not approvals, not CI results. Vole
183 relies on the SSH key at the connection, the checks before a push is accepted,
184 and the audit log.
185- The web interface is read-only for anonymous visitors. Passkey sign-in enables
186 issue, comment, and approval writes; HTTP Git clone remains read-only. There is
187 no password store. Restoring an old `users.db` should be followed by
188 `vole admin account revoke-sessions --all`. The landing page introduction is
189 not an exception: it is `profile/README.md` in the instance repository
190 (`instance_repo`), edited by pushing like every other customization.
191- Account names are permanent. They name contributor branches, so an upgrade from
192 a build where names could change keeps each account's current name and forgets
193 the former ones.
194- Open registration creates accounts without email verification. Leave
195 `open_registration` off unless the instance is meant to accept public
196 contributions.
197- Remote administration uses the same SSH listener and key. Only active instance
198 admins can run it, and every admin command is audited.
199- Private repositories return `404` on the web and over HTTP, so visitors cannot
200 confirm that they exist.
201- Page-view metrics are daily totals keyed by account, plus one anonymous total.
202 They store no path, address, referrer, or user agent; the admin-only status
203 page uses them only to show how much the instance is read.
204- Vole can obtain and renew TLS certificates itself when `https_addr` is set;
205 otherwise put it behind a proxy that terminates TLS and keeps the `Host`
206 header, so site routing still works. See
207 [Configuration § Automatic HTTPS with ACME](configuration.md#automatic-https-with-acme).
208
209## Production baseline
210
211A public instance is a web server, a Git server and a policy engine in one
212process. The settings below are the ones that matter on the day it is exposed;
213the linked sections explain each in full.
214
215- **Web behind TLS, on loopback.** Leave `https_addr` empty, bind
216 `http_addr = "127.0.0.1:8080"`, and put a proxy in front that terminates TLS
217 and passes the `Host` header through unchanged. Alternatively set `https_addr`
218 with `acme_email` and `acme_domains` and let Vole do it. Set `public_url` to
219 the canonical `https://` address so links and passkeys agree with it. When the
220 request arrives over TLS, or `public_url` is `https://`, Vole sends HSTS on
221 every response, not only on the pages that use passkeys. See
222 [Configuration](configuration.md).
223- **Compression is already here.** Forge pages, hosted sites, the stylesheet,
224 scripts and source blobs are gzipped when the client asks for it, so a proxy in
225 front should not compress them again: double compression costs CPU and hides
226 the `ETag` revalidation that keeps the shared scripts and each Prism grammar to
227 one download per browser. Archives, LFS objects, Git responses and ranged
228 downloads are passed through untouched.
229- **Shared caches.** If a caching proxy or CDN sits in front of the instance, it
230 may keep public pages, static assets, and artifacts from public repositories.
231 Private pages and downloads are marked `private` or `no-store`, so a cache that
232 honours those directives will not serve them to an anonymous reader. Do not
233 strip or rewrite those headers at the proxy.
234- **SSH firewalled.** The SSH listener is the only write transport and the only
235 place an account is authenticated. Expose it to the people who push, not to the
236 internet at large, and leave it on a port other than 22 unless you also run
237 `sshd` elsewhere.
238- **No development shortcuts.** `dev_only` and `dev_login` are refused together
239 with a public address, and an instance that needs neither should set neither.
240 The same goes for `open_registration`, which is off by default.
241- **Quotas.** Every repository is bounded, so one contributor or maintainer cannot
242 make every page slow or fill the disk: `repo_max_refs` (500), `repo_max_metadata_entries`
243 (2000) and `repo_max_metadata_bytes` (32 MiB) of documents on the metadata branch
244 (issues, requests, comments, approvals, and release records),
245 `repo_max_bytes` (5 GiB) of Git objects, and `repo_max_lfs_bytes` (10 GiB) of
246 LFS objects and artifacts. The checks run in pre-receive, so a refused push
247 moves no refs, and the object bound is enforced on upload. When one bites, the
248 message names it: raise it in `vole.toml`, delete the branches or metadata that
249 are not wanted, or run `vole admin gc` to drop unreachable objects. Per-account
250 browser writes are budgeted for the untrusted (`contribute`) role too
251 (`contributor_writes_per_hour`, 300); a person discussing never reaches it, and
252 maintainers and admins are trusted roles that are not budgeted at all.
253- **Conservative limits.** The defaults are chosen for a small instance:
254 `git_http_max_concurrent = 16`, `git_http_max_per_client = 8`,
255 `ssh_max_connections = 64`, `ssh_max_git_processes = 16`,
256 `ssh_handshake_timeout_seconds = 20`, `max_blob_bytes = 100 MiB`,
257 `receive_max_bytes = 1 GiB`, and a 600 s budget for one clone. Raise them with
258 measurements, not with optimism; the read path is anonymous, so a limit that is
259 too low is a slow clone and a limit that is missing is an outage.
260- **Bounded sign-in.** The passkey ceremonies are the only anonymous endpoints
261 that cost the instance work, so they are rate-limited per client address
262 (`20` begins a minute), per account name (`10`), and for registrations (`5`),
263 and the in-flight ceremony map is capped at `4096` entries with a five-minute
264 expiry. A refused client gets `429` with `Retry-After`. Behind a proxy, set
265 `git_http_client_ip_header` so the budgets follow the real address. Invite
266 tokens live in the URL fragment, so they never reach an access log; links in
267 the old `/-/invite/<token>` shape are redirected into it. See
268 [Accounts § Browser sign-in](accounts-and-access.md#browser-sign-in).
269- **Bounded before authentication.** The SSH listener refuses connections past
270 `ssh_max_connections` before the handshake and drops a connection that has not
271 finished its handshake in `ssh_handshake_timeout_seconds`, so an anonymous
272 client cannot hold a file descriptor by opening a socket and saying nothing.
273 After the handshake one connection may hold eight session channels, and the
274 git commands they run share `ssh_max_git_processes`; an exec line longer than
275 4 KiB and a runner request line longer than 1 MiB are refused. Address-based
276 backoff for repeated bad keys is a quota concern, not a transport bound, so it
277 belongs with the abuse controls rather than here.
278- **Audited administration.** Remote administration uses the same SSH listener
279 and is restricted to instance admins. Ship the audit log off the host: local
280 append-only is not tamper-evident. See [Audit log](#audit-log) and
281 [Architecture § Trust model](architecture.md).
282- **Backups you have restored once.** `users.db` is not derived and not
283 rebuildable; everything else either is, or can be re-pushed. Test the restore,
284 not the backup. See [Back up an instance](#back-up-an-instance).
285- **The service unit.** The packaged unit drops all capabilities, sets
286 `UMask=0027`, and hides other processes and devices. If you serve HTTPS or SSH
287 on a port below 1024, grant `CAP_NET_BIND_SERVICE` with a drop-in rather than
288 running the service as root. See [Packaging](packaging.md).
289