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, | |