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.

Delete the instance signing key

5d6db0240d02ffd70acff6ee98b3a1d521353e85 · Keenan Nemetz · 16d ago · parent 22528ac3

Delete the instance signing key

The key existed to sign run records, and runner signing made it pointless — but
deleting it is not tidying. An instance-signed anything can only be checked by
trusting the instance, so a mirror or a third party cannot verify it: trust would sit
on the box. With users, runners and approvers each holding a key, trust sits on the
parties, and the server is a relay that can withhold service but cannot forge a
result. That is a property worth having, and the key was the one thing contradicting
it.

So: `internal/signing` is gone, `Config.InstanceKey()` is gone, the server no longer
loads a key at boot, and `/healthz` reports only the user count instead of a
fingerprint nothing verifies. The host key is now the only secret the instance holds,
which is what the backup section of the README says.

`go mod tidy` changes nothing: the package only used the standard library.

Verified: scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...,
including TestHealthzReportsUsers against the new body and the full end-to-end suite
that starts the server from a fresh data directory).

docs/roadmap.md +5−3

@@ -81,9 +81,10 @@ The unglamorous foundation. If this phase isn't solid, nothing above it is trust
81 81 - ACL v1: owner / write / read, stored on `refs/vole/acl`
82 82 - `pre-receive` + `update` hooks enforcing ACLs, protected branches, and object size limits
83 83 - Web UI: repo list, tree/blob browser, raw view, commit log
84 - Instance signing key generated at first boot and persisted. It no longer signs run
85 records — those are signed by the runner that ran them (Phase 4 slice 4) — so its
86 remaining job is naming the instance at `/health` and on boot
84 - ~~Instance signing key generated at first boot and persisted~~ — removed. Nothing
85 the instance writes is signed by the instance: run records are signed by the runner
86 that ran them (Phase 4 slice 4), and a signature only the instance can check is not
87 evidence. The host key is now the only secret on the box
87 88 - `vole index rebuild` implemented early and exercised by tests, even though the index has
88 89 little to hold yet
89 90
@@ -855,6 +856,7 @@ Filled in as phases land. Append; don't rewrite.
855 856 | 2026-10-23 | The author of a metadata write is the account the key resolves to, asked with `vole-whoami`; `--author` is gone | `git config user.name` is a name somebody typed for commits: "Keenan Nemetz" sanitises to `keenan-nemetz`, which is not an account, so every issue filed from a normal machine was attributed to nobody and nothing complained. The instance is the only place that maps a key to an account, and it is one SSH verb away on the connection already open. A fallback exists for `--no-push` with the instance unreachable, and it warns loudly rather than guessing silently |
856 857 | 2026-10-23 | A remote given as an SSH URL is fetched like a named one | Skipping the fetch for a URL is what made `--remote ssh://…` write on a branch that had never been updated: the first write in a fresh repository succeeded (nothing to fast-forward from) and every later one failed as a "lost race" that never happened, re-applying on the same stale tip and reporting "already exists". `git init` plus a URL is now the real no-clone filing path |
857 858 | 2026-10-23 | The instance tells the client where it is served (`public_url`), so a command can print the page it wrote | A command that answers with an id leaves the reader to assemble a URL, and the instance is the only party that knows its own address behind a proxy. Optional: unset means ids only, which is what it printed before |
859 | 2026-10-23 | The instance holds no signing key at all | The instance key existed to sign run records, and runner signing (Phase 4 slice 4) made it pointless. Deleting it is not tidying: an instance-signed anything is a statement that can only be checked by trusting the instance, so a mirror or a third party cannot verify it — trust would sit on the box. With users, runners and approvers each holding a key, trust sits on the parties, and the server becomes a relay that can withhold service but cannot forge a result. `/healthz` no longer reports a key fingerprint, and the host key is the only secret the instance stores |
858 860 | 2026-10-23 | A finished run is signed by the runner that ran it, not by the instance | An instance-key signature is the server witnessing itself: it cannot be checked without trusting the box that produced it, and it says nothing about the party that actually ran the steps. The runner already has a key — the one it connected with — so the claim binds repo, ref, SHA, workflow blob, step digest, exit, log object, artifacts and the runner's own finishing time, and the server verifies it against the account the SSH channel authenticated as. Consequences: the runner must have its private key on disk (nothing signs arbitrary bytes through an agent), an unsigned finish is refused, and "signed" still only means "attributed" — a runner can lie about an exit code. Whether a required check must be attested by a particular runner is the next policy question, deliberately not answered here |
859 861 | 2026-10-23 | A run log is uploaded by the runner, one object per attempt; nothing is streamed | Streaming made the server the author of bytes the runner produced: heartbeats shipped chunks, the server concatenated them in a SQLite column and hashed the result, so the archived object's id was the server's, not the writer's — and a signed record could never bind it. A log is now uploaded through the same put-object path as an artifact, and the store is the authority on whether the bytes exist. The cost is a live tail: nothing is visible until the attempt ends, and an attempt that is killed leaves no log at all (the attempt that hung is exactly the log somebody wants). Accepted, because a log that can be shown to be the runner's is worth more than one that arrives early |
860 862 | 2026-10-23 | A review comment anchors on the blob of the content reviewed, and the page decides whether it is stale | A line number is not an identity: the file moves and the number points at different code. A commit id is too coarse — any unrelated change to the branch would invalidate every anchor. The blob is content-addressed, so a comment stays glued to exactly what was read, current for as long as the file is unchanged, and an anchor that lies can only make itself stale. It also means nothing has to be checked at the push boundary: the page resolves the blob and compares, so a later head never retroactively rejects an old comment |