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.

Replace the git-backed roster with a trusted keys directory

197ecaa29d992ecce2b6078803f3512f565f3672 · Keenan Nemetz · 17d ago · parent a5f19b3c

Replace the git-backed roster with a trusted keys directory

The roster was 476 lines of CAS retries, tree building and snapshot reloading to
store what amounts to username -> authorized_keys lines. Two of the three fields
in meta.json were unused: ACLs key on username, never on id, and nothing ever set
disabled. The git version was paying for history and tamper evidence that
nothing consumed.

Now:

  data/trusted_keys/keenan.keys    # one authorized_keys line per device

This is the AuthorizedKeysFile convention, so ssh-keygen -lf, ansible, rsync and
an editor all work on it, and dropping a key in by hand is a supported way to add
a user because the running instance re-reads the directory every five seconds.
Adds are O_APPEND, so two processes cannot lose each other's writes; removes are
read-modify-rename and assume a single operator. A file that does not parse, or
the same key under two names, still refuses startup rather than silently changing
who can authenticate.

What the directory could not provide on its own is an audit trail, so key changes
append a line to data/audit.log: timestamp, action, user, and key fingerprint.
That is the git log we deleted, at about 15 lines instead of a commit graph. If a
user must be blocked rather than removed, their keys are removed; there is no
disabled flag until something needs one.

Identity is now ~200 lines instead of 476, and the lost-update class of bug that
the CAS loop made possible is gone with the loop.

Verified by hand: fresh init shows data/ only holding audit.log, index.db, repos
and trusted_keys; a key appended to the file by hand starts working within the
poll interval without a restart; removing a key by CLI stops that key
authenticating; audit.log records both.

README.md +10−5

@@ -33,8 +33,11 @@ Working today:
33 33 - Policy enforced in `pre-receive`, so a rejected push moves no refs at all
34 34 - Protected refs, per-blob size limits, and a push cap enforced by git's own
35 35 `receive.maxInputSize`
36 - The user roster is public keys in `data/system.git`: cloning that repo is a
37 complete backup of every user and key, and `git log` there is the audit trail
36 - The user roster is one file of SSH public keys per user in `data/trusted_keys/`
37 (plain `authorized_keys` format), with an append-only `data/audit.log` recording
38 every key added or revoked. A running instance re-reads the directory every few
39 seconds, so `vole user add-key` — or an edit by hand — applies without a restart,
40 and revocation takes effect the same way
38 41 - Repo create / rename / delete from the CLI
39 42 - A derived SQLite index that can be deleted and rebuilt from the repos
40 43 - An instance ed25519 key generated at first boot (used for metadata signing in
@@ -70,11 +73,13 @@ over HTTP — not a `403` — so the read-only surface never confirms that it ex
70 73 Two things, and both are small:
71 74
72 75 ```sh
73 git clone --bare data/system.git backup/system.git # users, keys, history
74 cp data/instance.key data/ssh_host_ed25519_key backup/ # the two secrets
76 rsync -a data/trusted_keys/ backup/trusted_keys/ # users and their keys
77 cp data/instance.key data/ssh_host_ed25519_key backup/ # the only two secrets
78 cp data/audit.log backup/ 2>/dev/null # who added what, when
75 79 ```
76 80
77 Losing the key files means re-issuing them, not losing data.
81 Losing the key files means re-issuing them, not losing data — nothing in the
82 roster is secret, so a copied directory leaks nothing.
78 83
79 84 Run everything before pushing:
80 85