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/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.

Make the audit log one shape that a privileged change cannot skip

b1499c729a026cfda32d8a90374b3b8391ca0090 · Keenan Nemetz · 12d ago · parent d74d4de8

Make the audit log one shape that a privileged change cannot skip

Every writer used to format its own line and open its own file, so
data/audit.log held six different column layouts and a write that failed
changed nothing. A log that can be bypassed by a full disk, or by a key
comment containing a tab, is not the security promise the docs make.

internal/audit is now the only writer: one line per action, always
timestamp, action, actor, subject, detail. Every field is escaped and
capped at 4 KiB, so a key comment, an LFS lock path, a repository name or
an admin argument cannot add a column or a record. One write per line
under O_APPEND keeps the web, the SSH server and each hook process from
interleaving, and secure.OpenAppend keeps the file at 0600 including on an
upgrade from 0644.

Privileged changes now fail closed, and they are recorded before they
happen, so a failed write means the change did not occur:

  - roster, key and instance-role changes (inside the transaction)
  - access-rule changes, and a repository deletion, which records before
    removing content and forgets the rules only afterwards
  - session creation and revocation
  - a forced LFS unlock
  - an admin command over SSH, which reports audit_unavailable
  - a push that overrides a lock, an approval, a release or history

Ordinary pushes and web actions are reported instead: their changes are
already durable in git, so a write failure is logged at error level (or on
the pusher's terminal from post-receive) rather than making a working push
look rejected.

Opening an instance now checks the log is a regular file at 0600 owned by
the service account and appendable, and refuses to start otherwise. There
is no setting that disables the log or returns it to best effort.

Regression tests cover field injection through key comments, lock paths,
admin argv and repository names; 0600 on create and on upgrade; an
unwritable sink failing an ACL change, an account change, a session, a
forced unlock, an admin command and a push override while leaving the
underlying state unchanged; concurrent appends keeping whole lines; and
startup refusing a symlinked log.

docs/operations.md +42−1

@@ -4,6 +4,47 @@ A forge you own should be easy to back up and recover. Vole keeps the important
4 4 state small: an account database, a handful of files, and ordinary Git repositories.
5 5 These are the tasks that keep an instance healthy.
6 6
7 ## Audit log
8
9 `data/audit.log` records every privileged action — roster and key changes, access
10 rules, forced lock releases, forced pushes, release rewrites, admin commands,
11 sessions, and ordinary pushes — as one tab-separated line:
12
13 ```text
14 timestamp 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
42 Ship the file off the host — a read-only mount, a log shipper, or a periodic
43 `rsync` of an append-only copy — and alert on the absence of expected lines
44 as well as on the presence of unexpected ones. Local append-only storage is not
45 tamper-evident: anyone who can write `data/` can edit it, so keep an off-host copy
46 that the service account cannot reach.
47
7 48 ## Back up an instance
8 49
9 50 Back up the instance state:
@@ -153,7 +194,7 @@ the linked sections explain each in full.
153 194 missing is an outage.
154 195 - **Audited administration.** Remote administration uses the same SSH listener
155 196 and is restricted to instance admins. Ship the audit log off the host: local
156 append-only is not tamper-evident. See
197 append-only is not tamper-evident. See [Audit log](#audit-log) and
157 198 [Architecture § Trust model](architecture.md).
158 199 - **Backups you have restored once.** `users.db` is not derived and not
159 200 rebuildable; everything else either is, or can be re-pushed. Test the restore,