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.

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