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.
Move writes to SSH, drop web authentication, keep identity in git
a5f19b3c19861e080953096f934df25dc38beaf7 · Keenan Nemetz · 17d ago · parent 653e0cfc
Move writes to SSH, drop web authentication, keep identity in git The authenticated-HTTP design is gone rather than extended. Writes now require an SSH key, the web is read-only, and the user roster lives in git. What replaced what: - internal/sshserver is the only write transport. An embedded SSH server (golang.org/x/crypto/ssh) authenticates by public key and hands the principal to pre-receive through the environment, so the hook's policy code is unchanged. The exec surface is whitelisted to git-upload-pack and git-receive-pack, the repository argument is resolved through the store rather than concatenated, and everything else -- including `sh -c` -- is refused and asserted refused in a test. - internal/identity stores users on refs/heads/main in data/system.git. The directory listing is the table: users/<name>/keys/<sha>.pub. Writes are CAS commits, reads come from an in-memory snapshot so the SSH auth callback is a map lookup. Nothing stored is a secret, so a clone of system.git is a complete backup and git log on it is the audit trail. - internal/transport is now anonymous read-only: upload-pack behind a visibility check, 404 for private repositories, and no push path at all. - internal/web has no login, no sessions and no mutations. Deleted outright: internal/auth, the Basic-auth path, login/logout, web settings/rename/delete, chunked body spooling, the spool janitor, the push semaphore and the hand-rolled byte cap. The cap is now git's receive.maxInputSize per repo, which enforces it on every transport without buffering. Three things worth recording: - The CAS helper read the tree outside the retry loop and compared against a freshly-read old value, so a concurrent writer's commit could be silently discarded while the CAS "succeeded". A test with two stores adding eight keys at once found it losing half of them; read, mutate and compare now all happen inside the loop. - The roster moved from refs/vole/users to refs/heads/main because `git clone` only mirrors refs/heads/*: the documented backup command produced an instance with zero users. Storing on a branch also means git itself refuses to point the ref at a non-commit. - Exec refusals are answered on the channel rather than by rejecting the request. Rejecting made the client print "exec request failed on channel 0", so the reason was unreadable. Verified by hand as well as by test: init -> user -> add-key -> repo -> serve, push over SSH, anonymous HTTP clone, web browse, a private repo answering 404 on both anonymous surfaces while still cloning over SSH, a refused `sh -c` that prints why and executes nothing, and a plain clone of system.git yielding the whole roster with no side database present.
README.md +33−15
| @@ -18,17 +18,24 @@ See [docs/roadmap.md](docs/roadmap.md) for the phased implementation plan. | |||
| 18 | 18 | ||
| 19 | 19 | ## Status | |
| 20 | 20 | ||
| 21 | **Phase 1 is implemented.** Vole is a working git remote with access control. | ||
| 21 | **Phase 1 is implemented.** Vole is a working forge: writes go over SSH and are | ||
| 22 | authenticated by your key; reads are open. | ||
| 22 | 23 | ||
| 23 | 24 | Working today: | |
| 24 | 25 | ||
| 25 | - Smart HTTP clone / push / fetch with a stock `git` client | ||
| 26 | - Users, personal access tokens, and web sessions | ||
| 26 | - `git push` / `fetch` / `clone` over SSH, authenticated by a registered SSH key. | ||
| 27 | There is no password, token, or session anywhere in the system | ||
| 28 | - A read-only web interface for public repositories, with no login | ||
| 29 | - Anonymous HTTP clone for public repositories (useful for CI, or a network that | ||
| 30 | blocks port 22) | ||
| 27 | 31 | - Per-repo roles (read / write / admin) stored *in the repository* at | |
| 28 | 32 | `refs/vole/acl`, so access rules travel with a clone | |
| 29 | 33 | - Policy enforced in `pre-receive`, so a rejected push moves no refs at all | |
| 30 | - Protected refs, per-blob size limits, push size cap, push concurrency limit | ||
| 31 | - Repo create / rename / delete, and a web browser for files and history | ||
| 34 | - Protected refs, per-blob size limits, and a push cap enforced by git's own | ||
| 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 | ||
| 38 | - Repo create / rename / delete from the CLI | ||
| 32 | 39 | - A derived SQLite index that can be deleted and rebuilt from the repos | |
| 33 | 40 | - An instance ed25519 key generated at first boot (used for metadata signing in | |
| 34 | 41 | Phase 7) | |
| @@ -40,26 +47,37 @@ Not yet: issues, change requests, CI, LFS, static sites, signature verification. | |||
| 40 | 47 | ```sh | |
| 41 | 48 | go build -o vole ./cmd/vole | |
| 42 | 49 | ||
| 43 | ./vole init -dir . # writes vole.toml | ||
| 44 | VOLE_PASSWORD=correct-horse ./vole user create you | ||
| 50 | ./vole init -dir . # config + data/system.git | ||
| 51 | ./vole user create you | ||
| 52 | ./vole user add-key you -key ~/.ssh/id_ed25519.pub | ||
| 45 | 53 | ./vole repo create demo -owner you | |
| 46 | ./vole serve # http://127.0.0.1:8080 | ||
| 54 | ./vole acl default demo read # publish: 404 until you do this | ||
| 55 | ./vole serve # ssh :2222, http :8080 | ||
| 47 | 56 | ``` | |
| 48 | 57 | ||
| 49 | Create a token to use as your git password: | ||
| 58 | Then push with your key, and read without one: | ||
| 50 | 59 | ||
| 51 | 60 | ```sh | |
| 52 | ./vole user token you -scope write | ||
| 53 | git clone http://you:<token>@127.0.0.1:8080/git/demo.git | ||
| 61 | git clone ssh://git@127.0.0.1:2222/demo.git # read/write, key required | ||
| 62 | git clone http://127.0.0.1:8080/git/demo.git # read only, no credentials | ||
| 54 | 63 | ``` | |
| 55 | 64 | ||
| 56 | A single-user instance treats that user as the instance admin, so a fresh | ||
| 57 | install is usable without editing config. On a multi-user instance, list | ||
| 58 | admins under `admin_users` in `vole.toml`. | ||
| 65 | Repositories are private by default. A private repo is a `404` on the web and | ||
| 66 | over HTTP — not a `403` — so the read-only surface never confirms that it exists. | ||
| 67 | |||
| 68 | ## Backup | ||
| 69 | |||
| 70 | Two things, and both are small: | ||
| 71 | |||
| 72 | ```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 | ||
| 75 | ``` | ||
| 76 | |||
| 77 | Losing the key files means re-issuing them, not losing data. | ||
| 59 | 78 | ||
| 60 | 79 | Run everything before pushing: | |
| 61 | 80 | ||
| 62 | 81 | ```sh | |
| 63 | 82 | ./scripts/ci.sh | |
| 64 | 83 | ``` | |
| 65 | |||